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>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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"]);
|
||||
});
|
||||
Reference in New Issue
Block a user