Files
InfoRSI/tests/ledger-store.test.ts
T
iomgaa 852600df02 feat: 诊断(任务书 18 第一块):诊断数学、四种提交、一次运行跑多个实验、账本与材料、flow 诊断各步
消融默认关,关掉时解释阶段一并跳过;选诊断实验用信息价值减成本。
eval/diagnosis/research.json 为第三块配置。vocab 字段说明的出口文字随提示词一起改,见下一次提交的 prompts/ 与同文件改动(本次含在内)。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 10:36:20 -04:00

508 lines
26 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 { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { test } from "node:test";
import { Ledger, type LedgerRecord } from "../src/ledger/index.ts";
import { hash, type JevJudgment } 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", wallSeconds: { status: "measured", value: 12.5 }, status: "submitted" });
assert.throws(
() => ledger.append("callEnded", { call: "k1", wallSeconds: { status: "measured", value: 3 }, status: "failed", reason: "又报了一次" }),
/call_ended_once/,
);
assert.equal(countRows(path), 2);
});
test("唯一约束:同一轮结算两次报错,不同轮各一次照写;结算不带消融编号", (t) => {
// 每轮最多采纳一个改法,所以最多一次消融(docs/结构.md 第四部分):结算按轮次只发生一次。
const { ledger, path } = freshLedger(t);
ledger.append("settlement", { round: 1, result: { cells: { p1: "valid" } } });
assert.throws(() => ledger.append("settlement", { round: 1, result: { cells: { p1: "insufficient" } } }), /settlement_once/);
ledger.append("settlement", { round: 2, result: { cells: { p1: "insufficient" } } });
ledger.append("settlement", { round: 3, result: { cells: { p1: "valid" } } });
assert.throws(() => ledger.append("settlement", { ablation: "a1", result: {} } as never), /settlement/);
assert.throws(() => ledger.append("settlement", { round: 4, ablation: "a4", result: {} } as never), /settlement/);
assert.equal(countRows(path), 3);
});
test("私人笔记:同一人写两条都存下,以后写的(序号大的)为准;笔记不带版本号", (t) => {
// 笔记不属于任何阶段,以最后写入的一条为准,序号就是先后(docs/结构.md 第四部分)。
// 后写的正文按字典序排在前面,中间夹着别人的笔记:取"序号最大的那条"才得到后写的。
const { ledger, path } = freshLedger(t);
ledger.append("note", { author: "r1", body: "z 先写的" });
ledger.append("note", { author: "r2", body: "别人的笔记" });
ledger.append("note", { author: "r1", body: "a 后写的" });
const mine = ledger
.records()
.filter((r): r is LedgerRecord<"note"> => r.kind === "note")
.filter((r) => r.content.author === "r1");
assert.deepEqual(mine.map((r) => [r.seq, r.content.body]), [[1, "z 先写的"], [3, "a 后写的"]]);
assert.equal(mine.at(-1)!.content.body, "a 后写的");
assert.throws(() => ledger.append("note", { author: "r1", version: 2, body: "带版本号" } as never), /note/);
assert.equal(countRows(path), 3);
});
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, jev: null, 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, jev: null, 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, jev: null, 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)), jev: null,
});
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);
});
// Jev 判断的样子照 src/vocab/jev.ts 的 JevJudgment 手写:问题名、问题全文哈希(64 位十六进制)、阈值、原始概率、结论、请求编号。
const judgment = (question: string, probability: number, threshold: number, requestId: string): JevJudgment => ({
question,
questionHash: "ab".repeat(32),
threshold,
probability,
yes: probability > threshold,
requestId,
});
test("退回:被退回的内容原样存;程序检查退回的 jev 为 null,Jev 退回的原样存它用到的几条判断", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
const malformed = { factors: "不是数组" };
// 冻结条款一次问三问,退回时三条判断都存,包括判"否"的两条。
const clauses = [
judgment("changes_measurement", 0.97, 0.5, "req-1"),
judgment("self_contradictory", 0.02, 0.6, "req-2"),
judgment("too_vague", 0.1, 0.7, "req-3"),
];
ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", target: null, submitted: malformed, reason: "因子清单格式不对", jev: null });
ledger.append("rejection", {
stageId: "s6", author: "r2", kind: "response", target: "r1-c1", submitted: { body: "改了评测" }, reason: "冻结条款改动了评测", jev: clauses,
});
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const rejections = reopened.records().filter((r) => r.kind === "rejection");
assert.deepEqual(rejections.map((r) => r.content.submitted), [malformed, { body: "改了评测" }]);
assert.deepEqual(rejections.map((r) => r.content.target), [null, "r1-c1"]);
assert.equal(rejections[0]!.content.jev, null);
assert.deepEqual(rejections[1]!.content.jev, [
{ question: "changes_measurement", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.97, yes: true, requestId: "req-1" },
{ question: "self_contradictory", questionHash: "ab".repeat(32), threshold: 0.6, probability: 0.02, yes: false, requestId: "req-2" },
{ question: "too_vague", questionHash: "ab".repeat(32), threshold: 0.7, probability: 0.1, yes: false, requestId: "req-3" },
]);
});
test("退回:对象字段必填,没有对象写 null;不写就拒收", (t) => {
const { ledger, path } = freshLedger(t);
assert.throws(
() => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", submitted: {}, reason: "缺对象", jev: null } as never),
/账本记录"rejection"的内容不符合定义/,
);
assert.equal(countRows(path), 0);
});
test("退回:jev 不合 JevJudgment 的拒收,一条都不写", (t) => {
const { ledger, path } = freshLedger(t);
const good = judgment("same_change", 0.9, 0.5, "req-1");
const { requestId: _, ...withoutRequest } = good;
const bad: unknown[] = [
[], // 空数组:Jev 退回至少有一条判断,程序退回写 null
{ probabilities: { yes: 0.97, no: 0.03 }, conclusion: "changesEvaluation" }, // 旧写法:不是数组
good, // 一条判断也要放进数组
[{ ...good, probability: 1.2 }], // 概率超出 0 到 1
[{ ...good, threshold: -0.1 }], // 阈值超出 0 到 1
[withoutRequest], // 缺请求编号
[{ ...good, requestId: "" }], // 请求编号为空
[{ ...good, questionHash: "abc" }], // 哈希不是 64 位十六进制
[{ ...good, question: "Same Change" }], // 问题名格式不对
[{ ...good, yes: "true" }], // 结论不是布尔值
[{ ...good, note: "多写一项" }], // 不认识的字段
];
for (const jev of bad) {
assert.throws(
() => ledger.append("rejection", { stageId: "s1", author: "r1", kind: "proposal", target: null, submitted: {}, reason: "判为同一个改法", jev } as never),
/账本记录"rejection"的内容不符合定义/,
JSON.stringify(jev),
);
}
assert.equal(countRows(path), 0);
});
test("模型调用结束:四种结局各存一条,墙钟时间(秒)测到带值、测不到带原因,关掉再打开读出不变", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
const endings = [
{ call: "k1", wallSeconds: { status: "measured", value: 812.25 }, status: "submitted" },
{ call: "k2", wallSeconds: { status: "measured", value: 95 }, status: "notSubmitted", reason: "模型自己停下了,一份都没交" },
{ call: "k3", wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断,恢复时补写" },
{ call: "k4", wallSeconds: { status: "measured", value: 0 }, status: "stopped" },
] as const;
for (const e of endings) {
ledger.append("callStarted", callStart(e.call));
ledger.append("callEnded", e);
}
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const ended = reopened.records().filter((r) => r.kind === "callEnded").map((r) => r.content);
assert.deepEqual(ended, [
{ call: "k1", wallSeconds: { status: "measured", value: 812.25 }, status: "submitted" },
{ call: "k2", wallSeconds: { status: "measured", value: 95 }, status: "notSubmitted", reason: "模型自己停下了,一份都没交" },
{ call: "k3", wallSeconds: { status: "unmeasured", reason: "进程中断" }, status: "failed", reason: "进程中断,恢复时补写" },
{ call: "k4", wallSeconds: { status: "measured", value: 0 }, status: "stopped" },
]);
assert.deepEqual(reopened.pending().calls, []);
});
test("模型调用结束:结局和原因对不上、墙钟时间缺失或不合 Measured、测到的值为负,都拒收", (t) => {
const { ledger, path } = freshLedger(t);
const s3 = { status: "measured", value: 3 };
const bad: unknown[] = [
{ call: "k1", wallSeconds: s3, status: "notSubmitted" }, // 没交要带原因
{ call: "k1", wallSeconds: s3, status: "failed" }, // 出错要带原因
{ call: "k1", wallSeconds: s3, status: "failed", reason: "" }, // 原因不能为空
{ call: "k1", wallSeconds: s3, status: "submitted", reason: "交了" }, // 交了不带原因
{ call: "k1", wallSeconds: s3, status: "stopped", reason: "用户停下" }, // 被停下不带原因
{ call: "k1", wallSeconds: s3, status: "completed" }, // 旧写法
{ call: "k1", status: "submitted" }, // 缺墙钟时间
{ call: "k1", wallSeconds: 3, status: "submitted" }, // 裸数字,不是 Measured
{ call: "k1", wallSeconds: null, status: "submitted" }, // 测不到不能写 null
{ call: "k1", wallSeconds: { status: "measured", value: "3" }, status: "submitted" }, // 值不是数
{ call: "k1", wallSeconds: { status: "measured" }, status: "submitted" }, // 测到却没带值
{ call: "k1", wallSeconds: { status: "unmeasured" }, status: "failed", reason: "进程中断" }, // 测不到却没带原因
{ call: "k1", wallSeconds: { status: "unmeasured", reason: "" }, status: "failed", reason: "进程中断" }, // 原因为空
{ call: "k1", wallSeconds: { status: "unmeasured", reason: "进程中断", value: 0 }, status: "failed", reason: "进程中断" }, // 测不到又带值
];
for (const content of bad) {
assert.throws(() => ledger.append("callEnded", content as never), /账本记录"callEnded"的内容不符合定义/, JSON.stringify(content));
}
assert.equal(countRows(path), 0);
});
test("提交:jev 在外层与 content 并列,null 或 JevJudgment 数组都能存能读;提案拆封存不受影响", (t) => {
const path = join(tempDir(t), "ledger.db");
const ledger = Ledger.open(path);
openStage(ledger, "s1", "proposal", 1);
openStage(ledger, "s9", "implementation", 1);
// 代码改动五问都判"否"才收下,五条判断连同原始概率都存。
const checks = [
judgment("unseeded_randomness", 0.01, 0.5, "req-1"),
judgment("reads_clock", 0.02, 0.5, "req-2"),
judgment("touches_measurement", 0.2, 0.6, "req-3"),
judgment("early_exit", 0.05, 0.5, "req-4"),
judgment("zero_for_unmeasured", 0.3, 0.7, "req-5"),
];
const p = ledger.append("submission", { stageId: "s1", author: "r1", target: null, item: "r1-1", version: 1, kind: "proposal", content: proposal(range(1, 2, 3)), jev: null });
ledger.append("submission", { stageId: "s9", author: "e1", target: "c1", item: null, version: 1, kind: "implementation", content: implementation(), jev: checks });
ledger.close();
const reopened = Ledger.open(path);
t.after(() => reopened.close());
const all = reopened.records();
const subs = all.filter((r) => r.kind === "submission");
assert.deepEqual(subs.map((r) => r.content.jev), [
null,
[
{ question: "unseeded_randomness", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.01, yes: false, requestId: "req-1" },
{ question: "reads_clock", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.02, yes: false, requestId: "req-2" },
{ question: "touches_measurement", questionHash: "ab".repeat(32), threshold: 0.6, probability: 0.2, yes: false, requestId: "req-3" },
{ question: "early_exit", questionHash: "ab".repeat(32), threshold: 0.5, probability: 0.05, yes: false, requestId: "req-4" },
{ question: "zero_for_unmeasured", questionHash: "ab".repeat(32), threshold: 0.7, probability: 0.3, yes: false, requestId: "req-5" },
],
]);
assert.equal("jev" in (subs[1]!.content.content as object), false);
const seal = all.find((r) => r.kind === "seal")!;
assert.deepEqual(seal.content, { proposal: p.seq, range: range(1, 2, 3) });
assert.equal(seal.seq, p.seq + 1);
});
test("提交:缺 jev、jev 不合 JevJudgment 数组、jev 写进 content 里,都拒收", (t) => {
const { ledger, path } = freshLedger(t);
openStage(ledger, "s1", "wrapUp", 1);
const good = judgment("same_change", 0.1, 0.5, "req-1");
const base = { stageId: "s1", author: "r1", target: null, item: null, version: 1, kind: "wrapUp", content: { body: "收尾" } };
const bad: unknown[] = [
base, // 缺 jev:没用到 Jev 要写 null,不靠省略
{ ...base, jev: [] }, // 空数组
{ ...base, jev: good }, // 一条也要放进数组
{ ...base, jev: [{ ...good, probability: -0.1 }] }, // 概率超出 0 到 1
{ ...base, jev: [{ ...good, extra: 1 }] }, // 不认识的字段
{ ...base, jev: null, content: { body: "收尾", jev: null } }, // jev 不进 content
];
for (const content of bad) {
assert.throws(() => ledger.append("submission", content as never), /账本记录"submission(提交种类 wrapUp)"的内容不符合定义/, JSON.stringify(content));
}
assert.equal(countRows(path), 1);
});
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, jev: null, kind: "proposal", content: proposal(range(1, 2, 3)) });
ledger.append("note", { author: "r2", 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 { ledger, path } = freshLedger(t);
ledger.append("diagnosisSettlement", { round: 1, result: { experiments: [] } });
assert.throws(() => ledger.append("diagnosisSettlement", { round: 1, result: { experiments: [] } }), /diagnosis_settlement_once/);
ledger.append("diagnosisSettlement", { round: 2, result: { experiments: [] } });
ledger.append("settlement", { round: 1, result: { cells: {} } });
assert.throws(() => ledger.append("diagnosisSettlement", { round: 3, run: "x", result: {} } as never), /diagnosisSettlement/);
assert.equal(countRows(path), 3);
});
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/);
// 版本 4 是还没有诊断结算和诊断运行用途时的账本,不迁移,打开报错。
const previous = join(dir, "v4.db");
const v4 = new DatabaseSync(previous);
v4.exec("PRAGMA user_version = 4");
v4.close();
assert.throws(() => Ledger.open(previous), /版本是 4.*只认版本 5/s);
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, jev: null, content: { target: "whole", body: "第一版" } });
const v2 = ledger.append("submission", { ...slot, version: 2, jev: null, content: { target: "whole", body: "第二版" } });
ledger.append("submission", { ...slot, item: "k2", version: 1, jev: null, content: { target: "p1", body: "另一份" } });
assert.deepEqual(ledger.latestSubmission(slot), v2);
closeStage(ledger, "s1");
assert.equal(ledger.stageClosed("s1"), true);
});