import assert from "node:assert/strict"; import { test } from "node:test"; import { checkConfig, stepList, type ResearchConfig, type StepList } from "../src/config/index.ts"; import { fullConfig, naiveConfig } from "./config-helpers.ts"; // 每份预期都对照 docs/结构.md 第三部分"步骤清单"和 docs/方法.md 第十一部分逐条手写,不从实现里推。 // 记法:[步骤, 执行 / 跳过, 规则];规则写名字和版本,版本取 rules 登记表里写的 1。 type Row = [string, "execute" | "skip", { name: string; version: number } | null]; const E = "execute" as const; const S = "skip" as const; function rows(steps: StepList["study"] | StepList["round"]): Row[] { return steps.map((s) => [s.id, s.status, s.rule]); } function listOf(change: (flow: Extract) => void): StepList { const config = fullConfig(); change(config.flow as Extract); return stepList(checkConfig(config)); } /** 跳过的步骤都写了原因。 */ function assertReasons(list: StepList): void { for (const s of [...list.study, ...list.round]) { if (s.status === "skip") assert.ok(s.reason.length > 0, `${s.id} 跳过却没写原因`); } } test("步骤清单:完整流程全开", () => { const list = listOf((f) => (f.diagnosis = { enabled: true, perResearcher: 2, maxExperiments: 4 })); assert.equal(list.preset, "full"); assert.deepEqual(rows(list.study), [ ["freezeConfig", E, null], ["baselineRun", E, null], ["entryDraft", E, null], ["discussion1", E, null], ["discussion2", E, null], ["claim", E, null], ["assignEntries", E, null], ["rounds", E, null], ["export", E, null], ]); assert.deepEqual(rows(list.round), [ ["openRound", E, null], ["carryOverPool", E, null], ["diagnosisProposal", E, null], ["diagnosisDedupe", E, null], ["diagnosisPrediction", E, null], ["selectDiagnosis", E, { name: "valueMinusCost", version: 1 }], ["diagnosisImplementation", E, null], ["diagnosisRun", E, null], ["diagnosisSettle", E, null], ["proposal", E, null], ["dedupe", E, null], ["firstPrediction", E, null], ["critique", E, null], ["response", E, null], ["freezeProtocol", E, null], ["secondPrediction", E, null], ["selectBatch", E, { name: "bestExpected", version: 1 }], ["implementation", E, null], ["verification", E, null], ["repair", E, null], ["forwardRun", E, null], ["publish", E, null], ["informationAccount", E, null], ["adopt", E, null], ["listCells", E, null], ["explanation", E, null], ["selectCells", E, { name: "valueMinusCost", version: 1 }], ["ablationImplementation", E, null], ["ablationVerification", E, null], ["ablationRun", E, null], ["settle", E, null], ["wrapUp", E, null], ]); }); test("步骤清单:只关第零轮", () => { const list = listOf((f) => (f.roundZero = { enabled: false })); assertReasons(list); assert.equal(list.preset, "full"); assert.deepEqual(rows(list.study), [ ["freezeConfig", E, null], ["baselineRun", E, null], ["entryDraft", S, null], ["discussion1", S, null], ["discussion2", S, null], ["claim", S, null], ["assignEntries", S, null], ["rounds", E, null], ["export", E, null], ]); assert.deepEqual(rows(list.round), rows(stepList(fullConfig()).round)); }); test("步骤清单:只关第一次预测", () => { const list = listOf((f) => (f.firstPrediction = false)); assertReasons(list); assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study)); assert.deepEqual(rows(list.round), [ ["openRound", E, null], ["carryOverPool", E, null], ["diagnosisProposal", S, null], ["diagnosisDedupe", S, null], ["diagnosisPrediction", S, null], ["selectDiagnosis", S, null], ["diagnosisImplementation", S, null], ["diagnosisRun", S, null], ["diagnosisSettle", S, null], ["proposal", E, null], ["dedupe", E, null], ["firstPrediction", S, null], ["critique", E, null], ["response", E, null], ["freezeProtocol", E, null], ["secondPrediction", E, null], ["selectBatch", E, { name: "bestExpected", version: 1 }], ["implementation", E, null], ["verification", E, null], ["repair", E, null], ["forwardRun", E, null], ["publish", E, null], ["informationAccount", E, null], ["adopt", E, null], ["listCells", E, null], ["explanation", E, null], ["selectCells", E, { name: "valueMinusCost", version: 1 }], ["ablationImplementation", E, null], ["ablationVerification", E, null], ["ablationRun", E, null], ["settle", E, null], ["wrapUp", E, null], ]); }); test("步骤清单:只关挑刺与回应,协议照样冻结", () => { const list = listOf((f) => (f.critique = false)); assertReasons(list); assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study)); assert.deepEqual(rows(list.round), [ ["openRound", E, null], ["carryOverPool", E, null], ["diagnosisProposal", S, null], ["diagnosisDedupe", S, null], ["diagnosisPrediction", S, null], ["selectDiagnosis", S, null], ["diagnosisImplementation", S, null], ["diagnosisRun", S, null], ["diagnosisSettle", S, null], ["proposal", E, null], ["dedupe", E, null], ["firstPrediction", E, null], ["critique", S, null], ["response", S, null], ["freezeProtocol", E, null], ["secondPrediction", E, null], ["selectBatch", E, { name: "bestExpected", version: 1 }], ["implementation", E, null], ["verification", E, null], ["repair", E, null], ["forwardRun", E, null], ["publish", E, null], ["informationAccount", E, null], ["adopt", E, null], ["listCells", E, null], ["explanation", E, null], ["selectCells", E, { name: "valueMinusCost", version: 1 }], ["ablationImplementation", E, null], ["ablationVerification", E, null], ["ablationRun", E, null], ["settle", E, null], ["wrapUp", E, null], ]); }); test("步骤清单:只关消融,解释也跳过,选格没有规则", () => { const list = listOf((f) => (f.ablation = { enabled: false })); assertReasons(list); assert.deepEqual(rows(list.study), rows(stepList(fullConfig()).study)); assert.deepEqual(rows(list.round), [ ["openRound", E, null], ["carryOverPool", E, null], ["diagnosisProposal", S, null], ["diagnosisDedupe", S, null], ["diagnosisPrediction", S, null], ["selectDiagnosis", S, null], ["diagnosisImplementation", S, null], ["diagnosisRun", S, null], ["diagnosisSettle", S, null], ["proposal", E, null], ["dedupe", E, null], ["firstPrediction", E, null], ["critique", E, null], ["response", E, null], ["freezeProtocol", E, null], ["secondPrediction", E, null], ["selectBatch", E, { name: "bestExpected", version: 1 }], ["implementation", E, null], ["verification", E, null], ["repair", E, null], ["forwardRun", E, null], ["publish", E, null], ["informationAccount", E, null], ["adopt", E, null], ["listCells", S, null], ["explanation", S, null], ["selectCells", S, null], ["ablationImplementation", S, null], ["ablationVerification", S, null], ["ablationRun", S, null], ["settle", S, null], ["wrapUp", E, null], ]); }); test("步骤清单:朴素循环", () => { const list = stepList(naiveConfig()); assertReasons(list); assert.equal(list.preset, "naive"); assert.deepEqual(rows(list.study), [ ["freezeConfig", E, null], ["baselineRun", E, null], ["entryDraft", S, null], ["discussion1", S, null], ["discussion2", S, null], ["claim", S, null], ["assignEntries", S, null], ["rounds", E, null], ["export", E, null], ]); assert.deepEqual(rows(list.round), [ ["openRound", E, null], ["carryOverPool", S, null], // 候选不留池 ["diagnosisProposal", S, null], // 不诊断 ["diagnosisDedupe", S, null], // 不诊断 ["diagnosisPrediction", S, null], // 不诊断 ["selectDiagnosis", S, null], // 不诊断 ["diagnosisImplementation", S, null], // 不诊断 ["diagnosisRun", S, null], // 不诊断 ["diagnosisSettle", S, null], // 不诊断 ["proposal", E, null], ["dedupe", E, null], // 判重照常 ["firstPrediction", S, null], // 不预测 ["critique", S, null], // 不挑刺 ["response", S, null], // 不回应 ["freezeProtocol", E, null], // 提案直接冻结 ["secondPrediction", S, null], // 不预测 ["selectBatch", E, { name: "uniformRandom", version: 1 }], // 均匀随机选批 ["implementation", E, null], ["verification", E, null], ["repair", E, null], ["forwardRun", E, null], ["publish", E, null], ["informationAccount", S, null], // 没有预测,记不了信息账 ["adopt", E, null], // 采纳规则和完整流程相同 ["listCells", S, null], // 不消融 ["explanation", S, null], ["selectCells", S, null], ["ablationImplementation", S, null], ["ablationVerification", S, null], ["ablationRun", S, null], ["settle", S, null], ["wrapUp", S, null], // 没有收尾 ]); }); test("步骤清单:第零轮讨论 1 次时跳过第 2 次;只有一位研究者时不讨论", () => { const once = listOf((f) => (f.roundZero = { enabled: true, discussions: 1 })); assertReasons(once); assert.deepEqual(rows(once.study).slice(2, 7), [ ["entryDraft", E, null], ["discussion1", E, null], ["discussion2", S, null], ["claim", E, null], ["assignEntries", E, null], ]); const config = fullConfig(); config.models.researchers = [config.models.researchers[0]!]; const single = stepList(checkConfig(config)); assertReasons(single); assert.deepEqual(rows(single.study).slice(2, 7), [ ["entryDraft", E, null], ["discussion1", S, null], ["discussion2", S, null], ["claim", E, null], ["assignEntries", E, null], ]); }); test("步骤清单:换规则后清单里的规则跟着换", () => { const list = listOf((f) => { f.batchRule = "uniformRandom"; f.ablation = { enabled: true, cellRule: "randomSameCount" }; }); const rule = (id: string) => list.round.find((s) => s.id === id)!.rule; assert.deepEqual(rule("selectBatch"), { name: "uniformRandom", version: 1 }); assert.deepEqual(rule("selectCells"), { name: "randomSameCount", version: 1 }); }); test("步骤清单:诊断开、消融关(现行配置):诊断七步执行,消融七步(含解释)跳过", () => { const list = listOf((f) => { f.diagnosis = { enabled: true, perResearcher: 2, maxExperiments: 4 }; f.ablation = { enabled: false }; }); assertReasons(list); const status = (id: string) => list.round.find((s) => s.id === id)!.status; for (const id of [ "diagnosisProposal", "diagnosisDedupe", "diagnosisPrediction", "selectDiagnosis", "diagnosisImplementation", "diagnosisRun", "diagnosisSettle", ]) { assert.equal(status(id), "execute", id); } for (const id of [ "listCells", "explanation", "selectCells", "ablationImplementation", "ablationVerification", "ablationRun", "settle", ]) { assert.equal(status(id), "skip", id); } // 诊断七步排在留池之后、提案之前(方法.md 4.3"诊断"一段)。 const ids = list.round.map((s) => s.id); assert.deepEqual(ids.slice(ids.indexOf("carryOverPool"), ids.indexOf("proposal") + 1), [ "carryOverPool", "diagnosisProposal", "diagnosisDedupe", "diagnosisPrediction", "selectDiagnosis", "diagnosisImplementation", "diagnosisRun", "diagnosisSettle", "proposal", ]); assert.deepEqual(list.round.find((s) => s.id === "selectDiagnosis")!.rule, { name: "valueMinusCost", version: 1 }); });