feat: 账本 ledger 模块(任务书 03)
一张只追加的表(序号、种类、内容、哈希),禁改禁删两条触发器。 有自己编号、只发生一次的 8 种记录加部分唯一索引,重复就报错:阶段开、阶段关、运行登记、 确认启动(确认没启动的结果也算这一条,防止同一次运行既扣次又退还)、运行结果、模型调用开始、结束、消融结算。 写入时只查四种拒收:只收开着的阶段的提交;替换必须接本人同一阶段、同一对象、同一种提交的最新一版,实现与核验类不能替换; 结果先于确认启动(没启动除外);本轮消融已有格启动后不能关解释阶段。 写提案时作者自评拆成封存。提供打开(核对表结构版本,非账本库不改动)、追加、同步事务、读取、未完成的动作。 第一版的可见性、截止序号、通用唯一键、jev 记录和多余的前提检查已按新版任务书清掉。 三轮全新上下文审阅;flow 传错参数一类的意见按 CLAUDE.md"罕见情况怎么办"第 2 条只记录不加代码。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { TestContext } from "node:test";
|
||||
import { Ledger } from "../src/ledger/index.ts";
|
||||
import type { ModelUsage, ProcessResult, Range, RunDefinition, Stage, Submission } from "../src/vocab/index.ts";
|
||||
|
||||
// ledger 测试共用:每个测试在自己的临时目录里建一个真实的数据库文件,测试结束时连目录一起删掉。
|
||||
|
||||
export function tempDir(t: TestContext): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "inforsi-ledger-"));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function freshLedger(t: TestContext): { ledger: Ledger; path: string } {
|
||||
const path = join(tempDir(t), "ledger.db");
|
||||
const ledger = Ledger.open(path);
|
||||
t.after(() => ledger.close());
|
||||
return { ledger, path };
|
||||
}
|
||||
|
||||
export const range = (q1: number, q5: number, q9: number): Range => ({ q1, q5, q9 });
|
||||
|
||||
export function proposal(estimate: Range, body = "提案正文"): Submission<"proposal"> {
|
||||
return {
|
||||
factors: [{ name: "换优化器", method: "把 SGD 换成 Adam", keySettings: "学习率 0.01", continues: null }],
|
||||
safeguards: [],
|
||||
metrics: { defined: [], reused: [] },
|
||||
authorEstimate: estimate,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
export const prediction = (r: Range): Submission<"firstPrediction"> => ({ range: r, body: "理由" });
|
||||
export const critique = (body = "挑刺内容"): Submission<"critique"> => ({ target: "whole", body });
|
||||
export const wrapUp = (body = "收尾"): Submission<"wrapUp"> => ({ body });
|
||||
|
||||
export const implementation = (body = "无"): Submission<"implementation"> => ({
|
||||
factorLocations: [{ factor: "p1", locations: ["solve.py:10-20"] }],
|
||||
safeguardLocations: [],
|
||||
unassigned: [],
|
||||
concerns: "无",
|
||||
body,
|
||||
});
|
||||
|
||||
export const definition: RunDefinition = {
|
||||
command: ["python", "solve.py"],
|
||||
workdir: "/work",
|
||||
image: { digest: `sha256:${"0".repeat(64)}`, runtime: "runc" },
|
||||
wallSeconds: 120,
|
||||
cores: 1,
|
||||
inheritEnv: false,
|
||||
env: {},
|
||||
};
|
||||
|
||||
export const exited: ProcessResult = {
|
||||
ending: "exited",
|
||||
exitCode: 0,
|
||||
cpuSeconds: { status: "measured", value: 100 },
|
||||
wallSeconds: { status: "measured", value: 101 },
|
||||
peakMemoryBytes: { status: "measured", value: 1e8 },
|
||||
};
|
||||
|
||||
export const notStarted: ProcessResult = { ending: "notStarted", reason: "启动前取消" };
|
||||
|
||||
/** 开一个阶段,返回开阶段那条记录的序号。 */
|
||||
export function openStage(
|
||||
ledger: Ledger,
|
||||
stageId: string,
|
||||
stage: Stage,
|
||||
round: number,
|
||||
roster: string[] = ["r1", "r2"],
|
||||
): number {
|
||||
return ledger.append("stageOpen", { stageId, stage, round, roster }).seq;
|
||||
}
|
||||
|
||||
export function closeStage(ledger: Ledger, stageId: string): number {
|
||||
return ledger.append("stageClose", { stageId }).seq;
|
||||
}
|
||||
|
||||
/** 登记一次运行。 */
|
||||
export function planRun(ledger: Ledger, run: string, purpose: "baseline" | "forward" | "ablation", round: number): number {
|
||||
return ledger.append("runPlanned", { run, purpose, round, object: purpose === "baseline" ? null : `obj-${run}`, definition }).seq;
|
||||
}
|
||||
|
||||
/** 一次模型调用的开始记录,materials 是这次任务的全部材料。 */
|
||||
export const callStart = (call: string, actor = "r1", stageId: string | null = null) => ({
|
||||
call,
|
||||
actor,
|
||||
stageId,
|
||||
model: "glm-5.3",
|
||||
materials: { firstMessage: "任务说明……", tools: ["read", "submit"] },
|
||||
});
|
||||
|
||||
const unknownCount = { status: "unmeasured" as const, reason: "服务没报" };
|
||||
export const usage: ModelUsage = {
|
||||
inputTokens: unknownCount,
|
||||
cacheReadTokens: unknownCount,
|
||||
cacheWriteTokens: unknownCount,
|
||||
outputTokens: unknownCount,
|
||||
cost: unknownCount,
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { Ledger } from "../src/ledger/index.ts";
|
||||
import { callStart, exited, freshLedger, notStarted, planRun, tempDir, usage } from "./ledger-helpers.ts";
|
||||
|
||||
// 未完成的动作(方法.md 第九部分"中断与恢复时的记账含义":先登记、再执行、再登记结果):
|
||||
// 登记了但没有结果的运行、开始了但没有结束的模型调用能被找出来;写了结果之后就不再出现。
|
||||
|
||||
test("运行:登记了没结果的被找出来,带着有没有确认启动;写了结果就不再出现", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
planRun(ledger, "base", "baseline", 0);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
planRun(ledger, "run2", "forward", 1);
|
||||
ledger.append("runStarted", { run: "base" });
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
ledger.append("runResult", { run: "base", process: exited, judged: null });
|
||||
assert.deepEqual(
|
||||
ledger.pending().runs.map((r) => [r.plan.content.run, r.started]),
|
||||
[
|
||||
["run1", true],
|
||||
["run2", false],
|
||||
],
|
||||
);
|
||||
ledger.append("runResult", { run: "run2", process: notStarted, judged: null });
|
||||
assert.deepEqual(ledger.pending().runs.map((r) => r.plan.content.run), ["run1"]);
|
||||
ledger.append("runResult", { run: "run1", process: exited, judged: null });
|
||||
assert.deepEqual(ledger.pending().runs, []);
|
||||
});
|
||||
|
||||
test("模型调用:开始了没结束的被找出来,带着这次任务的全部材料;有用量也还算没结束;结束后不再出现", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
ledger.append("callStarted", callStart("k1", "r1", "s1"));
|
||||
ledger.append("callStarted", callStart("k2", "e1"));
|
||||
ledger.append("callUsage", { call: "k1", reply: 1, usage });
|
||||
ledger.append("callEnded", { call: "k2", ending: "failed", reason: "接口超时" });
|
||||
const calls = ledger.pending().calls;
|
||||
assert.deepEqual(calls.map((c) => c.content.call), ["k1"]);
|
||||
assert.deepEqual(calls[0]!.content.materials, callStart("k1").materials);
|
||||
ledger.append("callEnded", { call: "k1", ending: "completed" });
|
||||
assert.deepEqual(ledger.pending(), { runs: [], calls: [] });
|
||||
});
|
||||
|
||||
test("关掉再打开:未完成的动作照样找得出来", (t) => {
|
||||
const path = join(tempDir(t), "ledger.db");
|
||||
const ledger = Ledger.open(path);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
ledger.append("callStarted", callStart("k1"));
|
||||
const before = ledger.pending();
|
||||
ledger.close();
|
||||
const reopened = Ledger.open(path);
|
||||
t.after(() => reopened.close());
|
||||
assert.deepEqual(reopened.pending(), before);
|
||||
assert.equal(before.runs.length, 1);
|
||||
assert.equal(before.calls.length, 1);
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import type { Ledger } from "../src/ledger/index.ts";
|
||||
import {
|
||||
closeStage,
|
||||
critique,
|
||||
exited,
|
||||
freshLedger,
|
||||
implementation,
|
||||
notStarted,
|
||||
openStage,
|
||||
planRun,
|
||||
prediction,
|
||||
proposal,
|
||||
range,
|
||||
wrapUp,
|
||||
} from "./ledger-helpers.ts";
|
||||
|
||||
// 写入时由 ledger 拒绝的四种情况(docs/结构.md 第四部分"写入时由 ledger 拒绝的几种情况"),每种一条拒收、一条对应的正常情况。
|
||||
// 预期值来自文字:
|
||||
// 1. 只收开着的阶段(有开阶段记录、没有关阶段记录)的提交,别的拒收;
|
||||
// 2. 要替换的那一份不是本人在这个阶段、对同一个对象的最新一版 → 拒收;实现、修复、核验、消融实现、消融核验不能替换(方法.md 第五部分);
|
||||
// 3. 运行结果写在"确认启动"之前 → 拒收;"没启动"的结果不需要启动记录(方法.md 第七部分);
|
||||
// 4. 这一轮消融已经有格确认启动了,才来关这一轮的解释阶段 → 拒收(方法.md 4.3 ⑭)。
|
||||
|
||||
function submitWrapUp(ledger: Ledger, stageId: string, author: string, version: number, body = "收尾") {
|
||||
return ledger.append("submission", { stageId, author, target: null, item: null, version, kind: "wrapUp", content: wrapUp(body) });
|
||||
}
|
||||
|
||||
test("拒收 1:只收开着的阶段的提交:没开过的不收,关了以后不收、替换也不收;开着时收", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
assert.throws(() => submitWrapUp(ledger, "s1", "r1", 1), /没有开着/);
|
||||
openStage(ledger, "s1", "wrapUp", 1);
|
||||
submitWrapUp(ledger, "s1", "r1", 1);
|
||||
closeStage(ledger, "s1");
|
||||
assert.throws(() => submitWrapUp(ledger, "s1", "r2", 1), /没有开着/);
|
||||
assert.throws(() => submitWrapUp(ledger, "s1", "r1", 2, "关闭后想替换"), /没有开着/);
|
||||
assert.equal(ledger.records().filter((r) => r.kind === "submission").length, 1);
|
||||
});
|
||||
|
||||
test("拒收 2 的正常情况:替换本人在这个阶段、对同一个对象的最新一版,版本号加 1", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "s1", "firstPrediction", 1);
|
||||
const slot = { stageId: "s1", author: "r1", target: "c1", item: null, kind: "firstPrediction" as const };
|
||||
ledger.append("submission", { ...slot, version: 1, content: prediction(range(1, 2, 3)) });
|
||||
ledger.append("submission", { ...slot, version: 2, content: prediction(range(1, 2, 4)) });
|
||||
ledger.append("submission", { ...slot, version: 3, content: prediction(range(1, 2, 5)) });
|
||||
assert.equal(ledger.latestSubmission(slot)?.content.version, 3);
|
||||
});
|
||||
|
||||
test("拒收 2:要替换的那一份不是最新一版、不是本人的、不是同一个对象的、不在这个阶段,都拒收", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "s1", "firstPrediction", 1);
|
||||
openStage(ledger, "s2", "firstPrediction", 1);
|
||||
const slot = { stageId: "s1", author: "r1", target: "c1", item: null, kind: "firstPrediction" as const };
|
||||
ledger.append("submission", { ...slot, version: 1, content: prediction(range(1, 2, 3)) });
|
||||
ledger.append("submission", { ...slot, version: 2, content: prediction(range(1, 2, 4)) });
|
||||
const next = { ...slot, version: 3, content: prediction(range(1, 2, 6)) };
|
||||
// 想替换第 1 版,但最新是第 2 版。
|
||||
assert.throws(() => ledger.append("submission", { ...slot, version: 2, content: prediction(range(0, 2, 4)) }), /最新是第 2 版/);
|
||||
// 要替换的第 3 版不存在。
|
||||
assert.throws(() => ledger.append("submission", { ...slot, version: 4, content: prediction(range(1, 2, 6)) }), /最新是第 2 版/);
|
||||
// 不是本人的、不是同一个对象的、不在这个阶段的:本人在那里还没有交过。
|
||||
assert.throws(() => ledger.append("submission", { ...next, author: "r2" }), /还没有交过/);
|
||||
assert.throws(() => ledger.append("submission", { ...next, target: "c2" }), /还没有交过/);
|
||||
assert.throws(() => ledger.append("submission", { ...next, stageId: "s2" }), /还没有交过/);
|
||||
assert.equal(ledger.latestSubmission(slot)?.content.version, 2);
|
||||
});
|
||||
|
||||
test("拒收 2:同一个人对同一个候选的几份挑刺按各自的编号分开算版本", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "s1", "critique", 1);
|
||||
const slot = { stageId: "s1", author: "r1", target: "c1", kind: "critique" as const };
|
||||
ledger.append("submission", { ...slot, item: "k1", version: 1, content: critique("第一份") });
|
||||
ledger.append("submission", { ...slot, item: "k2", version: 1, content: critique("第二份") });
|
||||
ledger.append("submission", { ...slot, item: "k1", version: 2, content: critique("第一份改") });
|
||||
assert.throws(() => ledger.append("submission", { ...slot, item: "k3", version: 2, content: critique("x") }), /还没有交过/);
|
||||
});
|
||||
|
||||
test("拒收 2:实现、修复、核验、消融实现、消融核验只收第一版,不能替换", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "impl", "implementation", 1, ["e1"]);
|
||||
openStage(ledger, "fix", "implementation", 1, ["e1"]);
|
||||
openStage(ledger, "ver", "verification", 1, ["v1"]);
|
||||
openStage(ledger, "aimpl", "ablationImplementation", 1, ["e1"]);
|
||||
openStage(ledger, "aver", "ablationVerification", 1, ["v1"]);
|
||||
const base = { author: "e1", target: "c1", item: null };
|
||||
const verification = { clauses: [], factors: [], safeguards: [], unassigned: [], body: "无" };
|
||||
const ablImpl = { alsoOff: [], cannotRun: null, concerns: "无" };
|
||||
const cases = [
|
||||
{ ...base, stageId: "impl", kind: "implementation" as const, content: implementation() },
|
||||
{ ...base, stageId: "fix", kind: "repair" as const, content: implementation("修复") },
|
||||
{ ...base, author: "v1", stageId: "ver", kind: "verification" as const, content: verification },
|
||||
{ ...base, stageId: "aimpl", target: "cell1", kind: "ablationImplementation" as const, content: ablImpl },
|
||||
{ ...base, author: "v1", stageId: "aver", target: "cell1", kind: "ablationVerification" as const, content: { factors: [] } },
|
||||
];
|
||||
for (const c of cases) {
|
||||
ledger.append("submission", { ...c, version: 1 });
|
||||
assert.throws(() => ledger.append("submission", { ...c, version: 2 }), /不能替换/, c.kind);
|
||||
}
|
||||
});
|
||||
|
||||
test("拒收 2:版本号只在同一种提交里接:不能替换的实现不能被别的种类顶替,提案和挑刺用同一个编号也接不上", (t) => {
|
||||
// 同一位置上,实现 v1 之后交收尾 v2 拒收,最新一版仍是实现;提案和挑刺用同一个编号,版本号各算各的。
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "impl", "implementation", 1, ["e1"]);
|
||||
const implSlot = { stageId: "impl", author: "e1", target: "c1", item: null };
|
||||
ledger.append("submission", { ...implSlot, version: 1, kind: "implementation", content: implementation() });
|
||||
assert.throws(() => ledger.append("submission", { ...implSlot, version: 2, kind: "wrapUp", content: wrapUp() }), /还没有交过/);
|
||||
assert.equal(ledger.latestSubmission({ ...implSlot, kind: "implementation" })?.content.kind, "implementation");
|
||||
|
||||
openStage(ledger, "mix", "proposal", 1);
|
||||
const mixSlot = { stageId: "mix", author: "r1", target: null, item: "x1" };
|
||||
ledger.append("submission", { ...mixSlot, version: 1, kind: "proposal", content: proposal(range(1, 2, 3)) });
|
||||
assert.throws(() => ledger.append("submission", { ...mixSlot, version: 2, kind: "critique", content: critique() }), /还没有交过/);
|
||||
assert.equal(ledger.latestSubmission({ ...mixSlot, kind: "critique" }), undefined);
|
||||
});
|
||||
|
||||
test("拒收 3:没有确认启动就写运行结果,拒收;确认启动后可以写", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
assert.throws(() => ledger.append("runResult", { run: "run1", process: exited, judged: null }), /还没有"确认启动"/);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
ledger.append("runResult", { run: "run1", process: exited, judged: null });
|
||||
});
|
||||
|
||||
test("拒收 3 的例外:确认没启动的结果不需要启动记录", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runResult", { run: "run1", process: notStarted, judged: null });
|
||||
assert.deepEqual(ledger.pending().runs, []);
|
||||
});
|
||||
|
||||
test("拒收 4:这一轮已经有消融格确认启动,就不能关这一轮的解释阶段", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "expl", "explanation", 2);
|
||||
planRun(ledger, "cellrun", "ablation", 2);
|
||||
ledger.append("runStarted", { run: "cellrun" });
|
||||
assert.throws(() => closeStage(ledger, "expl"), /解释冻结必须早于任何一格启动/);
|
||||
assert.equal(ledger.stageClosed("expl"), false);
|
||||
});
|
||||
|
||||
test("拒收 4 的正常情况:只登记没启动、别的轮的消融启动、本轮正向启动,都不挡关解释阶段;关别的阶段不受限", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "expl", "explanation", 2);
|
||||
planRun(ledger, "cell-planned", "ablation", 2); // 只登记,还没确认启动
|
||||
planRun(ledger, "cell-round1", "ablation", 1);
|
||||
ledger.append("runStarted", { run: "cell-round1" });
|
||||
planRun(ledger, "fwd-round2", "forward", 2);
|
||||
ledger.append("runStarted", { run: "fwd-round2" });
|
||||
closeStage(ledger, "expl");
|
||||
assert.equal(ledger.stageClosed("expl"), true);
|
||||
openStage(ledger, "wrap", "wrapUp", 2);
|
||||
planRun(ledger, "cell2", "ablation", 2);
|
||||
ledger.append("runStarted", { run: "cell2" });
|
||||
closeStage(ledger, "wrap");
|
||||
assert.equal(ledger.stageClosed("wrap"), true);
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { test } from "node:test";
|
||||
import { Ledger } from "../src/ledger/index.ts";
|
||||
import { hash } from "../src/vocab/index.ts";
|
||||
import {
|
||||
callStart,
|
||||
closeStage,
|
||||
exited,
|
||||
freshLedger,
|
||||
implementation,
|
||||
notStarted,
|
||||
openStage,
|
||||
planRun,
|
||||
proposal,
|
||||
range,
|
||||
tempDir,
|
||||
} from "./ledger-helpers.ts";
|
||||
|
||||
// 预期值照 docs/结构.md 第四部分"账本记录"、方法.md 第七部分"什么时候扣次数"和第九部分的文字写:
|
||||
// 整张表禁改禁删;按定义只发生一次的记录重复就报错,其余照写;事务要么全写要么全不写;
|
||||
// 关掉再打开读出不变;表结构版本不对打开报错。
|
||||
|
||||
function countRows(path: string): number {
|
||||
const db = new DatabaseSync(path);
|
||||
const n = Number((db.prepare("SELECT count(*) AS n FROM records").get() as { n: number }).n);
|
||||
db.close();
|
||||
return n;
|
||||
}
|
||||
|
||||
test("触发器:直接用 SQL 执行 UPDATE、DELETE 都被拒绝,记录不变", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
ledger.append("config", { n: 2 });
|
||||
const before = ledger.records();
|
||||
const db = new DatabaseSync(path);
|
||||
t.after(() => db.close());
|
||||
assert.throws(() => db.exec("UPDATE records SET content = '{\"n\":3}'"), /禁止修改/);
|
||||
assert.throws(() => db.exec("UPDATE records SET hash = 'x' WHERE seq = 1"), /禁止修改/);
|
||||
assert.throws(() => db.exec("DELETE FROM records"), /禁止删除/);
|
||||
assert.throws(() => db.exec("DELETE FROM records WHERE seq = 1"), /禁止删除/);
|
||||
assert.deepEqual(ledger.records(), before);
|
||||
});
|
||||
|
||||
// 唯一约束(schema.ts):有自己编号、按定义只发生一次的记录,同一个编号写第二条就报错,内容完全相同也报错,
|
||||
// 不多写一条;别的编号照常写。
|
||||
|
||||
test("唯一约束:同一个阶段开两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
openStage(ledger, "s1", "proposal", 1);
|
||||
assert.throws(() => openStage(ledger, "s1", "proposal", 1), /stage_open_once/);
|
||||
openStage(ledger, "s2", "proposal", 1);
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:同一个阶段关两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
openStage(ledger, "s1", "proposal", 1);
|
||||
closeStage(ledger, "s1");
|
||||
assert.throws(() => closeStage(ledger, "s1"), /stage_close_once/);
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:同一次运行登记两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
assert.throws(() => planRun(ledger, "run1", "forward", 1), /run_planned_once/);
|
||||
planRun(ledger, "run2", "forward", 1);
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:同一次运行确认启动两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
assert.throws(() => ledger.append("runStarted", { run: "run1" }), /run_started_or_not_started_once/);
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:确认启动和\"结果是没启动\"算同一条,一次运行只能有一样,两种先后都报错", (t) => {
|
||||
// 登记→确认启动→结果没启动、登记→结果没启动→确认启动,第三步都报错:一次运行不能既扣次又退还(方法.md 第七部分)。
|
||||
const { ledger, path } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
assert.throws(() => ledger.append("runResult", { run: "run1", process: notStarted, judged: null }), /run_started_or_not_started_once/);
|
||||
planRun(ledger, "run2", "forward", 1);
|
||||
ledger.append("runResult", { run: "run2", process: notStarted, judged: null });
|
||||
assert.throws(() => ledger.append("runStarted", { run: "run2" }), /run_started_or_not_started_once/);
|
||||
// 正常情况:确认启动后写启动过的结果。
|
||||
ledger.append("runResult", { run: "run1", process: exited, judged: null });
|
||||
assert.equal(countRows(path), 5); // 两条登记、一条确认启动、两条结果
|
||||
});
|
||||
|
||||
test("唯一约束:同一次运行写两个结果报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
ledger.append("runResult", { run: "run1", process: exited, judged: null });
|
||||
assert.throws(() => ledger.append("runResult", { run: "run1", process: exited, judged: null }), /run_result_once/);
|
||||
assert.equal(countRows(path), 3);
|
||||
});
|
||||
|
||||
test("唯一约束:同一次模型调用开始两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
ledger.append("callStarted", callStart("k1"));
|
||||
assert.throws(() => ledger.append("callStarted", callStart("k1")), /call_started_once/);
|
||||
ledger.append("callStarted", callStart("k2"));
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:同一次模型调用结束两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
ledger.append("callStarted", callStart("k1"));
|
||||
ledger.append("callEnded", { call: "k1", ending: "completed" });
|
||||
assert.throws(() => ledger.append("callEnded", { call: "k1", ending: "failed", reason: "又报了一次" }), /call_ended_once/);
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("唯一约束:同一次消融结算两次报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
ledger.append("settlement", { ablation: "a1", result: { cells: { p1: "valid" } } });
|
||||
assert.throws(() => ledger.append("settlement", { ablation: "a1", result: { cells: { p1: "valid" } } }), /settlement_once/);
|
||||
ledger.append("settlement", { ablation: "a2", result: { cells: { p1: "insufficient" } } });
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("没有唯一约束的记录照写:同样的事件、决定写两次就是两条", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
const e = { event: "untested", reason: "剩余次数不够", objects: ["cell3"] };
|
||||
const first = ledger.append("event", e);
|
||||
const second = ledger.append("event", e);
|
||||
assert.equal(second.seq, first.seq + 1);
|
||||
const d = { decision: "batch", round: 1, rule: "bestExpected", ruleVersion: 1, params: {}, seed: null, input: { x: 1 }, output: ["c1"] };
|
||||
ledger.append("decision", d);
|
||||
ledger.append("decision", d);
|
||||
assert.equal(countRows(path), 4);
|
||||
});
|
||||
|
||||
test("内容不符合定义时不写入:多出的字段、提交内容不合格、提交编号 item 和提交种类对不上", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
openStage(ledger, "s1", "wrapUp", 1);
|
||||
assert.throws(() => ledger.append("runStarted", { run: "r1", extra: 1 } as never), /runStarted/);
|
||||
assert.throws(
|
||||
() => ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: null, version: 1, kind: "wrapUp", content: { body: "" } }),
|
||||
/wrapUp/,
|
||||
);
|
||||
// 提案、挑刺一人能交好几份,必须带编号;实现一人对一个候选只交一份,编号必须为空,
|
||||
// 否则另起一个编号就能绕开"实现不能替换"。
|
||||
openStage(ledger, "prop", "proposal", 1);
|
||||
assert.throws(
|
||||
() => ledger.append("submission", { stageId: "prop", author: "r1", target: null, item: null as never, version: 1, kind: "proposal", content: proposal(range(1, 2, 3)) }),
|
||||
/proposal/,
|
||||
);
|
||||
openStage(ledger, "impl", "implementation", 1, ["e1"]);
|
||||
assert.throws(
|
||||
() => ledger.append("submission", { stageId: "impl", author: "e1", target: "c1", item: "x" as never, version: 1, kind: "implementation", content: implementation() }),
|
||||
/implementation/,
|
||||
);
|
||||
assert.equal(countRows(path), 3); // 三条开阶段
|
||||
});
|
||||
|
||||
test("写提案:作者自评拆成一条封存,提案记录里不带它", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "s1", "proposal", 1);
|
||||
const record = ledger.append("submission", {
|
||||
stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)),
|
||||
});
|
||||
const all = ledger.records();
|
||||
const stored = all.find((r) => r.kind === "submission")!;
|
||||
assert.equal(JSON.stringify(stored.content).includes("authorEstimate"), false);
|
||||
assert.equal(JSON.stringify(record.content).includes("authorEstimate"), false);
|
||||
const { authorEstimate: _, ...withoutEstimate } = proposal(range(1, 2, 3));
|
||||
assert.deepEqual(stored.content.content, withoutEstimate);
|
||||
const seal = all.find((r) => r.kind === "seal")!;
|
||||
assert.deepEqual(seal.content, { proposal: record.seq, range: range(1, 2, 3) });
|
||||
assert.equal(seal.seq, record.seq + 1);
|
||||
});
|
||||
|
||||
test("退回:被退回的内容原样存,带退回理由;来自 Jev 的带原始概率和结论", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
const malformed = { factors: "不是数组" };
|
||||
ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: malformed, reason: "因子清单格式不对", jev: null });
|
||||
ledger.append("rejection", {
|
||||
stageId: "s6", author: "r2", kind: "response", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测",
|
||||
jev: { probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" },
|
||||
});
|
||||
const rejections = ledger.records().filter((r) => r.kind === "rejection");
|
||||
assert.deepEqual(rejections.map((r) => r.content.submitted), [malformed, { body: "改了评测" }]);
|
||||
assert.deepEqual(rejections[1]!.content.jev, { probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" });
|
||||
});
|
||||
|
||||
test("事务:中途出错,一条都没写进去;中途被拒收,前面写的也撤销;正常结束则全部写进去", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
assert.throws(
|
||||
() =>
|
||||
ledger.transaction(() => {
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
throw new Error("算到一半出错");
|
||||
}),
|
||||
/算到一半出错/,
|
||||
);
|
||||
assert.equal(countRows(path), 0);
|
||||
assert.throws(
|
||||
() =>
|
||||
ledger.transaction(() => {
|
||||
planRun(ledger, "run9", "forward", 1);
|
||||
ledger.append("runResult", { run: "run9", process: exited, judged: null });
|
||||
}),
|
||||
/确认启动/,
|
||||
);
|
||||
assert.equal(countRows(path), 0);
|
||||
ledger.transaction(() => {
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("runStarted", { run: "run1" });
|
||||
});
|
||||
assert.equal(countRows(path), 2);
|
||||
});
|
||||
|
||||
test("事务:异步函数被类型检查挡住", () => {
|
||||
// 只给 npm run check 看,不执行:await 之后的写入发生在事务提交之后,回滚管不到。
|
||||
const typeOnly = (ledger: Ledger) =>
|
||||
// @ts-expect-error 事务函数的返回值类型排除了 Promise。
|
||||
ledger.transaction(async () => {
|
||||
ledger.append("config", { n: 1 });
|
||||
});
|
||||
assert.equal(typeof typeOnly, "function");
|
||||
});
|
||||
|
||||
test("关掉再打开:读出的记录不变,序号接着往下排", (t) => {
|
||||
const path = join(tempDir(t), "ledger.db");
|
||||
const ledger = Ledger.open(path);
|
||||
ledger.append("config", { researchers: ["r1", "r2"] });
|
||||
openStage(ledger, "s1", "proposal", 1);
|
||||
ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)) });
|
||||
ledger.append("note", { author: "r2", version: 1, body: "笔记" });
|
||||
closeStage(ledger, "s1");
|
||||
planRun(ledger, "run1", "forward", 1);
|
||||
ledger.append("callStarted", callStart("k1"));
|
||||
const before = ledger.records();
|
||||
ledger.close();
|
||||
const reopened = Ledger.open(path);
|
||||
t.after(() => reopened.close());
|
||||
assert.deepEqual(reopened.records(), before);
|
||||
assert.equal(before.length, 8); // 配置、开阶段、提案、封存、笔记、关阶段、登记运行、调用开始
|
||||
assert.deepEqual(before.map((r) => r.hash), before.map((r) => hash(r.content)));
|
||||
assert.equal(reopened.append("runStarted", { run: "run1" }).seq, 9);
|
||||
});
|
||||
|
||||
test("打开:表结构版本不对报错;不是空文件也不是账本的库报错", (t) => {
|
||||
const dir = tempDir(t);
|
||||
const wrongVersion = join(dir, "v99.db");
|
||||
const db = new DatabaseSync(wrongVersion);
|
||||
db.exec("PRAGMA user_version = 99");
|
||||
db.close();
|
||||
assert.throws(() => Ledger.open(wrongVersion), /版本是 99/);
|
||||
|
||||
const foreign = join(dir, "foreign.db");
|
||||
const other = new DatabaseSync(foreign);
|
||||
other.exec("CREATE TABLE t (x INTEGER)");
|
||||
other.close();
|
||||
assert.throws(() => Ledger.open(foreign), /版本是 0/);
|
||||
});
|
||||
|
||||
test("打开:不是账本的库报错后,那个库的 journal_mode 不变", (t) => {
|
||||
const foreign = join(tempDir(t), "foreign.db");
|
||||
const other = new DatabaseSync(foreign);
|
||||
other.exec("CREATE TABLE t (x INTEGER)");
|
||||
const modeOf = (db: DatabaseSync) => (db.prepare("PRAGMA journal_mode").get() as { journal_mode: string }).journal_mode;
|
||||
const before = modeOf(other);
|
||||
other.close();
|
||||
assert.notEqual(before, "wal");
|
||||
assert.throws(() => Ledger.open(foreign), /版本是 0/);
|
||||
const reopened = new DatabaseSync(foreign);
|
||||
t.after(() => reopened.close());
|
||||
assert.equal(modeOf(reopened), before);
|
||||
});
|
||||
|
||||
test("读出时按种类检查内容:绕过 ledger 插进去的坏记录,读取时报错", (t) => {
|
||||
const { ledger, path } = freshLedger(t);
|
||||
const db = new DatabaseSync(path);
|
||||
t.after(() => db.close());
|
||||
db.exec(`INSERT INTO records (kind, content, hash) VALUES ('runStarted', '{"run":"run1","x":1}', 'h')`);
|
||||
assert.throws(() => ledger.records(), /runStarted/);
|
||||
});
|
||||
|
||||
test("查询:阶段关了没有、某份提交的最新一版(同一人对同一候选的另一份挑刺不算这一份的版本)", (t) => {
|
||||
const { ledger } = freshLedger(t);
|
||||
openStage(ledger, "s1", "critique", 1);
|
||||
assert.equal(ledger.stageClosed("s1"), false);
|
||||
const slot = { stageId: "s1", author: "r1", kind: "critique" as const, target: "c1", item: "k1" };
|
||||
assert.equal(ledger.latestSubmission(slot), undefined);
|
||||
ledger.append("submission", { ...slot, version: 1, content: { target: "whole", body: "第一版" } });
|
||||
const v2 = ledger.append("submission", { ...slot, version: 2, content: { target: "whole", body: "第二版" } });
|
||||
ledger.append("submission", { ...slot, item: "k2", version: 1, content: { target: "p1", body: "另一份" } });
|
||||
assert.deepEqual(ledger.latestSubmission(slot), v2);
|
||||
closeStage(ledger, "s1");
|
||||
assert.equal(ledger.stageClosed("s1"), true);
|
||||
});
|
||||
Reference in New Issue
Block a user