fix: 用容器生命周期核实恢复并单列三份总体验收

审阅发现spawn后快照早于恢复实际执行,不能证明没有重启;改用杀前至目标结果落账的Docker历史事件,保留缓存完整性限制。逐份stats不再冒称任务书总体通过;独立验收要求三份不同研究、成功杀进程及不重叠区间。两项缺陷附回归,7项定向测试与check通过,独立复审无新问题;真实B尚待启动。
This commit is contained in:
2026-10-04 04:52:13 -04:00
parent b7be524cdc
commit de8e13b031
5 changed files with 473 additions and 25 deletions
+203 -7
View File
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { test } from "node:test";
@@ -10,6 +10,11 @@ import {
FORWARD_STOP,
scoreBand,
} from "../eval/lib/forward.ts";
import {
forwardAcceptance,
sameContainerThroughResult,
type StudySummary,
} from "../eval/lib/forward-evidence.ts";
import { lightCheckUsage } from "../eval/lib/light-checks.ts";
import { referenceScores } from "../eval/lib/reference-scores.ts";
import { Ledger } from "../src/ledger/index.ts";
@@ -106,10 +111,11 @@ test("forward 导出/统计:历史代码配对、两次核验一次修复、
JSON.stringify({
run: "forward-1",
candidate: "r1-c1",
container: "inforsi-test-forward-1",
beforeKillSeq: atKill.at(-1)!.seq,
}),
);
for (const part of ["before", "after-kill", "after-resume"])
for (const part of ["before", "after-kill", "before-resume"])
writeFileSync(
join(dir, `container-${part}.json`),
JSON.stringify({
@@ -122,6 +128,25 @@ test("forward 导出/统计:历史代码配对、两次核验一次修复、
process: exited,
judged: judged(2.5, "main"),
});
writeFileSync(
join(dir, "container-events-window.json"),
JSON.stringify({
since: "2026-10-04T00:00:01Z",
until: "2026-10-04T00:02:00Z",
run: "forward-1",
resultSeq: ledger.records().at(-1)!.seq,
}),
);
const event = (action: string) =>
JSON.stringify({
Type: "container",
Action: action,
Actor: {
ID: "same-container",
Attributes: { name: "inforsi-test-forward-1" },
},
});
writeFileSync(join(dir, "container-events.jsonl"), event("die") + "\n");
w.decision("adopt", 1, null, { adopted: "r1-c1" });
ledger.append("event", {
event: "halted",
@@ -180,6 +205,7 @@ test("forward 导出/统计:历史代码配对、两次核验一次修复、
{ encoding: "utf8" },
);
assert.equal(statsResult.status, 0, statsResult.stderr);
assert.match(statsResult.stdout, /逐研究检查通过.*不是任务书16总体验收/);
const scores = JSON.parse(
readFileSync(join(statsOut, "predictions-scores.json"), "utf8"),
);
@@ -194,12 +220,10 @@ test("forward 导出/统计:历史代码配对、两次核验一次修复、
chargedOnceNoRerun: true,
resultAndContinued: true,
});
// 三份早期快照完全一致,之后同一容器重启仍必须使验收失败。
writeFileSync(
join(dir, "container-after-resume.json"),
JSON.stringify({
Id: "replacement",
State: { Running: true, StartedAt: "2026-10-04T00:01:00Z" },
}),
join(dir, "container-events.jsonl"),
[event("die"), event("start"), event("restart"), event("die")].join("\n"),
);
const badOut = join(root, "bad-stats");
const bad = spawnSync(
@@ -213,6 +237,22 @@ test("forward 导出/统计:历史代码配对、两次核验一次修复、
.sameContainer,
false,
);
rmSync(join(dir, "kill.json"));
const singleOut = join(root, "single-without-kill");
const single = spawnSync(
process.execPath,
["eval/forward/stats.ts", singleOut, refs, dir],
{ encoding: "utf8" },
);
assert.equal(single.status, 0, single.stderr);
assert.match(
single.stdout,
/所给 1 份研究的逐研究检查通过.*不是任务书16总体验收/,
);
assert.equal(
JSON.parse(readFileSync(join(singleOut, "stats.json"), "utf8"))[0].kill,
null,
);
});
test("forward 账目:没启动退还、修复用尽不扣次;缺不跑事件或结果不能通过", (t) => {
@@ -352,3 +392,159 @@ test("forward 参考分数:incomplete 原值保留但有效分数为空", (t)
assert.equal(rows[0]!.adopted, false);
assert.equal(rows[0]!.observationSeq, 2);
});
test("forward 生命周期:同名替换、缺完成事件、历史缓存触顶都不能证明没有重启", () => {
const snapshots = [
{ Id: "old", State: { Running: true, StartedAt: "before" } },
];
const event = (id: string, action: string) => ({
Type: "container",
Action: action,
Actor: { ID: id, Attributes: { name: "target" } },
});
assert.equal(
sameContainerThroughResult(snapshots, [event("old", "die")], "target")
.noRestartOrReplacement,
true,
);
assert.equal(
sameContainerThroughResult(
[...snapshots, { Id: "replacement", State: snapshots[0]!.State }],
[event("old", "die")],
"target",
).snapshotsAgree,
false,
);
assert.equal(
sameContainerThroughResult(
snapshots,
[event("old", "die"), event("new", "create")],
"target",
).noRestartOrReplacement,
false,
);
assert.equal(
sameContainerThroughResult(snapshots, [], "target").completionObserved,
false,
);
assert.equal(
sameContainerThroughResult(
snapshots,
Array.from({ length: 256 }, () => event("old", "die")),
"target",
).historyWithinLimit,
false,
);
});
test("forward 总体验收:必须三份不同完成研究、成功kill和不重叠的完整启动区间", (t) => {
const root = tempDir(t);
const launchFile = (dir: string, start: string, end: string) =>
writeFileSync(
join(dir, "launches.jsonl"),
[
{ event: "start", pid: 1, at: start },
{ event: "end", pid: 1, at: end },
]
.map((e) => JSON.stringify(e))
.join("\n"),
);
const summaries: StudySummary[] = [0, 1, 2].map((i) => {
const dir = join(root, `study-${i}`);
mkdirSync(dir);
launchFile(dir, `2026-10-04T0${i}:00:00Z`, `2026-10-04T0${i}:30:00Z`);
return {
dir,
study: `study-${i}`,
pass: true,
accounts: { checks: { adoptedAndStopped: true } },
kill:
i === 0
? {
checks: {
sameContainer: true,
chargedOnceNoRerun: true,
resultAndContinued: true,
},
}
: null,
};
});
// 杀后恢复的进程对也必须完整;间隔仍属于该研究,不能让另一研究插入。
writeFileSync(
join(summaries[0]!.dir, "launches.jsonl"),
[
{ event: "start", pid: 1, at: "2026-10-04T00:00:00Z" },
{ event: "end", pid: 1, at: "2026-10-04T00:10:00Z" },
{ event: "start", pid: 2, at: "2026-10-04T00:15:00Z" },
{ event: "end", pid: 2, at: "2026-10-04T00:30:00Z" },
]
.map((e) => JSON.stringify(e))
.join("\n"),
);
assert.equal(forwardAcceptance(summaries).pass, true);
assert.equal(forwardAcceptance(summaries.slice(0, 1)).pass, false);
assert.equal(forwardAcceptance(summaries.slice(0, 2)).pass, false);
assert.equal(
forwardAcceptance(summaries.map((s) => ({ ...s, kill: null }))).pass,
false,
);
assert.equal(
forwardAcceptance([summaries[0]!, summaries[0]!, summaries[2]!]).pass,
false,
);
assert.equal(
forwardAcceptance(
summaries.map((s) => ({
...s,
accounts: { checks: { adoptedAndStopped: false } },
})),
).pass,
false,
);
assert.equal(
forwardAcceptance(
summaries.map((s) =>
s.kill === null
? s
: { ...s, kill: { checks: { sameContainer: false } } },
),
).pass,
false,
);
const statsDir = join(root, "stats");
mkdirSync(statsDir);
writeFileSync(join(statsDir, "stats.json"), JSON.stringify(summaries));
const passed = spawnSync(
process.execPath,
["eval/forward/acceptance.ts", statsDir],
{ encoding: "utf8" },
);
assert.equal(passed.status, 0, passed.stderr);
assert.equal(
JSON.parse(readFileSync(join(statsDir, "acceptance.json"), "utf8")).pass,
true,
);
writeFileSync(
join(statsDir, "stats.json"),
JSON.stringify(summaries.slice(0, 1)),
);
const failed = spawnSync(
process.execPath,
["eval/forward/acceptance.ts", statsDir],
{ encoding: "utf8" },
);
assert.equal(failed.status, 1, failed.stderr);
assert.match(failed.stdout, /总体程序验收未通过/);
// 包含杀后恢复的整个研究区间不能与下一研究重叠。
launchFile(summaries[1]!.dir, "2026-10-04T00:20:00Z", "2026-10-04T01:30:00Z");
assert.equal(forwardAcceptance(summaries).checks.studiesDoNotOverlap, false);
writeFileSync(
join(summaries[1]!.dir, "launches.jsonl"),
JSON.stringify({ event: "start", pid: 1, at: "2026-10-04T01:00:00Z" }),
);
assert.equal(
forwardAcceptance(summaries).checks.launchIntervalsComplete,
false,
);
});