Files
InfoRSI/tests/flow-docker.test.ts
T
iomgaa 75d7cffd27 feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验
按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。

审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。

eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:58:52 -04:00

79 lines
3.6 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, 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 { 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"]);
});