feat: 任务书 05,runner 与 tasks 两个模块

runner 用 Docker 跑一条命令并交回结局和资源用量;tasks 读 task.json、准备运行目录、给出正式运行/校验器/轻量检查的运行定义,并从原始产物独立判分。任务目录从参考项目搬到 tasks/circle_packing/,新增 task.json;vocab 的 VERDICTS 删掉"判不了",ProcessResult 的 cancelled 改为"容器从外面被杀了"。

全新上下文审阅后的两处修复:计量 shell 以容器内 root 运行,命令经 setpriv 降权,结果写在容器自己的文件层、结束后 docker cp 取出,并加 no-new-privileges,候选无法伪造计量结果把超时记成正常退出;harness 开跑前删掉旧的 solution.json、metrics.json,旧产物不会被当成本次的值。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 13:55:47 -04:00
parent 6005fb8ec2
commit cf6bc4c844
25 changed files with 3205 additions and 6 deletions
+224
View File
@@ -0,0 +1,224 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, test, type TestContext } from "node:test";
import { Runner } from "../src/runner/index.ts";
import type { ProcessResult, RunDefinition } from "../src/vocab/index.ts";
// 用真实 Docker 测(任务书 05 第五部分):没有 Docker 时整个文件失败,不跳过。
// 镜像、容器名一律 inforsi-test- 开头的固定名字;本机还有别的项目在建删容器,只动自己前缀的。
const TASK_DIR = join(import.meta.dirname, "..", "tasks", "circle_packing");
const IMAGE_TAG = "inforsi-test-circle";
const MISSING_DIGEST = `sha256:${"0".repeat(64)}`;
const runner = new Runner(null);
const digest = await runner.buildImage(TASK_DIR, IMAGE_TAG);
after(() => docker(["rmi", IMAGE_TAG]));
function docker(args: string[]): { status: number | null; stdout: string; stderr: string } {
return spawnSync("docker", args, { encoding: "utf8" });
}
/** 删掉测试自己的容器;不存在不算错,别的错误照样报。 */
function forceRemove(name: string): void {
const removed = docker(["rm", "-f", name]);
if (removed.status !== 0 && !removed.stderr.includes("No such container")) throw new Error(removed.stderr);
}
/** 一个测试用的容器名:先清掉上次没删干净的同名容器,测试结束后再删。 */
function containerName(t: TestContext, suffix: string): string {
const name = `inforsi-test-${suffix}`;
forceRemove(name);
t.after(() => forceRemove(name));
return name;
}
function dirs(t: TestContext): { runDir: string; logDir: string } {
const root = mkdtempSync(join(tmpdir(), "inforsi-runner-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
const runDir = join(root, "run");
const logDir = join(root, "log");
mkdirSync(runDir);
mkdirSync(logDir);
return { runDir, logDir };
}
function definition(command: string[], wallSeconds: number, image: string): RunDefinition {
return {
command,
workdir: "/work",
image: { digest: image, runtime: "runc" },
wallSeconds,
cores: 1,
inheritEnv: false,
env: {},
};
}
const python = (code: string): string[] => ["python3", "-c", code];
/** 起一个容器并等它结束,交回结果和日志目录。 */
async function runToEnd(t: TestContext, suffix: string, command: string[], wallSeconds: number) {
const name = containerName(t, suffix);
const { runDir, logDir } = dirs(t);
const outcome = await runner.start(name, definition(command, wallSeconds, digest), runDir);
assert.deepEqual(outcome, { started: true });
const result = await runner.wait(name, logDir);
return { name, result, logDir };
}
function value(result: ProcessResult, key: "cpuSeconds" | "wallSeconds" | "peakMemoryBytes"): number {
assert.notEqual(result.ending, "notStarted");
if (result.ending === "notStarted") throw new Error("unreachable");
const item = result[key];
assert.equal(item.status, "measured", `${key} 应当测到`);
if (item.status !== "measured") throw new Error("unreachable");
return item.value;
}
test("建镜像命中参考项目的缓存;查镜像:有的答有,没有的答没有", async () => {
assert.match(digest, /^sha256:7ed52793365c/);
assert.equal(await runner.hasImage(digest), true);
assert.equal(await runner.hasImage(MISSING_DIGEST), false);
});
test("正常退出:带退出码和三项资源用量,标准输出、标准错误存成两个文件", async (t) => {
const code = "import sys; print('out'); print('err', file=sys.stderr); sys.exit(3)";
const { result, logDir } = await runToEnd(t, "exit", python(code), 30);
assert.equal(result.ending, "exited");
assert.equal(result.ending === "exited" && result.exitCode, 3);
assert.ok(value(result, "cpuSeconds") > 0);
assert.ok(value(result, "wallSeconds") > 0 && value(result, "wallSeconds") < 30);
assert.ok(value(result, "peakMemoryBytes") > 0);
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "out\n");
assert.equal(readFileSync(join(logDir, "stderr.log"), "utf8"), "err\n");
});
test("超时:结局 timeout,CPU 秒接近时限,墙钟不短于时限", async (t) => {
const { result } = await runToEnd(t, "timeout", python("while True: pass"), 2);
assert.equal(result.ending, "timeout");
const cpu = value(result, "cpuSeconds");
assert.ok(cpu > 1.7 && cpu < 2.3, `CPU 秒 ${cpu}`);
assert.ok(value(result, "wallSeconds") >= 2);
});
test("候选伪造计量结果再死循环:伪造不成,结局仍是 timeout,资源用量是真实测到的;写出的文件归当前用户", async (t) => {
// 审阅时实测过的手法:先写一份假的 usage,再把目录改成只读,然后死循环到超时。
const code = [
"import os",
"for attempt in (lambda: open('/inforsi-record/usage', 'w').write('exited 0 1 1 1\\n'), lambda: os.chmod('/inforsi-record', 0o555)):",
" try:",
" attempt()",
" except OSError as error:",
" print(type(error).__name__, flush=True)",
"open('out.txt', 'w').write('x')",
"while True: pass",
].join("\n");
const name = containerName(t, "forge");
const { runDir, logDir } = dirs(t);
assert.deepEqual(await runner.start(name, definition(python(code), 2, digest), runDir), { started: true });
const result = await runner.wait(name, logDir);
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "PermissionError\nPermissionError\n");
assert.equal(result.ending, "timeout");
const cpu = value(result, "cpuSeconds");
t.diagnostic(`结局 ${result.ending},CPU 秒 ${cpu},墙钟秒 ${value(result, "wallSeconds")}`);
assert.ok(cpu > 1.7 && cpu < 2.3, `CPU 秒 ${cpu}`);
assert.ok(value(result, "wallSeconds") >= 2);
assert.equal(statSync(join(runDir, "out.txt")).uid, process.getuid?.());
});
test("命令自己以 124 或 137 退出,不判成超时", async (t) => {
const own124 = await runToEnd(t, "exit124", python("import sys; sys.exit(124)"), 30);
assert.equal(own124.result.ending, "exited");
assert.equal(own124.result.ending === "exited" && own124.result.exitCode, 124);
const own137 = await runToEnd(t, "exit137", python("import os; os.kill(os.getpid(), 9)"), 30);
assert.equal(own137.result.ending, "exited");
assert.equal(own137.result.ending === "exited" && own137.result.exitCode, 137);
});
test("从外面 docker kill:结局 cancelled,三项资源都测不到并写明原因", async (t) => {
const name = containerName(t, "killed");
const { runDir, logDir } = dirs(t);
const outcome = await runner.start(name, definition(python("import time; time.sleep(60)"), 120, digest), runDir);
assert.deepEqual(outcome, { started: true });
assert.equal(await runner.status(name), "running");
const killed = docker(["kill", name]);
assert.equal(killed.status, 0, killed.stderr);
const result = await runner.wait(name, logDir);
assert.equal(result.ending, "cancelled");
if (result.ending !== "cancelled") return;
for (const item of [result.cpuSeconds, result.wallSeconds, result.peakMemoryBytes]) {
assert.equal(item.status, "unmeasured");
assert.match(item.status === "unmeasured" ? item.reason : "", /从外面被结束/);
}
});
test("镜像编号不存在:没启动,原因是 Docker 的报错原文,也没留下容器", async (t) => {
const name = containerName(t, "noimage");
const { runDir } = dirs(t);
const outcome = await runner.start(name, definition(python("pass"), 30, MISSING_DIGEST), runDir);
assert.equal(outcome.started, false);
assert.match(outcome.started ? "" : outcome.reason, /No such image/);
assert.equal(await runner.status(name), "absent");
});
test("断网:容器里连不上外网", async (t) => {
const code = [
"import socket, sys",
"try:",
" socket.create_connection(('1.1.1.1', 53), timeout=3)",
"except OSError as error:",
" print(type(error).__name__); sys.exit(0)",
"sys.exit(1)",
].join("\n");
const { result } = await runToEnd(t, "network", python(code), 30);
assert.equal(result.ending === "exited" && result.exitCode, 0);
});
test("核数:容器里 os.cpu_count() 报 1", async (t) => {
const { logDir } = await runToEnd(t, "cores", python("import os; print(os.cpu_count())"), 30);
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "1\n");
});
test("查状态:没有这个容器、建了没启动、在跑、已结束、问不到", async (t) => {
assert.equal(await runner.status(containerName(t, "status-absent")), "absent");
const created = containerName(t, "status-created");
const create = docker(["create", "--name", created, digest, "true"]);
assert.equal(create.status, 0, create.stderr);
assert.equal(await runner.status(created), "created");
const name = containerName(t, "status-running");
const { runDir, logDir } = dirs(t);
await runner.start(name, definition(python("import time; time.sleep(3)"), 30, digest), runDir);
assert.equal(await runner.status(name), "running");
await runner.wait(name, logDir);
assert.equal(await runner.status(name), "exited");
const nowhere = new Runner("unix:///nonexistent/docker.sock");
assert.equal(await nowhere.status(name), "unreachable");
await runner.remove(name);
assert.equal(await runner.status(name), "absent");
});
test("等结束能接上另一个 Runner 实例启动的容器;已结束的容器再等一次,结果相同", async (t) => {
const name = containerName(t, "resume");
const { runDir, logDir } = dirs(t);
const starter = new Runner(null);
await starter.start(name, definition(python("import time; time.sleep(2); print('done')"), 30, digest), runDir);
const resumed = new Runner(null);
const first = await resumed.wait(name, logDir);
assert.equal(first.ending === "exited" && first.exitCode, 0);
assert.equal(readFileSync(join(logDir, "stdout.log"), "utf8"), "done\n");
const again = await new Runner(null).wait(name, logDir);
assert.deepEqual(again, first);
});
test("容器名不符合 Docker 的要求时报错", async () => {
await assert.rejects(runner.status("inforsi test"), /不符合 Docker 的要求/);
await assert.rejects(runner.status("-x"), /不符合 Docker 的要求/);
});