Files
InfoRSI/tests/jev.test.ts
T
iomgaa 13a7e60355 feat: 任务书 11,指标名加候选前缀、消融重做接着改、判重说明补关键设定
- 假设专用指标短名不能含"."(最长 64),冻结时由 materials 的 freezeProtocol 改为全名"候选编号.短名";可引用清单用全名,已占用清单只剩任务指标和两个保留键。
- 消融格核验不通过重做时,工作目录是上一次交的这一格代码的副本,能读的版本加上它,完整代码照旧列出;消融实现提示词同步改。
- jev 判重材料说明补 keySettings 一句。
- 结构.md 第八部分 vocab、materials、jev,WORK_LOG 同步更新。

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

310 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");
// 判重放名称、做法和关键设定:关掉时换成什么、延续哪个因子不放。材料说明里也讲了关键设定是什么(任务书 11 第三部分)。
assert.match(dedup.task, /`keySettings`: the setting values that decide its effect/);
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");
});