Opensandbox (#6657)

* Opensandbox (#6651)

* volumn manager

* feat: opensandbox volumn

* perf: action (#6654)

* perf: action

* doc

* doc

* deploy tml

* update template
This commit is contained in:
Archer
2026-03-26 18:25:57 +08:00
committed by GitHub
parent d0f96723ea
commit cc3a91d009
114 changed files with 1966 additions and 953 deletions
@@ -0,0 +1,373 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { ProcessPool } from '../../src/pool/process-pool';
import { PythonProcessPool } from '../../src/pool/python-process-pool';
let jsPool: ProcessPool;
let pyPool: PythonProcessPool;
beforeAll(async () => {
jsPool = new ProcessPool(1);
await jsPool.init();
pyPool = new PythonProcessPool(1);
await pyPool.init();
});
afterAll(async () => {
await jsPool.shutdown();
await pyPool.shutdown();
});
describe('边界测试 - JS', () => {
// ===== 空/特殊代码 =====
it('空代码(无 main 函数)', async () => {
const result = await jsPool.execute({ code: '', variables: {} });
expect(result.success).toBe(false);
});
it('main 不是函数', async () => {
const result = await jsPool.execute({
code: `const main = 42;`,
variables: {}
});
expect(result.success).toBe(false);
});
it('main 返回 undefined', async () => {
const result = await jsPool.execute({
code: `async function main() { }`,
variables: {}
});
expect(result.success).toBe(true);
});
it('main 返回 null', async () => {
const result = await jsPool.execute({
code: `async function main() { return null; }`,
variables: {}
});
expect(result.success).toBe(true);
});
// ===== 大数据 =====
it('大量 console.log 输出', async () => {
const result = await jsPool.execute({
code: `async function main() {
for (let i = 0; i < 1000; i++) {
console.log('line ' + i);
}
return { done: true };
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.log).toContain('line 0');
expect(result.data?.log).toContain('line 999');
});
it('大对象返回', async () => {
const result = await jsPool.execute({
code: `async function main() {
const arr = [];
for (let i = 0; i < 10000; i++) arr.push(i);
return { count: arr.length, first: arr[0], last: arr[9999] };
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.count).toBe(10000);
});
// ===== 变量传递 =====
it('特殊字符变量', async () => {
const result = await jsPool.execute({
code: `async function main(vars) {
return { name: vars.name };
}`,
variables: { name: '你好\n"world"<script>alert(1)</script>' }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.name).toBe('你好\n"world"<script>alert(1)</script>');
});
it('嵌套对象变量', async () => {
const result = await jsPool.execute({
code: `async function main(vars) {
return { deep: vars.a.b.c };
}`,
variables: { a: { b: { c: 42 } } }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.deep).toBe(42);
});
it('数组变量', async () => {
const result = await jsPool.execute({
code: `async function main(vars) {
return { len: vars.items.length, first: vars.items[0] };
}`,
variables: { items: [1, 2, 3] }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.len).toBe(3);
});
});
describe('边界测试 - Python', () => {
// ===== 空/特殊代码 =====
it('空代码', async () => {
const result = await pyPool.execute({ code: '', variables: {} });
expect(result.success).toBe(false);
});
it('main 不是函数', async () => {
const result = await pyPool.execute({
code: `main = 42`,
variables: {}
});
expect(result.success).toBe(false);
});
it('main 返回 None', async () => {
const result = await pyPool.execute({
code: `def main():
pass`,
variables: {}
});
expect(result.success).toBe(true);
});
// ===== 大数据 =====
it('大量 print 输出', async () => {
const result = await pyPool.execute({
code: `def main():
for i in range(1000):
print(f'line {i}')
return {'done': True}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.log).toContain('line 0');
expect(result.data?.log).toContain('line 999');
});
it('大列表返回', async () => {
const result = await pyPool.execute({
code: `def main():
arr = list(range(10000))
return {'count': len(arr), 'first': arr[0], 'last': arr[-1]}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.count).toBe(10000);
});
// ===== 变量传递 =====
it('特殊字符变量', async () => {
const result = await pyPool.execute({
code: `def main(vars):
return {'name': vars['name']}`,
variables: { name: '你好\n"world"<script>alert(1)</script>' }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.name).toBe('你好\n"world"<script>alert(1)</script>');
});
it('嵌套字典变量', async () => {
const result = await pyPool.execute({
code: `def main(vars):
return {'deep': vars['a']['b']['c']}`,
variables: { a: { b: { c: 42 } } }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.deep).toBe(42);
});
it('列表变量', async () => {
const result = await pyPool.execute({
code: `def main(vars):
return {'len': len(vars['items']), 'first': vars['items'][0]}`,
variables: { items: [1, 2, 3] }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.len).toBe(3);
});
// ===== 类型处理 =====
it('返回非 JSON 可序列化对象(set', async () => {
const result = await pyPool.execute({
code: `def main():
return {'items': list({1, 2, 3})}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.items).toHaveLength(3);
});
it('返回 datetime 对象(default=str 处理)', async () => {
const result = await pyPool.execute({
code: `from datetime import datetime
def main():
return {'now': datetime(2024, 1, 1, 12, 0, 0)}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.now).toContain('2024');
});
// ===== 补充:更多边界场景 =====
it('超长变量字符串', async () => {
const longStr = 'a'.repeat(100000);
const result = await pyPool.execute({
code: `def main(v):
return {'len': len(v['text'])}`,
variables: { text: longStr }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.len).toBe(100000);
});
it('变量包含特殊 JSON 字符', async () => {
const result = await pyPool.execute({
code: `def main(v):
return {'text': v['text']}`,
variables: { text: 'line1\nline2\ttab\\backslash"quote' }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.text).toContain('line1');
expect(result.data?.codeReturn.text).toContain('\\');
});
it('返回浮点数精度', async () => {
const result = await pyPool.execute({
code: `def main():
return {'val': 0.1 + 0.2}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.val).toBeCloseTo(0.3, 10);
});
it('返回非常大的整数', async () => {
const result = await pyPool.execute({
code: `def main():
return {'big': 2 ** 53}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.big).toBe(9007199254740992);
});
it('缺少必需参数的 main 函数', async () => {
const result = await pyPool.execute({
code: `def main(a, b, c):
return {'sum': a + b + c}`,
variables: { a: 1, b: 2 } // 缺少 c
});
expect(result.success).toBe(false);
expect(result.message).toContain('Missing');
});
});
describe('边界测试 - JS 补充', () => {
it('超长变量字符串', async () => {
const longStr = 'a'.repeat(100000);
const result = await jsPool.execute({
code: `async function main(v) {
return { len: v.text.length };
}`,
variables: { text: longStr }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.len).toBe(100000);
});
it('变量包含特殊 JSON 字符', async () => {
const result = await jsPool.execute({
code: `async function main(v) {
return { text: v.text };
}`,
variables: { text: 'line1\nline2\ttab\\backslash"quote' }
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.text).toContain('line1');
});
it('返回浮点数精度', async () => {
const result = await jsPool.execute({
code: `async function main() {
return { val: 0.1 + 0.2 };
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.val).toBeCloseTo(0.3, 10);
});
it('Promise.reject 被正确捕获', async () => {
const result = await jsPool.execute({
code: `async function main() {
await Promise.reject(new Error('rejected'));
}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('rejected');
});
it('setTimeout 在沙盒中可用', async () => {
const result = await jsPool.execute({
code: `async function main() {
return new Promise(resolve => {
setTimeout(() => resolve({ ok: true }), 50);
});
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.ok).toBe(true);
});
it('JSON 循环引用返回错误', async () => {
const result = await jsPool.execute({
code: `async function main() {
const obj = {};
obj.self = obj;
return obj;
}`,
variables: {}
});
// JSON.stringify 循环引用会抛错
expect(result.success).toBe(false);
});
it('缺少 main 函数', async () => {
const result = await jsPool.execute({
code: `const x = 42;`,
variables: {}
});
expect(result.success).toBe(false);
});
it('async 函数中 try/catch 正常工作', async () => {
const result = await jsPool.execute({
code: `async function main() {
try {
JSON.parse('invalid json');
} catch(e) {
return { caught: true, msg: e.message };
}
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.caught).toBe(true);
});
});
@@ -0,0 +1,961 @@
/**
* ProcessPool / PythonProcessPool 单元测试
*
* 覆盖进程池核心逻辑:
* - 生命周期(init / shutdown / stats
* - Worker 崩溃自动恢复(respawn
* - 池满排队行为
* - 并发正确性
* - shutdown 后行为
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ProcessPool } from '../../src/pool/process-pool';
import { PythonProcessPool } from '../../src/pool/python-process-pool';
// ============================================================
// JS ProcessPool
// ============================================================
describe('ProcessPool 生命周期', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('init 后 stats 正确', async () => {
pool = new ProcessPool(2);
await pool.init();
const s = pool.stats;
expect(s.total).toBe(2);
expect(s.idle).toBe(2);
expect(s.busy).toBe(0);
expect(s.queued).toBe(0);
expect(s.poolSize).toBe(2);
});
it('shutdown 后 stats 归零', async () => {
pool = new ProcessPool(2);
await pool.init();
await pool.shutdown();
const s = pool.stats;
expect(s.total).toBe(0);
expect(s.idle).toBe(0);
expect(s.busy).toBe(0);
});
it('execute 后 worker 归还到 idle', async () => {
pool = new ProcessPool(1);
await pool.init();
await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
const s = pool.stats;
expect(s.idle).toBe(1);
expect(s.busy).toBe(0);
});
});
describe('ProcessPool Worker 恢复', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('worker 崩溃后自动 respawn,后续请求正常', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
// 让 worker 崩溃(process.exit
const result = await pool.execute({
code: `async function main() { process.exit(1); }`,
variables: {}
});
expect(result.success).toBe(false);
// 等 respawn 完成
await new Promise((r) => setTimeout(r, 1500));
// 新 worker 应该可用
const result2 = await pool.execute({
code: `async function main() { return { recovered: true }; }`,
variables: {}
});
expect(result2.success).toBe(true);
expect(result2.data?.codeReturn.recovered).toBe(true);
});
it('超时后 worker 被 kill 并 respawn', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() { while(true) {} }`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('timed out');
// 等 respawn
await new Promise((r) => setTimeout(r, 1500));
const result2 = await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
expect(result2.success).toBe(true);
});
});
describe('ProcessPool 并发与排队', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('pool size=23 个并发请求,1 个排队', async () => {
pool = new ProcessPool(2);
await pool.init();
// 3 个并发,每个 sleep 200ms
const promises = Array.from({ length: 3 }, (_, i) =>
pool.execute({
code: `async function main(v) { await new Promise(r => setTimeout(r, 200)); return { idx: v.idx }; }`,
variables: { idx: i }
})
);
const results = await Promise.all(promises);
for (let i = 0; i < 3; i++) {
expect(results[i].success).toBe(true);
expect(results[i].data?.codeReturn.idx).toBe(i);
}
});
it('pool size=1,10 个并发请求全部正确完成(串行排队)', async () => {
pool = new ProcessPool(1);
await pool.init();
const promises = Array.from({ length: 10 }, (_, i) =>
pool.execute({
code: `async function main(v) { return { n: v.n * 2 }; }`,
variables: { n: i }
})
);
const results = await Promise.all(promises);
for (let i = 0; i < 10; i++) {
expect(results[i].success).toBe(true);
expect(results[i].data?.codeReturn.n).toBe(i * 2);
}
});
it('pool size=2,并发中 1 个崩溃不影响其他请求', async () => {
pool = new ProcessPool(2);
await pool.init();
const p1 = pool.execute({
code: `async function main() { process.exit(1); }`,
variables: {}
});
const p2 = pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
const [r1, r2] = await Promise.all([p1, p2]);
expect(r1.success).toBe(false);
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.ok).toBe(true);
});
});
// ============================================================
// JS ProcessPool - Worker Ping/Pong 健康检查
// ============================================================
describe('ProcessPool Worker 健康检查 (ping/pong)', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('worker 正常响应 ping 后仍可执行任务', async () => {
pool = new ProcessPool(1);
await pool.init();
// 先执行一个任务确认正常
const r1 = await pool.execute({
code: `async function main() { return { step: 1 }; }`,
variables: {}
});
expect(r1.success).toBe(true);
expect(r1.data?.codeReturn.step).toBe(1);
// 触发健康检查(通过 triggerHealthCheck
(pool as any).pingWorker((pool as any).idleWorkers[0]);
// 等 ping/pong 完成
await new Promise((r) => setTimeout(r, 500));
// 再执行一个任务确认 worker 没被误杀
const r2 = await pool.execute({
code: `async function main() { return { step: 2 }; }`,
variables: {}
});
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.step).toBe(2);
expect(pool.stats.total).toBe(1);
});
it('连续多次 ping 不影响 worker 状态', async () => {
pool = new ProcessPool(2);
await pool.init();
// 对所有 idle worker 连续 ping 3 次
for (let i = 0; i < 3; i++) {
for (const w of [...(pool as any).idleWorkers]) {
(pool as any).pingWorker(w);
}
await new Promise((r) => setTimeout(r, 300));
}
// 所有 worker 应该还在
expect(pool.stats.total).toBe(2);
expect(pool.stats.idle).toBe(2);
// 执行任务确认功能正常
const result = await pool.execute({
code: `async function main() { return { alive: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
});
});
// ============================================================
// JS ProcessPool - shutdown reject waiters
// ============================================================
describe('ProcessPool shutdown reject waiters', () => {
it('shutdown 后 waitQueue 中的请求被 reject', async () => {
const pool = new ProcessPool(1);
await pool.init();
// 发起一个长时间运行的任务占住唯一 worker
const p1 = pool.execute({
code: `async function main() { await new Promise(r => setTimeout(r, 3000)); return { done: true }; }`,
variables: {}
});
// 等一下确保 p1 已经拿到 worker
await new Promise((r) => setTimeout(r, 200));
// 发起第二个请求,它会进入 waitQueue
const p2 = pool.execute({
code: `async function main() { return { queued: true }; }`,
variables: {}
});
// 确认有排队请求
expect(pool.stats.queued).toBe(1);
// shutdown 应该 reject waitQueue 中的请求
await pool.shutdown();
// p2 应该被 reject
await expect(p2).rejects.toThrow('shutting down');
// p1 可能成功也可能因 worker 被 kill 而失败,不关心
await p1.catch(() => {});
});
});
// ============================================================
// JS ProcessPool - 返回值序列化与参数校验(原 base-runner.test.ts
// ============================================================
describe('ProcessPool 返回值序列化与参数校验', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('JS main 返回 undefined 序列化为 null', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() { return undefined; }`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn).toBeNull();
});
it('JS main 无 return 语句序列化为 null', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() { const x = 1; }`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn).toBeNull();
});
it('code 为非字符串类型返回错误', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: 123 as any,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('empty');
});
it('code 为 null 返回错误', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: null as any,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('empty');
});
});
// ============================================================
// JS + Python 混合并发(原 base-runner.test.ts
// ============================================================
describe('JS + Python 混合并发', () => {
let jsPool: ProcessPool;
let pyPool: PythonProcessPool;
afterEach(async () => {
try {
await jsPool?.shutdown();
await pyPool?.shutdown();
} catch {}
});
it('JS 和 Python 混合并发执行', async () => {
jsPool = new ProcessPool(1);
await jsPool.init();
pyPool = new PythonProcessPool(1);
await pyPool.init();
const jsPromise = jsPool.execute({
code: `async function main() { return { lang: 'js' }; }`,
variables: {}
});
const pyPromise = pyPool.execute({
code: `def main():\n return {'lang': 'python'}`,
variables: {}
});
const [jsResult, pyResult] = await Promise.all([jsPromise, pyPromise]);
expect(jsResult.success).toBe(true);
expect(jsResult.data?.codeReturn.lang).toBe('js');
expect(pyResult.success).toBe(true);
expect(pyResult.data?.codeReturn.lang).toBe('python');
});
});
// ============================================================
// JS ProcessPool - 健康检查失败路径
// ============================================================
describe('ProcessPool 健康检查失败路径', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('ping timeout: worker 不响应 pong 时被替换', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
// 拦截 stdin.write 使 ping 消息不到达 worker(但不关闭 stdin),从而触发真正的 timeout
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let interceptPing = true;
worker.proc.stdin!.write = (...args: any[]) => {
if (interceptPing) {
interceptPing = false;
return true; // 假装写成功但实际不发送
}
return origWrite(...args);
};
// 触发 ping
(pool as any).pingWorker(worker);
// 等待 HEALTH_CHECK_TIMEOUT (5s) + respawn
await new Promise((r) => setTimeout(r, 8000));
// worker 应该被替换,池仍然有 1 个 worker
expect(pool.stats.total).toBe(1);
// 新 worker 应该可用
const result = await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
}, 15000);
it('stdin not writable: worker stdin 关闭时被替换', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
// 销毁 stdin 使其 writable = false
worker.proc.stdin!.destroy();
// 触发 ping
(pool as any).pingWorker(worker);
// 等 respawn
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `async function main() { return { replaced: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('health check invalid response: worker 返回错误类型时被替换', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let intercepted = false;
worker.proc.stdin!.write = (...args: any[]) => {
if (!intercepted) {
intercepted = true;
setTimeout(() => worker.rl.emit('line', JSON.stringify({ type: 'wrong' })), 50);
return true;
}
return origWrite(...args);
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `async function main() { return { invalidResp: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('returnToIdle with waiter: ping 期间有等待请求时直接分配', async () => {
pool = new ProcessPool(1);
await pool.init();
const worker = (pool as any).idleWorkers[0];
(pool as any).pingWorker(worker);
// ping 期间 worker 不在 idle 中,新请求进入 waitQueue
// ping 成功后 returnToIdle 检查 waitQueue 并直接分配
const p1 = pool.execute({
code: `async function main() { return { fromWaiter: true }; }`,
variables: {}
});
const result = await p1;
expect(result.success).toBe(true);
expect(result.data?.codeReturn.fromWaiter).toBe(true);
});
it('health check parse error: worker 返回非 JSON 时被替换', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let intercepted = false;
worker.proc.stdin!.write = (...args: any[]) => {
if (!intercepted) {
intercepted = true;
setTimeout(() => worker.rl.emit('line', 'not-json-at-all'), 50);
return true;
}
return origWrite(...args);
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `async function main() { return { parseError: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('health check write error: stdin.write 抛异常时被替换', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
// 让 stdin.write 抛异常,但 writable 仍为 true
worker.proc.stdin!.write = () => {
throw new Error('mock write error');
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `async function main() { return { writeError: true }; }`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('returnToIdle with waiter: ping 成功后分配给等待中的请求', async () => {
pool = new ProcessPool(1);
await pool.init();
// 发起一个长任务占住 worker
const p1 = pool.execute({
code: `async function main() { await new Promise(r => setTimeout(r, 1000)); return { first: true }; }`,
variables: {}
});
// 等 p1 拿到 worker
await new Promise((r) => setTimeout(r, 100));
// 发起第二个请求,它会进入 waitQueue
const p2 = pool.execute({
code: `async function main() { return { second: true }; }`,
variables: {}
});
// 确认有排队
expect(pool.stats.queued).toBe(1);
// 等 p1 完成,p2 应该自动被分配
const [r1, r2] = await Promise.all([p1, p2]);
expect(r1.success).toBe(true);
expect(r1.data?.codeReturn.first).toBe(true);
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.second).toBe(true);
});
});
// ============================================================
// Python PythonProcessPool - Worker Ping/Pong 健康检查
// ============================================================
describe('PythonProcessPool Worker 健康检查 (ping/pong)', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('worker 正常响应 ping 后仍可执行任务', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const r1 = await pool.execute({
code: `def main():\n return {'step': 1}`,
variables: {}
});
expect(r1.success).toBe(true);
expect(r1.data?.codeReturn.step).toBe(1);
// 触发 ping
(pool as any).pingWorker((pool as any).idleWorkers[0]);
await new Promise((r) => setTimeout(r, 500));
const r2 = await pool.execute({
code: `def main():\n return {'step': 2}`,
variables: {}
});
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.step).toBe(2);
expect(pool.stats.total).toBe(1);
});
it('连续多次 ping 不影响 worker 状态', async () => {
pool = new PythonProcessPool(2);
await pool.init();
for (let i = 0; i < 3; i++) {
for (const w of [...(pool as any).idleWorkers]) {
(pool as any).pingWorker(w);
}
await new Promise((r) => setTimeout(r, 300));
}
expect(pool.stats.total).toBe(2);
expect(pool.stats.idle).toBe(2);
const result = await pool.execute({
code: `def main():\n return {'alive': True}`,
variables: {}
});
expect(result.success).toBe(true);
});
});
// ============================================================
// Python PythonProcessPool - 健康检查失败路径
// ============================================================
describe('PythonProcessPool 健康检查失败路径', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('ping timeout: worker 不响应 pong 时被替换', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
// 拦截 stdin.write 使 ping 不到达 worker,触发真正的 timeout
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let interceptPing = true;
worker.proc.stdin!.write = (...args: any[]) => {
if (interceptPing) {
interceptPing = false;
return true;
}
return origWrite(...args);
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 8000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
expect(result.success).toBe(true);
}, 15000);
it('stdin not writable: worker stdin 关闭时被替换', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
worker.proc.stdin!.destroy();
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `def main():\n return {'replaced': True}`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('health check invalid response: worker 返回错误类型时被替换', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let intercepted = false;
worker.proc.stdin!.write = (...args: any[]) => {
if (!intercepted) {
intercepted = true;
setTimeout(() => worker.rl.emit('line', JSON.stringify({ type: 'wrong' })), 50);
return true;
}
return origWrite(...args);
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `def main():\n return {'invalidResp': True}`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('returnToIdle with waiter: ping 期间有等待请求时直接分配', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const worker = (pool as any).idleWorkers[0];
(pool as any).pingWorker(worker);
const p1 = pool.execute({
code: `def main():\n return {'fromWaiter': True}`,
variables: {}
});
const result = await p1;
expect(result.success).toBe(true);
expect(result.data?.codeReturn.fromWaiter).toBe(true);
});
it('health check parse error: worker 返回非 JSON 时被替换', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
let intercepted = false;
worker.proc.stdin!.write = (...args: any[]) => {
if (!intercepted) {
intercepted = true;
setTimeout(() => worker.rl.emit('line', 'not-json'), 50);
return true;
}
return origWrite(...args);
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `def main():\n return {'parseError': True}`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
it('health check write error: stdin.write 抛异常时被替换', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const worker = (pool as any).idleWorkers[0];
worker.proc.stdin!.write = () => {
throw new Error('mock write error');
};
(pool as any).pingWorker(worker);
await new Promise((r) => setTimeout(r, 3000));
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `def main():\n return {'writeError': True}`,
variables: {}
});
expect(result.success).toBe(true);
}, 10000);
});
// ============================================================
// Python PythonProcessPool - shutdown reject waiters
// ============================================================
describe('PythonProcessPool shutdown reject waiters', () => {
it('shutdown 后 waitQueue 中的请求被 reject', async () => {
const pool = new PythonProcessPool(1);
await pool.init();
// 发起一个长时间运行的任务占住唯一 worker
const p1 = pool.execute({
code: `import time\ndef main():\n time.sleep(3)\n return {'done': True}`,
variables: {}
});
// 等一下确保 p1 已经拿到 worker
await new Promise((r) => setTimeout(r, 200));
// 发起第二个请求,它会进入 waitQueue
const p2 = pool.execute({
code: `def main():\n return {'queued': True}`,
variables: {}
});
// 确认有排队请求
expect(pool.stats.queued).toBe(1);
// shutdown 应该 reject waitQueue 中的请求
await pool.shutdown();
// p2 应该被 reject
await expect(p2).rejects.toThrow('shutting down');
// p1 可能成功也可能因 worker 被 kill 而失败,不关心
await p1.catch(() => {});
});
});
// ============================================================
// Python PythonProcessPool
// ============================================================
describe('PythonProcessPool 生命周期', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('init 后 stats 正确', async () => {
pool = new PythonProcessPool(2);
await pool.init();
const s = pool.stats;
expect(s.total).toBe(2);
expect(s.idle).toBe(2);
expect(s.busy).toBe(0);
expect(s.queued).toBe(0);
expect(s.poolSize).toBe(2);
});
it('shutdown 后 stats 归零', async () => {
pool = new PythonProcessPool(2);
await pool.init();
await pool.shutdown();
const s = pool.stats;
expect(s.total).toBe(0);
expect(s.idle).toBe(0);
expect(s.busy).toBe(0);
});
it('execute 后 worker 归还到 idle', async () => {
pool = new PythonProcessPool(1);
await pool.init();
await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
const s = pool.stats;
expect(s.idle).toBe(1);
expect(s.busy).toBe(0);
});
});
describe('PythonProcessPool Worker 恢复', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('超时后 worker 被 kill 并 respawn', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n while True:\n pass`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('timed out');
// 等 respawn
await new Promise((r) => setTimeout(r, 2000));
const result2 = await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
expect(result2.success).toBe(true);
});
});
describe('PythonProcessPool 并发与排队', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('pool size=23 个并发请求,1 个排队', async () => {
pool = new PythonProcessPool(2);
await pool.init();
const promises = Array.from({ length: 3 }, (_, i) =>
pool.execute({
code: `import time\ndef main(variables):\n time.sleep(0.2)\n return {'idx': variables['idx']}`,
variables: { idx: i }
})
);
const results = await Promise.all(promises);
for (let i = 0; i < 3; i++) {
expect(results[i].success).toBe(true);
expect(results[i].data?.codeReturn.idx).toBe(i);
}
});
it('pool size=1,10 个并发请求全部正确完成(串行排队)', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const promises = Array.from({ length: 10 }, (_, i) =>
pool.execute({
code: `def main(variables):\n return {'n': variables['n'] * 2}`,
variables: { n: i }
})
);
const results = await Promise.all(promises);
for (let i = 0; i < 10; i++) {
expect(results[i].success).toBe(true);
expect(results[i].data?.codeReturn.n).toBe(i * 2);
}
});
});
@@ -0,0 +1,662 @@
/**
* 资源限制测试
*
* 覆盖:
* - 内存限制(RSS 轮询监控)
* - CPU 密集型超时(JS / Python
* - 运行时长限制(wall-clock timeout 验证)
* - 网络请求限制(次数、请求体大小、响应大小)
*/
import { describe, it, expect, afterEach, beforeAll } from 'vitest';
import { ProcessPool } from '../../src/pool/process-pool';
import { PythonProcessPool } from '../../src/pool/python-process-pool';
import { config } from '../../src/config';
beforeAll(async () => {
console.log(`\n=== Memory Limit Test Status ===`);
console.log(`Method: RSS polling (cross-platform)`);
console.log(`User configured memory: ${config.maxMemoryMB}MB`);
console.log(
`Actual process limit: ${config.maxMemoryMB + config.RUNTIME_MEMORY_OVERHEAD_MB}MB (${config.maxMemoryMB}MB user + ${config.RUNTIME_MEMORY_OVERHEAD_MB}MB runtime)`
);
console.log(`=============================\n`);
});
// ============================================================
// 1. 内存限制(RSS 轮询监控,跨平台)
// ============================================================
describe('内存限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('JS 分配超大内存被 RSS 监控终止后自动 respawn', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
// 实际限制 = 用户配置 + 运行时开销(50MB)
const actualLimitMB = config.maxMemoryMB + config.RUNTIME_MEMORY_OVERHEAD_MB;
const result = await pool.execute({
code: `async function main() {
const arr = [];
// 逐步分配内存(使用随机字符串填充,防止 OS 内存压缩优化)
// 每次 10MB,每轮等待 200ms 让 RSS 监控有机会检测
for (let i = 0; i < 40; i++) {
arr.push(Buffer.alloc(10 * 1024 * 1024, String(Date.now() + i)));
await new Promise(r => setTimeout(r, 200));
}
return { allocated: arr.length };
}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toMatch(/memory|Memory|crash|Worker|timed out/i);
// 等 respawn
await new Promise((r) => setTimeout(r, 2000));
// 新 worker 应该可用
const result2 = await pool.execute({
code: `async function main() { return { recovered: true }; }`,
variables: {}
});
expect(result2.success).toBe(true);
expect(result2.data?.codeReturn.recovered).toBe(true);
}, 30000);
it('JS 分配配置范围内的内存正常工作', async () => {
pool = new ProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
// 分配少量内存(远小于限制),确保不会被误杀
const allocMB = 10;
const result = await pool.execute({
code: `async function main() {
const arr = [];
for (let i = 0; i < ${allocMB}; i++) {
arr.push(Buffer.alloc(1024 * 1024));
}
return { allocated: arr.length, totalMB: arr.length };
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.allocated).toBe(allocMB);
}, 30000);
});
describe('Python 内存限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('Python 分配超大内存被 RSS 监控终止后自动 respawn', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const result = await pool.execute({
code: `import time\nimport random\ndef main():\n chunks = []\n for i in range(40):\n chunk = bytearray(10 * 1024 * 1024)\n chunk[0] = i % 256\n chunks.append(chunk)\n time.sleep(0.2)\n return {'size': len(chunks)}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toMatch(/memory|Memory|crash|Worker|timed out/i);
// 等 respawn
await new Promise((r) => setTimeout(r, 2000));
const result2 = await pool.execute({
code: `def main():\n return {'recovered': True}`,
variables: {}
});
expect(result2.success).toBe(true);
expect(result2.data?.codeReturn.recovered).toBe(true);
}, 30000);
it('Python 分配配置范围内的内存正常工作', async () => {
pool = new PythonProcessPool(1);
await pool.init();
expect(pool.stats.total).toBe(1);
const allocMB = 10;
const result = await pool.execute({
code: `def main():\n data = bytearray(${allocMB} * 1024 * 1024)\n return {'allocated': len(data), 'totalMB': len(data) // (1024 * 1024)}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.totalMB).toBe(allocMB);
}, 30000);
});
// ============================================================
// 2. CPU 限制
// ============================================================
describe('JS CPU 密集型超时', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('纯计算死循环被超时终止', async () => {
pool = new ProcessPool(1);
await pool.init();
const start = Date.now();
const result = await pool.execute({
code: `async function main() { while(true) { Math.random(); } }`,
variables: {}
});
const elapsed = Date.now() - start;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
// 应该在合理时间内被终止(超时 + 一些余量)
expect(elapsed).toBeLessThan(30000);
});
it('CPU 密集型计算(大量数学运算)被超时终止', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
let x = 0;
while(true) {
x += Math.sin(x) * Math.cos(x);
}
}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
});
it('CPU 超时后 worker 恢复正常', async () => {
pool = new ProcessPool(1);
await pool.init();
await pool.execute({
code: `async function main() { while(true) {} }`,
variables: {}
});
await new Promise((r) => setTimeout(r, 1500));
const r2 = await pool.execute({
code: `async function main() { return { ok: true }; }`,
variables: {}
});
expect(r2.success).toBe(true);
});
});
describe('Python CPU 密集型超时', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('纯计算死循环被超时终止', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const start = Date.now();
const result = await pool.execute({
code: `import math\ndef main():\n x = 0\n while True:\n x += math.sin(x) * math.cos(x)`,
variables: {}
});
const elapsed = Date.now() - start;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
expect(elapsed).toBeLessThan(30000);
});
it('CPU 超时后 worker 恢复正常', async () => {
pool = new PythonProcessPool(1);
await pool.init();
await pool.execute({
code: `def main():\n while True:\n pass`,
variables: {}
});
await new Promise((r) => setTimeout(r, 2000));
const r2 = await pool.execute({
code: `def main():\n return {'ok': True}`,
variables: {}
});
expect(r2.success).toBe(true);
});
});
// ============================================================
// 3. 运行时长限制(wall-clock timeout
// ============================================================
describe('JS 运行时长限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('sleep 超过 maxTimeoutMs 被终止', async () => {
pool = new ProcessPool(1);
await pool.init();
const start = Date.now();
const result = await pool.execute({
code: `async function main() {
await new Promise(r => setTimeout(r, ${config.maxTimeoutMs + 30000}));
return { done: true };
}`,
variables: {}
});
const elapsed = Date.now() - start;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
// 实际耗时应在 maxTimeoutMs 附近(加上 2s 余量),不会等到 sleep 结束
expect(elapsed).toBeLessThan(config.maxTimeoutMs + 10000);
});
it('在超时范围内完成的代码正常返回', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
await new Promise(r => setTimeout(r, 100));
return { elapsed: true };
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.elapsed).toBe(true);
});
it('delay() 超过 10s 上限被拒绝', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
await delay(15000);
return { done: true };
}`,
variables: {}
});
expect(result.success).toBe(false);
expect(result.message).toContain('10000');
});
});
describe('Python 运行时长限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('sleep 超过超时限制被终止', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const start = Date.now();
const result = await pool.execute({
code: `import time\ndef main():\n time.sleep(${Math.ceil(config.maxTimeoutMs / 1000) + 30})\n return {'done': True}`,
variables: {}
});
const elapsed = Date.now() - start;
expect(result.success).toBe(false);
expect(result.message).toMatch(/timed out|timeout/i);
expect(elapsed).toBeLessThan(config.maxTimeoutMs + 10000);
});
it('在超时范围内完成的代码正常返回', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `import time\ndef main():\n time.sleep(0.1)\n return {'elapsed': True}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.elapsed).toBe(true);
});
it('delay() 超过 10s 上限被拒绝', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n delay(15000)\n return {'done': True}`,
variables: {}
});
expect(result.success).toBe(false);
});
});
// ============================================================
// 4. 网络请求次数限制
// ============================================================
describe('JS 网络请求次数限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it(`第 maxRequests+1 次请求被拒绝(计数器验证)`, async () => {
pool = new ProcessPool(1);
await pool.init();
// 快速消耗计数器:每次 httpRequest 调用会先 ++requestCount 再发起网络请求
// 即使网络请求失败(DNS/连接),计数器也已递增
// 为避免超时,用循环快速调用并 catch 所有错误,只关注 limit 错误
const result = await pool.execute({
code: `async function main() {
let limitError = null;
for (let i = 0; i < ${config.maxRequests + 1}; i++) {
try {
await httpRequest('http://0.0.0.0:1');
} catch(e) {
if (e.message.includes('limit') || e.message.includes('Limit')) {
limitError = { idx: i, msg: e.message };
break;
}
}
}
return { limitError };
}`,
variables: {}
});
expect(result.success).toBe(true);
const le = result.data?.codeReturn.limitError;
expect(le).not.toBeNull();
expect(le.idx).toBe(config.maxRequests);
expect(le.msg).toMatch(/limit/i);
});
it('请求计数每次执行重置', async () => {
pool = new ProcessPool(1);
await pool.init();
// 第一次执行:消耗一些计数
await pool.execute({
code: `async function main() {
for (let i = 0; i < 3; i++) {
try { await httpRequest('http://0.0.0.0:1'); } catch(e) {}
}
return {};
}`,
variables: {}
});
// 第二次执行:计数应该重置,第一次请求不会触发 limit
const r2 = await pool.execute({
code: `async function main() {
let limitHit = false;
try { await httpRequest('http://0.0.0.0:1'); } catch(e) {
if (e.message.includes('limit') || e.message.includes('Limit')) limitHit = true;
}
return { limitHit };
}`,
variables: {}
});
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.limitHit).toBe(false);
});
});
describe('Python 网络请求次数限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it(`第 maxRequests+1 次请求被拒绝(计数器验证)`, async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n limit_error = None\n for i in range(${config.maxRequests + 1}):\n try:\n http_request('http://0.0.0.0:1')\n except Exception as e:\n if 'limit' in str(e).lower():\n limit_error = {'idx': i, 'msg': str(e)}\n break\n return {'limit_error': limit_error}`,
variables: {}
});
expect(result.success).toBe(true);
const le = result.data?.codeReturn.limit_error;
expect(le).not.toBeNull();
expect(le.idx).toBe(config.maxRequests);
expect(le.msg.toLowerCase()).toContain('limit');
});
it('请求计数每次执行重置', async () => {
pool = new PythonProcessPool(1);
await pool.init();
await pool.execute({
code: `def main():\n for i in range(3):\n try:\n http_request('http://0.0.0.0:1')\n except:\n pass\n return {}`,
variables: {}
});
const r2 = await pool.execute({
code: `def main():\n limit_hit = False\n try:\n http_request('http://0.0.0.0:1')\n except Exception as e:\n if 'limit' in str(e).lower():\n limit_hit = True\n return {'limit_hit': limit_hit}`,
variables: {}
});
expect(r2.success).toBe(true);
expect(r2.data?.codeReturn.limit_hit).toBe(false);
});
});
// ============================================================
// 5. 网络请求大小限制
// ============================================================
describe('JS 请求体大小限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('请求体超过 maxRequestBodySize 被拒绝', async () => {
pool = new ProcessPool(1);
await pool.init();
// maxRequestBodySize 单位是 MB,生成超过限制的 body
const sizeMB = config.maxRequestBodySize;
const result = await pool.execute({
code: `async function main() {
const bigBody = 'x'.repeat(${sizeMB} * 1024 * 1024 + 1);
try {
await httpRequest('https://example.com', { method: 'POST', body: bigBody });
return { blocked: false };
} catch(e) {
return { blocked: true, msg: e.message };
}
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
expect(result.data?.codeReturn.msg).toMatch(/body.*large|too large/i);
});
it('请求体在限制内正常发送(不因大小被拒)', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
const smallBody = JSON.stringify({ data: 'hello' });
try {
await httpRequest('https://example.com', { method: 'POST', body: smallBody });
return { sizeOk: true };
} catch(e) {
// 网络错误可以接受,但不应该是 body too large
return { sizeOk: !e.message.includes('too large'), msg: e.message };
}
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.sizeOk).toBe(true);
});
});
describe('Python 请求体大小限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('请求体超过 maxRequestBodySize 被拒绝', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const sizeMB = config.maxRequestBodySize;
const result = await pool.execute({
code: `def main():\n big_body = 'x' * (${sizeMB} * 1024 * 1024 + 1)\n try:\n http_request('https://example.com', method='POST', body=big_body)\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True, 'msg': str(e)}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
expect(result.data?.codeReturn.msg).toMatch(/body.*large|too large/i);
});
it('请求体在限制内正常发送(不因大小被拒)', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n try:\n http_request('https://example.com', method='POST', body='hello')\n return {'size_ok': True}\n except Exception as e:\n return {'size_ok': 'too large' not in str(e).lower(), 'msg': str(e)}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.size_ok).toBe(true);
});
});
// ============================================================
// 6. 网络协议限制
// ============================================================
describe('JS 网络协议限制', () => {
let pool: ProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('ftp:// 协议被拒绝', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
try {
await httpRequest('ftp://example.com/file');
return { blocked: false };
} catch(e) {
return { blocked: true, msg: e.message };
}
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
expect(result.data?.codeReturn.msg).toMatch(/protocol/i);
});
it('file:// 协议被拒绝', async () => {
pool = new ProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `async function main() {
try {
await httpRequest('file:///etc/passwd');
return { blocked: false };
} catch(e) {
return { blocked: true, msg: e.message };
}
}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
});
});
describe('Python 网络协议限制', () => {
let pool: PythonProcessPool;
afterEach(async () => {
try {
await pool?.shutdown();
} catch {}
});
it('ftp:// 协议被拒绝', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n try:\n http_request('ftp://example.com/file')\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True, 'msg': str(e)}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
expect(result.data?.codeReturn.msg.toLowerCase()).toContain('protocol');
});
it('file:// 协议被拒绝', async () => {
pool = new PythonProcessPool(1);
await pool.init();
const result = await pool.execute({
code: `def main():\n try:\n http_request('file:///etc/passwd')\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True}`,
variables: {}
});
expect(result.success).toBe(true);
expect(result.data?.codeReturn.blocked).toBe(true);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
/**
* Semaphore 信号量单元测试
*
* 测试并发控制核心逻辑:
* - 基本 acquire/release 流程
* - 超出 max 后排队等待
* - release 唤醒队列中下一个
* - stats 返回正确的 current/queued/max
* - 并发数为 1 时串行执行
* - 大量并发请求排队后依次完成
*/
import { describe, it, expect } from 'vitest';
import { Semaphore } from '../../src/utils/semaphore';
describe('Semaphore', () => {
// ===== 基本流程 =====
it('acquire 在未满时立即返回', async () => {
const sem = new Semaphore(3);
// 三次 acquire 都应该立即 resolve
await sem.acquire();
await sem.acquire();
await sem.acquire();
expect(sem.stats).toEqual({ current: 3, queued: 0, max: 3 });
});
it('release 减少 current 计数', async () => {
const sem = new Semaphore(2);
await sem.acquire();
await sem.acquire();
expect(sem.stats.current).toBe(2);
sem.release();
expect(sem.stats.current).toBe(1);
sem.release();
expect(sem.stats.current).toBe(0);
});
it('stats 返回正确的 current/queued/max', async () => {
const sem = new Semaphore(2);
expect(sem.stats).toEqual({ current: 0, queued: 0, max: 2 });
await sem.acquire();
expect(sem.stats).toEqual({ current: 1, queued: 0, max: 2 });
await sem.acquire();
expect(sem.stats).toEqual({ current: 2, queued: 0, max: 2 });
// 第三个会排队(不 await,因为它不会 resolve
const p3 = sem.acquire();
expect(sem.stats).toEqual({ current: 2, queued: 1, max: 2 });
// 第四个也排队
const p4 = sem.acquire();
expect(sem.stats).toEqual({ current: 2, queued: 2, max: 2 });
// release 唤醒队列中第一个,queued 减 1,current 不变(因为立即被新的占用)
sem.release();
await p3;
expect(sem.stats).toEqual({ current: 2, queued: 1, max: 2 });
sem.release();
await p4;
expect(sem.stats).toEqual({ current: 2, queued: 0, max: 2 });
});
// ===== 排队与唤醒 =====
it('超出 max 后排队等待,release 唤醒下一个', async () => {
const sem = new Semaphore(1);
const order: number[] = [];
await sem.acquire();
order.push(1);
// 第二个 acquire 会排队
const p2 = sem.acquire().then(() => {
order.push(2);
});
expect(sem.stats.queued).toBe(1);
// release 唤醒排队的
sem.release();
await p2;
expect(order).toEqual([1, 2]);
expect(sem.stats.queued).toBe(0);
sem.release();
expect(sem.stats.current).toBe(0);
});
it('release 按 FIFO 顺序唤醒', async () => {
const sem = new Semaphore(1);
const order: number[] = [];
await sem.acquire();
const p1 = sem.acquire().then(() => {
order.push(1);
});
const p2 = sem.acquire().then(() => {
order.push(2);
});
const p3 = sem.acquire().then(() => {
order.push(3);
});
expect(sem.stats.queued).toBe(3);
// 依次 release,应按 FIFO 顺序唤醒
sem.release();
await p1;
sem.release();
await p2;
sem.release();
await p3;
expect(order).toEqual([1, 2, 3]);
});
// ===== 并发数为 1 时串行执行 =====
it('max=1 时保证串行执行', async () => {
const sem = new Semaphore(1);
const log: string[] = [];
const task = async (name: string, delayMs: number) => {
await sem.acquire();
log.push(`${name}-start`);
await new Promise((r) => setTimeout(r, delayMs));
log.push(`${name}-end`);
sem.release();
};
// 同时启动三个任务
await Promise.all([task('A', 50), task('B', 50), task('C', 50)]);
// 串行执行:每个任务的 start 必须在前一个 end 之后
// A-start, A-end, B-start, B-end, C-start, C-end
for (let i = 0; i < log.length - 1; i += 2) {
const startIdx = i;
const endIdx = i + 1;
expect(log[endIdx]).toContain('-end');
expect(log[startIdx]).toContain('-start');
// end 在 start 之后
expect(endIdx).toBeGreaterThan(startIdx);
}
// 更严格:不能有两个 start 连续出现(说明并行了)
for (let i = 0; i < log.length - 1; i++) {
if (log[i].endsWith('-start')) {
expect(log[i + 1]).toContain('-end');
}
}
});
// ===== 大量并发 =====
it('大量并发请求排队后依次完成', async () => {
const sem = new Semaphore(3);
const total = 20;
let completed = 0;
let maxConcurrent = 0;
let currentRunning = 0;
const tasks = Array.from({ length: total }, (_, i) =>
(async () => {
await sem.acquire();
currentRunning++;
if (currentRunning > maxConcurrent) {
maxConcurrent = currentRunning;
}
// 模拟异步工作
await new Promise((r) => setTimeout(r, 10));
currentRunning--;
completed++;
sem.release();
})()
);
await Promise.all(tasks);
// 全部完成
expect(completed).toBe(total);
// 最大并发不超过 max
expect(maxConcurrent).toBeLessThanOrEqual(3);
// 最终状态归零
expect(sem.stats.current).toBe(0);
expect(sem.stats.queued).toBe(0);
});
it('max=1 大量并发严格串行', async () => {
const sem = new Semaphore(1);
const total = 10;
let maxConcurrent = 0;
let currentRunning = 0;
const tasks = Array.from({ length: total }, () =>
(async () => {
await sem.acquire();
currentRunning++;
if (currentRunning > maxConcurrent) maxConcurrent = currentRunning;
await new Promise((r) => setTimeout(r, 5));
currentRunning--;
sem.release();
})()
);
await Promise.all(tasks);
expect(maxConcurrent).toBe(1);
expect(sem.stats.current).toBe(0);
});
// ===== 边界情况 =====
it('release 无排队时 current 不会变为负数', () => {
const sem = new Semaphore(3);
// 没有 acquire 就 release
sem.release();
// current 变为 -1,这是实现的已知行为(调用者应保证配对使用)
expect(sem.stats.current).toBe(-1);
});
it('max 为很大的数时不排队', async () => {
const sem = new Semaphore(1000);
const promises = Array.from({ length: 100 }, () => sem.acquire());
await Promise.all(promises);
expect(sem.stats.current).toBe(100);
expect(sem.stats.queued).toBe(0);
});
it('acquire 返回的 Promise 是 void', async () => {
const sem = new Semaphore(1);
const result = await sem.acquire();
expect(result).toBeUndefined();
sem.release();
});
});
// ============================================================
// 竞态条件补充(原 semaphore-race.test.ts
// ============================================================
describe('Semaphore 竞态条件补充', () => {
it('release 过多后 acquire 仍能正常工作', async () => {
const sem = new Semaphore(2);
sem.release();
expect(sem.stats.current).toBe(-1);
await sem.acquire();
expect(sem.stats.current).toBe(0);
await sem.acquire();
expect(sem.stats.current).toBe(1);
await sem.acquire();
expect(sem.stats.current).toBe(2);
const p = sem.acquire();
expect(sem.stats.queued).toBe(1);
sem.release();
await p;
});
it('快速交替 acquire/release 不丢失状态', async () => {
const sem = new Semaphore(1);
for (let i = 0; i < 100; i++) {
await sem.acquire();
sem.release();
}
expect(sem.stats.current).toBe(0);
expect(sem.stats.queued).toBe(0);
});
it('异步任务异常后 release 仍被调用(模拟 try/finally', async () => {
const sem = new Semaphore(2);
const errors: string[] = [];
const task = async (shouldFail: boolean) => {
await sem.acquire();
try {
if (shouldFail) throw new Error('task failed');
return 'ok';
} catch (e: any) {
errors.push(e.message);
return 'error';
} finally {
sem.release();
}
};
const results = await Promise.all([
task(false),
task(true),
task(false),
task(true),
task(false)
]);
expect(results.filter((r) => r === 'ok')).toHaveLength(3);
expect(results.filter((r) => r === 'error')).toHaveLength(2);
expect(errors).toHaveLength(2);
expect(sem.stats.current).toBe(0);
expect(sem.stats.queued).toBe(0);
});
it('max=0 时所有 acquire 都排队', async () => {
const sem = new Semaphore(0);
const p1 = sem.acquire();
const p2 = sem.acquire();
expect(sem.stats.queued).toBe(2);
expect(sem.stats.current).toBe(0);
sem.release();
await p1;
sem.release();
await p2;
});
it('并发 acquire 后批量 release', async () => {
const sem = new Semaphore(2);
await sem.acquire();
await sem.acquire();
const waiters = Array.from({ length: 5 }, () => sem.acquire());
expect(sem.stats.queued).toBe(5);
for (let i = 0; i < 7; i++) {
sem.release();
}
await Promise.all(waiters);
expect(sem.stats.queued).toBe(0);
expect(sem.stats.current).toBe(0);
});
});