import assert from "node:assert/strict"; import { test } from "node:test"; import { Value } from "typebox/value"; import { canonical, hash, JudgedResult, RunDefinition, SUBMISSION_KINDS, SUBMISSION_SCHEMAS, type Checklist, type Submission, type SubmissionKind, } from "../src/vocab/index.ts"; // 预期字符串手写;预期哈希用命令行 `printf '%s' '<预期字符串>' | sha256sum` 算出。 test("规范序列化:键的顺序不同,结果相同", () => { const expected = '{"a":[true,null,"x"],"b":1,"c":{"y":"中","z":2.5}}'; const one = { b: 1, a: [true, null, "x"], c: { z: 2.5, y: "中" } }; const two = { c: { y: "中", z: 2.5 }, a: [true, null, "x"], b: 1 }; assert.equal(canonical(one), expected); assert.equal(canonical(two), expected); assert.equal(hash(one), "668eb8c3a097ac33abf5f690ab18c5f753b911ff1c9bbfd6163deb5939b68e5d"); assert.equal(hash(two), "668eb8c3a097ac33abf5f690ab18c5f753b911ff1c9bbfd6163deb5939b68e5d"); }); test("规范序列化:嵌套数组保持顺序,数组里的对象也排键", () => { const value = [{ b: 0, a: [{ d: 1, c: -1 }] }, [1, [2, []]], {}, "s"]; const expected = '[{"a":[{"c":-1,"d":1}],"b":0},[1,[2,[]]],{},"s"]'; assert.equal(canonical(value), expected); assert.equal(hash(value), "521ca7249ce6c90890b5645dd2f8d46b42f6e000df5ad86aff42415035c7e8cb"); assert.equal(hash({}), "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"); }); test("规范序列化:字符串转义(引号、换行、制表、控制字符、反斜杠、中文)与 -0", () => { const value = { "中": [-0], z: -0, s: 'a"b\n\t\u0001中\\' }; const expected = String.raw`{"s":"a\"b\n\t\u0001中\\","z":0,"中":[0]}`; assert.equal(canonical(value), expected); assert.equal(hash(value), "d4605ee7ceddf1373f60f837ae86ce24de7319705bbcbce4706309fc4415c79a"); }); test("规范序列化:NaN 和 Infinity 报错,嵌套在里面也报错", () => { for (const bad of [NaN, Infinity, -Infinity, { a: [1, NaN] }, [{ x: Infinity }]]) { assert.throws(() => canonical(bad), /有限数/); assert.throws(() => hash(bad), /有限数/); } }); test("规范序列化:带自定义原型的对象和非 JSON 值报错", () => { class Point { x = 1; } const bad: unknown[] = [ new Point(), Object.create(null), new Date(0), new Map(), { a: new Point() }, { a: undefined }, () => 1, 1n, ]; for (const value of bad) assert.throws(() => canonical(value), /纯 JSON/); }); // 每种提交一份合法样例,同时用来核对字段写的是不是方法.md 第五部分的意思。 const range = { q1: 2.6, q5: 2.62, q9: 2.63 }; const factors = [ { name: "多起点局部优化", method: "从 8 个随机起点各跑一次 SLSQP,取最好的", keySettings: "起点数 8", continues: null }, { name: "停滞期扰动", method: "连续 20 步没有改进时扰动半径", keySettings: "幅度 0.004", offBehavior: "不扰动", continues: "r1-c2.p1", }, ]; const metrics = { defined: [ { id: "first-feasible-seconds", meaning: "第一次得到合法摆法的时间", unit: "秒", direction: "minimize" as const, howMeasured: "从进程启动计时,到第一个通过校验的摆法写出为止,每次运行记一次", missing: "recordMissing" as const, }, ], reused: ["wall-seconds"], }; const body = "## 为什么可能有效\n……"; const samples: { [K in SubmissionKind]: Submission } = { entry: { entries: [{ name: "起点分布", body: body }, { name: "收尾修复", body: body }] }, discussion: { body, entries: [ { name: "a", body }, { name: "b", body }, { name: "c", body }, ], }, claim: { choices: ["researcher-2-e1", "researcher-1-e2"] }, proposal: { factors, safeguards: ["final-shrink"], metrics, authorEstimate: range, body }, firstPrediction: { range, body: "理由" }, critique: { target: "p2", body: "(推测)扰动可能和起点数混杂" }, response: { replies: [{ critique: "cr-1", reply: "改了 p2 的幅度" }], changes: { factors: [factors[0]!] }, clauses: ["在 solve.py 的 main 里加多起点循环"], body: "答复", }, unification: { factors, safeguards: [], metrics: { defined: [], reused: [] }, body, replies: [], clauses: ["c1 的内容", "c2 的内容"], sources: [{ proposal: "r2-p3", coverage: "体现在 p1" }], }, secondPrediction: { range: { q1: 2.6, q5: 2.6, q9: 2.6 }, body: "理由" }, implementation: { factorLocations: [{ factor: "p1", locations: ["solve.py:10-40"] }], safeguardLocations: [{ safeguard: "final-shrink", locations: ["solve.py:88", "util.py"] }], unassigned: [{ location: "solve.py:5", note: "补了一个 import" }], concerns: "无", body: "说明", }, repair: { factorLocations: [], safeguardLocations: [], unassigned: [], concerns: "无", body: "按核验意见补上 c2", }, verification: { clauses: [{ clause: "c1", found: true, where: "solve.py:10-40" }], factors: [{ factor: "p1", found: false, where: "看了 solve.py 全文" }], safeguards: [{ safeguard: "final-shrink", found: true, where: "solve.py:88" }], unassigned: [{ location: "solve.py:5", outOfScope: false, reason: "只是 import" }], body: "说明", }, explanation: { explanations: [ { body: "p1 在出力", cells: [{ factor: "p1", range }, { factor: "p2", range }] }, { body: "我没有具体的机制判断", cells: [{ factor: "p1", range: { q1: 2.3, q5: 2.55, q9: 2.64 } }] }, ], }, ablationImplementation: { alsoOff: [], cannotRun: null, concerns: "无" }, ablationVerification: { factors: [{ factor: "p1", off: true, where: "solve.py:10-40" }] }, wrapUp: { body: "被支持的:……" }, }; test("每种提交都有阶段、角色和字段定义", () => { assert.deepEqual(Object.keys(SUBMISSION_SCHEMAS).sort(), Object.keys(SUBMISSION_KINDS).sort()); }); for (const kind of Object.keys(SUBMISSION_KINDS) as SubmissionKind[]) { test(`合法样例通过字段定义检查:${kind}`, () => { const schema = SUBMISSION_SCHEMAS[kind]; const errors = [...Value.Errors(schema, samples[kind])].map( (e) => `${e.instancePath} ${e.message}`, ); assert.deepEqual(errors, []); }); } test("关掉消融时,不带 cells 的解释通过字段定义检查", () => { const submission = { explanations: [{ body: "机制:p1 在出力" }] }; assert.equal(Value.Check(SUBMISSION_SCHEMAS.explanation, submission), true); }); // 复现:TypeBox 的 Record 对带格式的键只生成 patternProperties,不合格式的键原先完全不检查。 test("环境变量和指标取值里,不合格式的键被拒收", () => { const run = { command: ["python3", "solve.py"], workdir: "/work", image: { digest: `sha256:${"a".repeat(64)}`, runtime: "runc" }, wallSeconds: 120, cores: 1, inheritEnv: false, env: { OMP_NUM_THREADS: "1" }, }; assert.equal(Value.Check(RunDefinition, run), true); assert.equal(Value.Check(RunDefinition, { ...run, env: { "1BAD-KEY": "x" } }), false); assert.equal(Value.Check(RunDefinition, { ...run, env: { "x-y": 5 } }), false); const judged = { verdict: "valid", verdictReason: "合法", score: { status: "measured", value: 2.6 }, metrics: { "wall-seconds": { status: "measured", value: 3 } }, answerPath: { status: "read", value: "main" }, safeguardTriggers: { status: "unreadable", reason: "没写" }, extra: {}, }; assert.equal(Value.Check(JudgedResult, judged), true); const badMetrics = { "../x": { status: "measured", value: 1 } }; assert.equal(Value.Check(JudgedResult, { ...judged, metrics: badMetrics }), false); }); test("对号清单:16 种提交种类每种都有一项,不多不少(类型断言,npm run check 时检查)", () => { type Missing = Exclude; type Extra = Exclude; const none: [Missing, Extra] extends [never, never] ? true : false = true; assert.equal(none, true); });