Files
InfoRSI/tests/flow-docker.test.ts
T
iomgaa bf5b3f2358 feat: 接通实现核验修复到正向运行采纳
任务书16第二块:角色分派与每调用轻量检查,提交及代码原子记账,按次数核验修复且耗尽不跑,串行正式运行及恢复引用指标,采纳含null统一公开。451项免费测试通过,全新上下文独立审阅未发现问题;真实研究验收仍待进行。
2026-10-04 04:25:00 -04:00

150 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test, type TestContext } from "node:test";
import { lightChecker } from "../src/flow/task-code.ts";
import { recover } from "../src/flow/recover.ts";
import { containerName, freshDir, runPaths } from "../src/flow/paths.ts";
import type { Study } from "../src/flow/study.ts";
import { Board, protocolMetrics, type FrozenProtocol } from "../src/materials/index.ts";
import { loadTask, formalRun as runDefinition, prepareRun } from "../src/tasks/index.ts";
import { runBalance } from "../src/rules/index.ts";
import { runStudy } from "../src/flow/index.ts";
import { Ledger } from "../src/ledger/index.ts";
import { Runner } from "../src/runner/index.ts";
import { fullConfig } from "./config-helpers.ts";
// flow 碰到 Docker 的两处边界,用真实 Docker 测,不花模型的钱:走不到派模型任务就停下。
// 镜像一律 inforsi-test- 开头的固定名字;本机还有别的项目在建删容器和镜像,只动自己前缀的。
const TASK_DIR = join(import.meta.dirname, "..", "tasks", "circle_packing");
function docker(args: string[]): { status: number | null; stdout: string; stderr: string } {
return spawnSync("docker", args, { encoding: "utf8" });
}
/** 删掉测试自己的镜像标签;不存在不算错,别的错误照样报。 */
function removeImage(tag: string): void {
const removed = docker(["rmi", tag]);
if (removed.status !== 0 && !removed.stderr.includes("No such image")) throw new Error(removed.stderr);
}
/** 一个测试用的研究编号:先清掉上次没删干净的同名镜像,测试结束后再删。 */
function studyId(t: TestContext, suffix: string): string {
const id = `test-flow-${suffix}`;
removeImage(`inforsi-${id}`);
t.after(() => removeImage(`inforsi-${id}`));
return id;
}
function tempDir(t: TestContext): string {
const dir = mkdtempSync(join(tmpdir(), "inforsi-flow-"));
t.after(() => rmSync(dir, { recursive: true, force: true }));
return dir;
}
function ledgerKinds(studyDir: string): string[] {
const ledger = Ledger.open(join(studyDir, "ledger.sqlite"));
try {
return ledger.records().map((r) => r.kind);
} finally {
ledger.close();
}
}
test("研究编号本机用过(已有 inforsi-<研究编号> 镜像):冻结配置前报错停下,不建镜像、不写研究配置记录", async (t) => {
const id = studyId(t, "dup");
// 一份旧研究冻结配置时建下的镜像。
await new Runner(null).buildImage(TASK_DIR, `inforsi-${id}`);
const dir = tempDir(t);
await assert.rejects(
runStudy({ studyDir: dir, config: { ...fullConfig(), studyId: id }, stop: new AbortController().signal }),
new RegExp(`研究编号 ${id} 已被本机用过(已有镜像 inforsi-${id})`),
);
assert.deepEqual(ledgerKinds(dir), []);
});
test("docker run 没启动:写结果\"没启动\"后停下,原因带 Docker 报错原文,不在本进程里起新编号重跑", { timeout: 120_000 }, async (t) => {
// 任务事实里写一个本机没有的 Docker 运行时,docker run 每次都失败,也不建容器。
const task = join(tempDir(t), "task");
cpSync(TASK_DIR, task, { recursive: true });
const facts = JSON.parse(readFileSync(join(task, "task.json"), "utf8")) as { run: { runtime: string } };
facts.run.runtime = "inforsi-test-no-such-runtime";
writeFileSync(join(task, "task.json"), JSON.stringify(facts));
const dir = tempDir(t);
const ending = await runStudy({
studyDir: dir,
config: { ...fullConfig(), studyId: studyId(t, "not-started"), task },
stop: new AbortController().signal,
});
assert.equal(ending.status, "halted");
assert.match((ending as { reason: string }).reason, /运行 base-1 没有启动.*unknown or invalid runtime name: inforsi-test-no-such-runtime/);
assert.deepEqual(ledgerKinds(dir), ["config", "code", "runPlanned", "runResult", "event"]);
});
/** 不派模型,只接真实任务镜像、账本和运行器,检查 flow 的两处 Docker 接线。 */
async function runtimeStudy(t: TestContext, suffix: string): Promise<Study> {
const dir = tempDir(t);
const id = studyId(t, suffix);
const task = loadTask(TASK_DIR);
const runner = new Runner(null);
const image = await runner.buildImage(task.dir, `inforsi-${id}`);
const ledger = Ledger.open(join(dir, "ledger.sqlite"));
t.after(() => ledger.close());
return { dir, task, runner, ledger, stop: new AbortController().signal,
frozen: { config: { ...fullConfig(), studyId: id, lightChecks: { maxCount: 1, seconds: 7 } }, facts: task.facts, image, seed: 1 } } as Study;
}
test("flow 轻量检查:每次调用各自限次和限时,真实 Docker 只检查当前目录,不登记正式运行", { timeout: 120_000 }, async (t) => {
const s = await runtimeStudy(t, "light");
const work = join(s.dir, "work");
mkdirSync(work);
writeFileSync(join(work, "initial.py"), "raise RuntimeError('不应执行候选代码')\ndef run_packing():\n pass\n");
for (const call of ["first-call", "second-call"]) {
freshDir(join(s.dir, "calls", call));
const check = lightChecker(s, call, work);
const result = JSON.parse(await check());
assert.deepEqual(result, { used: 1, maxCount: 1, seconds: 7, result: { passed: true } });
assert.match(await check(), /已用完(上限 1 次)/);
assert.equal(await s.runner.status(containerName(s.frozen.config.studyId, `${call}-check-1`)), "absent");
}
assert.deepEqual(s.ledger.records(), []);
});
test("正向恢复:接上原容器不重跑,补启动只扣一次,引用指标沿用原登记缺失作废规则", { timeout: 120_000 }, async (t) => {
const s = await runtimeStudy(t, "forward-recover");
const metric = { id: "old-c.required", meaning: "必测量", unit: "次", direction: "maximize" as const, howMeasured: "原登记定义", missing: "invalidateRun" as const };
const original: FrozenProtocol = { round: 0, baseline: "base", proposals: [], factors: [], safeguards: [], metrics: { defined: [metric], reused: [] }, clauses: [], body: "登记" };
const current: FrozenProtocol = { ...original, round: 1, metrics: { defined: [], reused: [metric.id] } };
s.ledger.append("protocol", { candidate: "old-c", protocol: original });
s.ledger.append("protocol", { candidate: "r1-c1", protocol: current });
const board = new Board(s.ledger.records());
const run = "forward-1";
const paths = runPaths(s.dir, run);
const definition = runDefinition(s.task, s.frozen.image, "1");
s.ledger.append("runPlanned", { run, purpose: "forward", round: 1, object: "r1-c1", definition });
freshDir(paths.base);
prepareRun(s.task, s.task.baselineDir, freshDir(paths.run), protocolMetrics(board, current));
const name = containerName(s.frozen.config.studyId, run);
t.after(() => { const result = docker(["rm", "-f", name]); if (result.status !== 0 && !result.stderr.includes("No such container")) throw new Error(result.stderr); });
assert.deepEqual(await s.runner.start(name, definition, paths.run), { started: true });
// 中断点:容器已启动,账本还没写 runStarted。恢复不调用 formalRun,而是找到原容器等它。
assert.equal(await recover(s), null);
const records = s.ledger.records();
assert.equal(records.filter((r) => r.kind === "runPlanned").length, 1);
assert.equal(records.filter((r) => r.kind === "runStarted").length, 1);
assert.equal(records.filter((r) => r.kind === "runResult").length, 1);
const result = records.find((r) => r.kind === "runResult")!;
if (result.kind !== "runResult") throw new Error("缺运行结果");
assert.equal(result.content.judged!.verdict, "invalid");
assert.match(result.content.judged!.verdictReason, /old-c.required/);
assert.equal(result.content.judged!.metrics[metric.id]!.status, "unmeasured");
assert.deepEqual(runBalance(13, new Board(records).runStates()), { charged: 1, reserved: 0, balance: 12 });
assert.equal(await s.runner.status(name), "absent");
assert.equal(await recover(s), null);
assert.equal(s.ledger.records().length, records.length);
});