Files
InfoRSI/tests/jev.test.ts
T
iomgaa 09c44031a0 feat: 任务书 10,submissions 模块与 materials 修改
- 新增 src/submissions:先程序检查(14 条跨字段规则、不许改的文件、代码存不了、指标编号占用、提案数上限),全过再问 Jev;退回原因列出该写的全部编号;回应/统一拼冻结协议主体只在这一处;核验、消融核验通过没有。
- materials:挑刺单写"现在交什么";退回按阶段、作者、种类、对象找(ledger 退回记录加 target,表结构版本 4);核验材料去掉 Jev 预扫;信念本写合法化;删"本该关着";消融核验不通过可重做,不跑的格从 cellNotRun 事件读原因。
- 用户 2026-10-03 定:冻结协议去掉 replies、replyBody,第二次预测的答复从已公开的回应或统一读;takenMetricIds 加两个保留键;解释一律交 cells;Jev 退回只列判"是"的几问、用英文问题名。
- jev 判重材料带 keySettings;读代码工具删 offset;三处旧说法改正。
- 文档:结构.md 第四、七、八部分,WORK_LOG。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 06:08:34 -04:00

309 lines
14 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 { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test, type TestContext } from "node:test";
import { checkHunk, checkProtocol, codeHunks, judgeSameChange, type FrozenProtocol, type ProtocolRules } from "../src/jev/index.ts";
import { hash, type Json } from "../src/vocab/index.ts";
import { compareVersions, createRepository, saveVersion } from "../src/workspace/index.ts";
// jev 的测试(任务书 08 第七部分),不花钱:假 Jev 在测试自己的环境里造——把全局 fetch 换成假的,
// 产品代码照常向真实地址发请求,拿到的是这里造的回答。重试的等待把全局 setTimeout 换成立刻回调,并记下要等多久。
type Reply = { status: number; body: string; requestId: string | null };
type Sent = { url: string; auth: string | null; body: { model: string; state: any; questions: Record<string, any> } };
/** 装一个假 Jev:每次请求交给 answer 决定回什么,并记下发出去的请求。 */
function fakeJev(t: TestContext, answer: (body: Sent["body"], n: number) => Reply | Error): Sent[] {
const sent: Sent[] = [];
const key = process.env["TYPESAFE_API_KEY"];
process.env["TYPESAFE_API_KEY"] = "test-key";
t.after(() => {
if (key === undefined) delete process.env["TYPESAFE_API_KEY"];
else process.env["TYPESAFE_API_KEY"] = key;
});
t.mock.method(globalThis, "fetch", async (url: string, init: RequestInit) => {
const body = JSON.parse(init.body as string);
sent.push({ url, auth: new Headers(init.headers).get("authorization"), body });
const reply = answer(body, sent.length);
if (reply instanceof Error) throw reply;
const headers = reply.requestId === null ? {} : { "x-typesafe-request-id": reply.requestId };
return new Response(reply.body, { status: reply.status, headers });
});
return sent;
}
/** 正常回答:每一问的概率由 p 给,请求编号是 req_<第几次请求>。 */
function answerAll(p: (name: string) => number): (body: Sent["body"], n: number) => Reply {
return (body, n) => ({
status: 200,
requestId: `req_${n}`,
body: JSON.stringify({
model: "jev-test",
answers: Object.fromEntries(Object.keys(body.questions).map((name) => [name, { type: "noul", noul: p(name) }])),
}),
});
}
/** 把重试前的等待换成立刻继续,记下每次要等的毫秒数。 */
function instantWaits(t: TestContext): number[] {
const waits: number[] = [];
t.mock.method(globalThis, "setTimeout", (fn: () => void, ms: number) => {
waits.push(ms);
setImmediate(fn);
});
return waits;
}
const PROTOCOL: FrozenProtocol = {
factors: [
{
name: "外圈环半径缩到 0.45",
method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。",
keySettings: "环半径 0.45",
offBehavior: "回到 0.7",
continues: null,
},
],
safeguards: ["clip-radii"],
metrics: {
defined: [
{ id: "min-gap", meaning: "最小间隙", unit: "无", direction: "maximize", howMeasured: "最终解上所有圆对的最小间隙,测一次", missing: "recordMissing" },
],
reused: ["c1.overlap"],
},
body: "### 不许改什么\n\nharness.py 不改。",
clauses: ["外圈环半径常数改为 0.45(initial.py:36)"],
};
const RULES: ProtocolRules = {
body: PROTOCOL.body,
metrics: [{ id: "min-gap", howMeasured: "最终解上所有圆对的最小间隙,测一次", missing: "recordMissing" }],
};
const HUNK = { path: "initial.py", change: "modified", text: "@@ -1 +1 @@\n-a\n+b" } as const;
/** 参考项目 src/judge/questions.ts 的 JEV_THRESHOLDS 填圆那一套里这九问的值,抄自参考项目源码。 */
const REFERENCE_THRESHOLDS: Record<string, number> = {
same_change: 0.3,
self_contradictory: 0.5,
changes_measurement: 0.21,
too_vague: 0.78,
unseeded_randomness: 0.55,
reads_clock: 0.55,
touches_measurement: 0.65,
early_exit: 0.63,
zero_for_unmeasured: 0.62,
};
test("概率超过阈值判是,等于或低于阈值判否;阈值是参考项目的值", async (t) => {
// 冻结三问:第一问比阈值高 0.01,第二问正好等于阈值,第三问低 0.01。
const offsets: Record<string, number> = { self_contradictory: 0.01, changes_measurement: 0, too_vague: -0.01 };
const sent = fakeJev(t, answerAll((name) => Math.round(((REFERENCE_THRESHOLDS[name] as number) + (offsets[name] as number)) * 100) / 100));
const result = await checkProtocol("jev-x", PROTOCOL);
assert.deepEqual(
result.judgments.map((j) => [j.question, j.threshold, j.yes, j.requestId]),
[
["self_contradictory", 0.5, true, "req_1"],
["changes_measurement", 0.21, false, "req_2"],
["too_vague", 0.78, false, "req_3"],
],
);
assert.equal(result.flagged, true);
// 冻结三问每问单独一次请求,材料相同;模型名照传;密钥只在请求头里。
assert.deepEqual(
sent.map((s) => Object.keys(s.body.questions)),
[["self_contradictory"], ["changes_measurement"], ["too_vague"]],
);
for (const s of sent) {
assert.deepEqual(s.body.state, sent[0]?.body.state);
assert.equal(s.body.model, "jev-x");
assert.equal(s.auth, "Bearer test-key");
assert.equal(s.url, "https://api.typesafe.ai/v1/systemone");
assert.ok(!JSON.stringify(s.body).includes("test-key"));
}
});
test("一个差异块五问一次请求,判重一对一次请求;阈值都是参考项目的值", async (t) => {
const sent = fakeJev(t, answerAll((name) => (name === "early_exit" ? 0.64 : 0.2)));
const hunk = await checkHunk("jev-x", HUNK, RULES);
assert.equal(sent.length, 1);
assert.equal(hunk.flagged, true);
assert.deepEqual(
hunk.judgments.map((j) => [j.question, j.threshold, j.yes]),
[
["unseeded_randomness", 0.55, false],
["reads_clock", 0.55, false],
["touches_measurement", 0.65, false],
["early_exit", 0.63, true],
["zero_for_unmeasured", 0.62, false],
],
);
const same = await judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors);
assert.equal(sent.length, 2);
assert.deepEqual(same, { question: "same_change", questionHash: same.questionHash, threshold: 0.3, probability: 0.2, yes: false, requestId: "req_2" });
});
test("回答读不懂就报错,带上请求编号", async (t) => {
const cases: Array<[string, Reply, RegExp]> = [
["没有概率", { status: 200, requestId: "req_a", body: JSON.stringify({ answers: {} }) }, /same_change 没有 0 到 1 之间的概率(request-id: req_a)/],
[
"概率大于 1",
{ status: 200, requestId: "req_b", body: JSON.stringify({ answers: { same_change: { noul: 1.2 } } }) },
/same_change 没有 0 到 1 之间的概率(request-id: req_b)/,
],
[
"概率不是数",
{ status: 200, requestId: "req_c", body: JSON.stringify({ answers: { same_change: { noul: "0.4" } } }) },
/same_change 没有 0 到 1 之间的概率(request-id: req_c)/,
],
["不是 JSON", { status: 200, requestId: "req_d", body: "<html>" }, /不是 JSON(request-id: req_d)/],
["没带请求编号", { status: 200, requestId: null, body: JSON.stringify({ answers: { same_change: { noul: 0.1 } } }) }, /没带请求编号/],
];
for (const [what, reply, message] of cases) {
await t.test(what, async (t) => {
fakeJev(t, () => reply);
await assert.rejects(judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors), message);
});
}
});
test("调不通时按 2、10、30 秒重试,发满 4 次再报错", async (t) => {
const waits = instantWaits(t);
const sent = fakeJev(t, () => new TypeError("fetch failed"));
await assert.rejects(checkHunk("jev-x", HUNK, RULES), /发了 4 次都失败/);
assert.equal(sent.length, 4);
assert.deepEqual(waits, [2_000, 10_000, 30_000]);
});
test("服务暂时出错后恢复就照常判;请求本身被拒不重试;没有密钥直接报错", async (t) => {
await t.test("503 后恢复", async (t) => {
const waits = instantWaits(t);
const ok = answerAll(() => 0.9);
const sent = fakeJev(t, (body, n) => (n === 1 ? { status: 503, requestId: "req_x", body: "busy" } : ok(body, n)));
const j = await judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors);
assert.deepEqual([j.yes, j.requestId], [true, "req_2"]);
assert.equal(sent.length, 2);
assert.deepEqual(waits, [2_000]);
});
await t.test("422 直接报错", async (t) => {
const sent = fakeJev(t, () => ({ status: 422, requestId: "req_y", body: "bad" }));
await assert.rejects(judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors), /拒绝了请求,不重试:HTTP 422(request-id: req_y)/);
assert.equal(sent.length, 1);
});
await t.test("没有密钥", async (t) => {
const sent = fakeJev(t, answerAll(() => 0.1));
delete process.env["TYPESAFE_API_KEY"];
await assert.rejects(judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors), /TYPESAFE_API_KEY/);
assert.equal(sent.length, 0);
});
});
test("问题哈希把材料说明、措辞、示例、阈值一起算,改一个字就变", async (t) => {
const sent = fakeJev(t, answerAll(() => 0.1));
const judgments = [
await judgeSameChange("jev-x", PROTOCOL.factors, PROTOCOL.factors),
...(await checkProtocol("jev-x", PROTOCOL)).judgments,
...(await checkHunk("jev-x", HUNK, RULES)).judgments,
];
assert.equal(judgments.length, 9);
assert.equal(new Set(judgments.map((j) => j.questionHash)).size, 9);
// 从实际发出去的请求里取回每一问的材料说明和问题原文。
const asked = new Map(sent.flatMap((s) => Object.entries(s.body.questions).map(([name, noul]) => [name, { task: s.body.state.task, noul }])));
for (const j of judgments) {
const { task, noul } = asked.get(j.question) as { task: string; noul: Json };
const content = { task, name: j.question, noul, threshold: j.threshold };
assert.equal(j.questionHash, hash(content));
const changed = JSON.parse(JSON.stringify(content));
changed.noul.criteria.true.examples[0] = `${changed.noul.criteria.true.examples[0]}x`;
assert.notEqual(hash(changed), j.questionHash);
assert.notEqual(hash({ ...content, threshold: j.threshold + 0.01 }), j.questionHash);
assert.notEqual(hash({ ...content, task: `${task} ` }), j.questionHash);
}
});
test("材料:给定带类型的输入,发出去的材料与手写预期一致", async (t) => {
const sent = fakeJev(t, answerAll(() => 0.1));
const other = [{ name: "六行六边形网格", method: "用 6 行六边形网格摆圆心。", keySettings: "6 行", continues: "f2" }];
await judgeSameChange("jev-x", PROTOCOL.factors, other);
await checkProtocol("jev-x", PROTOCOL);
await checkHunk("jev-x", { path: "a.py", change: "added", text: "@@ -0,0 +1 @@\n+x" }, RULES);
const states = sent.map((s) => s.body.state);
const [dedup, freeze, , , diff] = states;
for (const state of states) assert.equal(typeof state.task, "string");
// 判重放名称、做法和关键设定:关掉时换成什么、延续哪个因子不放。
assert.deepEqual(dedup, {
task: dedup.task,
proposal_a: { factors: [{ name: "外圈环半径缩到 0.45", method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。", keySettings: "环半径 0.45" }] },
proposal_b: { factors: [{ name: "六行六边形网格", method: "用 6 行六边形网格摆圆心。", keySettings: "6 行" }] },
});
assert.deepEqual(freeze, {
task: freeze.task,
protocol: {
factors: [
{
name: "外圈环半径缩到 0.45",
method: "把 construct_packing 里外圈环半径常数 0.7 改为 0.45。",
keySettings: "环半径 0.45",
offBehavior: "回到 0.7",
continues: null,
},
],
safeguards: ["clip-radii"],
body: "### 不许改什么\n\nharness.py 不改。",
metrics: {
defined: [
{ id: "min-gap", meaning: "最小间隙", unit: "无", direction: "maximize", howMeasured: "最终解上所有圆对的最小间隙,测一次", missing: "recordMissing" },
],
reused: ["c1.overlap"],
},
clauses: ["外圈环半径常数改为 0.45(initial.py:36)"],
},
});
assert.deepEqual(diff, {
task: diff.task,
protocol: {
body: "### 不许改什么\n\nharness.py 不改。",
metrics: [{ id: "min-gap", howMeasured: "最终解上所有圆对的最小间隙,测一次", missing: "recordMissing" }],
},
change: { path: "a.py", change: "added", diff: "@@ -0,0 +1 @@\n+x" },
});
assert.equal(new Set(states.map((s) => s.task)).size, 3);
});
function tempDir(t: TestContext): string {
const dir = mkdtempSync(join(tmpdir(), "inforsi-jev-"));
t.after(() => rmSync(dir, { recursive: true, force: true }));
return dir;
}
test("把 workspace 的比较结果切成差异块,每块从 @@ 行起", (t) => {
const root = tempDir(t);
const repo = join(root, "v.git");
createRepository(repo);
const dir = join(root, "code");
mkdirSync(dir);
const lines = Array.from({ length: 30 }, (_, i) => `line ${i + 1}`);
writeFileSync(join(dir, "a.py"), `${lines.join("\n")}\n`);
writeFileSync(join(dir, "bin.dat"), Buffer.from([0, 1, 2]));
const v1 = saveVersion(repo, dir, []);
lines[1] = "changed 2";
lines[25] = "changed 26";
writeFileSync(join(dir, "a.py"), `${lines.join("\n")}\n`);
writeFileSync(join(dir, "bin.dat"), Buffer.from([0, 9, 2]));
writeFileSync(join(dir, "new.py"), "x = 1\n");
const v2 = saveVersion(repo, dir, []);
assert.ok(v1.saved && v2.saved);
const hunks = codeHunks(compareVersions(repo, v1.version, v2.version));
assert.deepEqual(
hunks.map((h) => [h.path, h.change, h.text.split("\n")[0]]),
[
["a.py", "modified", "@@ -1,5 +1,5 @@"],
["a.py", "modified", "@@ -23,7 +23,7 @@ line 22"],
["new.py", "added", "@@ -0,0 +1 @@"],
],
);
assert.equal(hunks[0]?.text, "@@ -1,5 +1,5 @@\n line 1\n-line 2\n+changed 2\n line 3\n line 4\n line 5");
assert.equal(hunks[2]?.text, "@@ -0,0 +1 @@\n+x = 1");
});