Files
InfoRSI/tests/rules-budget.test.ts
iomgaa 60c21ae346 feat: 研究规则 rules 模块(任务书 02)
rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。

预期值有三个来源:方法.md 的手算例;tests/fixtures/rules/ 下不看实现写的 Python 脚本;参考项目 calc15、calc14b 里与现行方法一致的部分。审阅做了两轮,每轮都换一个全新上下文的子 Agent,提出的问题都已修复或写明不修的理由。

用户确认的约定(原话"1–8 都同意"):入池、采纳、消融中发现都用严格不等号;成本用 max(0, ĝ₀);没测的格附"含它的最好组合"的净值;战绩在分数原值上判。三个数改为严格递增后,删掉了尖峰的全部专门处理(bfb96ba)。vocab 里 Range 的说明同步改为严格递增。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:21 -04:00

162 lines
5.6 KiB
TypeScript
Raw Permalink 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 { test } from "node:test";
import {
ablationCellLimit,
assignEntries,
decisionSeed,
forwardBatchSize,
runBalance,
seededRandom,
seededSample,
type RunState,
} from "../src/rules/index.ts";
// 预期值来源写在每条测试旁边。
test("第七部分扣次示例:运行次数 13、每批上限 2,逐行余额、每轮 k 与消融上限", () => {
// 来源:方法.md 第七部分示例表。每一行先按当时余额算出 k 或消融上限,再把这些运行记成确认启动。
const total = 13;
const cap = 2;
const runs: RunState[] = [];
const spend = (n: number): number => {
for (let i = 0; i < n; i++) runs.push("started");
return runBalance(total, runs).balance;
};
assert.equal(spend(1), 12); // 基线
assert.equal(forwardBatchSize(cap, 12), 2);
assert.equal(spend(2), 10); // 第 1 轮正向
assert.equal(ablationCellLimit(10), 9);
assert.equal(spend(2), 8); // 第 1 轮消融选中 2 格
assert.equal(forwardBatchSize(cap, 8), 2);
assert.equal(spend(2), 6); // 第 2 轮正向
assert.equal(forwardBatchSize(cap, 6), 2);
assert.equal(spend(2), 4); // 第 3 轮正向
assert.equal(ablationCellLimit(4), 3);
assert.equal(spend(0), 4); // 第 3 轮消融一格都不测
assert.equal(forwardBatchSize(cap, 4), 2);
assert.equal(spend(2), 2); // 第 4 轮正向
assert.equal(forwardBatchSize(cap, 2), 2);
assert.equal(spend(2), 0); // 第 5 轮正向,余额用完
assert.equal(runBalance(total, runs).charged, 13);
});
test("第七部分:确认启动的扣,确认没启动的退还,已预留还没结果的先占着名额", () => {
assert.deepEqual(runBalance(5, ["started", "reserved", "notStarted", "started"]), {
charged: 2,
reserved: 1,
balance: 2,
});
assert.deepEqual(runBalance(3, []), { charged: 0, reserved: 0, balance: 3 });
});
test("第七部分:余额 0、1、2 时的正向批量与消融上限", () => {
// 手算:k = min(2, 余额);消融上限 = 余额 − 1,余额为 0 或 1 时是 0。
assert.deepEqual([0, 1, 2].map((b) => forwardBatchSize(2, b)), [0, 1, 2]);
assert.deepEqual([0, 1, 2].map(ablationCellLimit), [0, 0, 1]);
});
test("4.2 入口分配第 1、2 条:只有一个人选就给他;几人同选时作者优先", () => {
// 手算:e2 被 r1、r2 同时选,作者 r2 在其中 → r2;e3 只有 r3 选 → r3;第 2 轮 r1 选 e1 → r1,第 2 志愿。
assert.deepEqual(
assignEntries({
researchers: [
{ id: "r1", choices: ["e2", "e1"] },
{ id: "r2", choices: ["e2", "e3"] },
{ id: "r3", choices: ["e3", "e1"] },
],
entries: [
{ id: "e1", author: "r1" },
{ id: "e2", author: "r2" },
{ id: "e3", author: "r3" },
{ id: "e4", author: "r1" },
],
}),
[
{ researcher: "r1", entry: "e1", rank: 2 },
{ researcher: "r2", entry: "e2", rank: 1 },
{ researcher: "r3", entry: "e3", rank: 1 },
],
);
});
test("4.2 入口分配第 2、3 条:作者不在其中给编号最小的;志愿用完的人按编号拿剩下编号最小的入口", () => {
// 手算:e3(作者 r3)被 r1、r2 同选,r3 不在其中 → r1。r3 选 e1 → r3。
// r2 志愿用完;剩下 e2、e4 → r2 拿 e2,rank 为 null。
assert.deepEqual(
assignEntries({
researchers: [
{ id: "r1", choices: ["e3"] },
{ id: "r2", choices: ["e3"] },
{ id: "r3", choices: ["e1"] },
],
entries: [
{ id: "e1", author: "r1" },
{ id: "e2", author: "r2" },
{ id: "e3", author: "r3" },
{ id: "e4", author: "r1" },
],
}),
[
{ researcher: "r1", entry: "e3", rank: 1 },
{ researcher: "r2", entry: "e2", rank: null },
{ researcher: "r3", entry: "e1", rank: 1 },
],
);
// 两个人都落空:按编号各拿剩下的编号最小的。
assert.deepEqual(
assignEntries({
researchers: [
{ id: "r1", choices: ["e1"] },
{ id: "r2", choices: ["e1"] },
{ id: "r3", choices: ["e1"] },
],
entries: [
{ id: "e1", author: "r9" },
{ id: "e2", author: "r9" },
{ id: "e3", author: "r9" },
],
}).map((a) => [a.researcher, a.entry, a.rank]),
[
["r1", "e1", 1],
["r2", "e2", null],
["r3", "e3", null],
],
);
});
test("4.2 N = 1 时直接分第一志愿", () => {
assert.deepEqual(
assignEntries({
researchers: [{ id: "r1", choices: ["e3", "e1"] }],
entries: [
{ id: "e1", author: "r1" },
{ id: "e2", author: "r1" },
{ id: "e3", author: "r1" },
],
}),
[{ researcher: "r1", entry: "e3", rank: 1 }],
);
});
test("第九部分:种子 = hash({ studyId, round, version })", () => {
// 预期哈希用命令行 printf '%s' '{"round":1,"studyId":"s1","version":1}' | sha256sum 算出。
assert.equal(
decisionSeed({ studyId: "s1", round: 1, version: 1 }),
"0fcf4530be51b098065a8f4a8774afc2c0be8f3a6de9b3b670bd26396d2c18ea",
);
});
test("第九部分:同一个种子永远得到同一串数;抽样只取决于种子和编号集合", () => {
const a = seededRandom("seed");
const b = seededRandom("seed");
const xs = Array.from({ length: 50 }, () => a.uniformInt(7));
assert.deepEqual(
Array.from({ length: 50 }, () => b.uniformInt(7)),
xs,
);
assert.ok(xs.every((x) => Number.isInteger(x) && x >= 0 && x < 7));
assert.deepEqual(seededSample(["c", "a", "b", "d"], 2, "k"), seededSample(["d", "b", "a", "c"], 2, "k"));
assert.deepEqual(seededSample(["c", "a", "b"], 3, "k"), ["a", "b", "c"]);
assert.deepEqual(seededSample(["c", "a", "b"], 0, "k"), []);
});