pi-skill-evolve 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +81 -2
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/after.md +10 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/delete-ban/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/after.md +9 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/rg-to-find/fixture.json +6 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/after.md +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/change.diff +8 -0
- package/extensions/skill-evolve/fixtures/selftest/weak-description/fixture.json +6 -0
- package/extensions/skill-evolve/index.ts +300 -0
- package/extensions/skill-evolve/lib/apply-gates.ts +204 -0
- package/extensions/skill-evolve/lib/apply-targets.ts +90 -0
- package/extensions/skill-evolve/lib/apply.ts +141 -0
- package/extensions/skill-evolve/lib/attribution.ts +138 -0
- package/extensions/skill-evolve/lib/audit.ts +221 -0
- package/extensions/skill-evolve/lib/baseline.ts +176 -0
- package/extensions/skill-evolve/lib/child-env.ts +10 -0
- package/extensions/skill-evolve/lib/classify-action.ts +161 -0
- package/extensions/skill-evolve/lib/command.ts +100 -0
- package/extensions/skill-evolve/lib/config-command.ts +38 -0
- package/extensions/skill-evolve/lib/config.ts +194 -0
- package/extensions/skill-evolve/lib/detect.ts +62 -0
- package/extensions/skill-evolve/lib/diff-apply.ts +90 -0
- package/extensions/skill-evolve/lib/diff-gen.ts +78 -0
- package/extensions/skill-evolve/lib/dispatch.ts +300 -0
- package/extensions/skill-evolve/lib/evolution-apply.ts +265 -0
- package/extensions/skill-evolve/lib/evolution-model.ts +72 -0
- package/extensions/skill-evolve/lib/evolution-observe.ts +82 -0
- package/extensions/skill-evolve/lib/evolution-views.ts +89 -0
- package/extensions/skill-evolve/lib/evolution.ts +289 -0
- package/extensions/skill-evolve/lib/flags.ts +44 -0
- package/extensions/skill-evolve/lib/gate0.ts +78 -0
- package/extensions/skill-evolve/lib/hard-rules.ts +167 -0
- package/extensions/skill-evolve/lib/lock.ts +215 -0
- package/extensions/skill-evolve/lib/logfile.ts +52 -0
- package/extensions/skill-evolve/lib/mechanical-report.ts +95 -0
- package/extensions/skill-evolve/lib/mechanical.ts +136 -0
- package/extensions/skill-evolve/lib/metrics.ts +172 -0
- package/extensions/skill-evolve/lib/model-config.ts +95 -0
- package/extensions/skill-evolve/lib/notify.ts +132 -0
- package/extensions/skill-evolve/lib/panel.ts +174 -0
- package/extensions/skill-evolve/lib/pending.ts +34 -0
- package/extensions/skill-evolve/lib/proc.ts +65 -0
- package/extensions/skill-evolve/lib/prompt.ts +80 -0
- package/extensions/skill-evolve/lib/proposal.ts +227 -0
- package/extensions/skill-evolve/lib/prune-plan.ts +62 -0
- package/extensions/skill-evolve/lib/queue.ts +136 -0
- package/extensions/skill-evolve/lib/replay-fixture.ts +80 -0
- package/extensions/skill-evolve/lib/replay.ts +246 -0
- package/extensions/skill-evolve/lib/report.ts +181 -0
- package/extensions/skill-evolve/lib/review-post.ts +101 -0
- package/extensions/skill-evolve/lib/review-prompt.ts +82 -0
- package/extensions/skill-evolve/lib/review-record.ts +63 -0
- package/extensions/skill-evolve/lib/review-run.ts +131 -0
- package/extensions/skill-evolve/lib/review-spawn.ts +83 -0
- package/extensions/skill-evolve/lib/review-supervisor.ts +267 -0
- package/extensions/skill-evolve/lib/round.ts +116 -0
- package/extensions/skill-evolve/lib/run-watch.ts +34 -0
- package/extensions/skill-evolve/lib/runs.ts +208 -0
- package/extensions/skill-evolve/lib/selftest.ts +201 -0
- package/extensions/skill-evolve/lib/session-index.ts +145 -0
- package/extensions/skill-evolve/lib/shutdown-dispatch.ts +177 -0
- package/extensions/skill-evolve/lib/single-flight.ts +111 -0
- package/extensions/skill-evolve/lib/spawn-plan.ts +103 -0
- package/extensions/skill-evolve/lib/stale.ts +217 -0
- package/extensions/skill-evolve/lib/telemetry.ts +205 -0
- package/extensions/skill-evolve/lib/ui-gate.ts +34 -0
- package/extensions/skill-evolve/lib/web/actions.ts +299 -0
- package/extensions/skill-evolve/lib/web/activity.ts +57 -0
- package/extensions/skill-evolve/lib/web/applied.ts +64 -0
- package/extensions/skill-evolve/lib/web/approvals.ts +58 -0
- package/extensions/skill-evolve/lib/web/audit-log.ts +115 -0
- package/extensions/skill-evolve/lib/web/autostart.ts +177 -0
- package/extensions/skill-evolve/lib/web/data.ts +298 -0
- package/extensions/skill-evolve/lib/web/diff-lines.ts +141 -0
- package/extensions/skill-evolve/lib/web/filter.ts +203 -0
- package/extensions/skill-evolve/lib/web/http.ts +68 -0
- package/extensions/skill-evolve/lib/web/json-read.ts +33 -0
- package/extensions/skill-evolve/lib/web/panel-pid.ts +99 -0
- package/extensions/skill-evolve/lib/web/render-controls.ts +156 -0
- package/extensions/skill-evolve/lib/web/render-rows.ts +252 -0
- package/extensions/skill-evolve/lib/web/render-run.ts +188 -0
- package/extensions/skill-evolve/lib/web/render-skills.ts +164 -0
- package/extensions/skill-evolve/lib/web/render.ts +136 -0
- package/extensions/skill-evolve/lib/web/reports.ts +160 -0
- package/extensions/skill-evolve/lib/web/run-detail.ts +234 -0
- package/extensions/skill-evolve/lib/web/run-path.ts +21 -0
- package/extensions/skill-evolve/lib/web/server.ts +268 -0
- package/extensions/skill-evolve/lib/web/shell.ts +87 -0
- package/extensions/skill-evolve/lib/web/skills.ts +202 -0
- package/extensions/skill-evolve/lib/web/style.ts +107 -0
- package/extensions/skill-evolve/lib/web/text.ts +52 -0
- package/extensions/skill-evolve/lib/web/trash.ts +36 -0
- package/extensions/skill-evolve/lib/weekly.ts +285 -0
- package/extensions/skill-evolve/prompts//345/244/215/347/233/230.md +172 -0
- package/extensions/skill-evolve/scripts/replay.ts +93 -0
- package/extensions/skill-evolve/test/apply.test.ts +278 -0
- package/extensions/skill-evolve/test/approve.test.ts +248 -0
- package/extensions/skill-evolve/test/attribution.test.ts +133 -0
- package/extensions/skill-evolve/test/audit.test.ts +151 -0
- package/extensions/skill-evolve/test/baseline.test.ts +132 -0
- package/extensions/skill-evolve/test/classify-action.test.ts +133 -0
- package/extensions/skill-evolve/test/command.test.ts +267 -0
- package/extensions/skill-evolve/test/config-command.test.ts +73 -0
- package/extensions/skill-evolve/test/config.test.ts +154 -0
- package/extensions/skill-evolve/test/detect.test.ts +71 -0
- package/extensions/skill-evolve/test/diff-apply.test.ts +46 -0
- package/extensions/skill-evolve/test/dispatch-busy.test.ts +223 -0
- package/extensions/skill-evolve/test/dispatch-supervisor.test.ts +88 -0
- package/extensions/skill-evolve/test/dispatch.test.ts +306 -0
- package/extensions/skill-evolve/test/evolution.test.ts +378 -0
- package/extensions/skill-evolve/test/fake-review-spawn.ts +60 -0
- package/extensions/skill-evolve/test/hard-rules.test.ts +116 -0
- package/extensions/skill-evolve/test/lock.test.ts +138 -0
- package/extensions/skill-evolve/test/logfile.test.ts +85 -0
- package/extensions/skill-evolve/test/mechanical-wiring.test.ts +217 -0
- package/extensions/skill-evolve/test/metrics.test.ts +129 -0
- package/extensions/skill-evolve/test/model-config.test.ts +84 -0
- package/extensions/skill-evolve/test/notify.test.ts +133 -0
- package/extensions/skill-evolve/test/observe.test.ts +100 -0
- package/extensions/skill-evolve/test/panel-pid.test.ts +84 -0
- package/extensions/skill-evolve/test/panel.test.ts +115 -0
- package/extensions/skill-evolve/test/proposal.test.ts +159 -0
- package/extensions/skill-evolve/test/queue.test.ts +110 -0
- package/extensions/skill-evolve/test/reject.test.ts +134 -0
- package/extensions/skill-evolve/test/replay.test.ts +111 -0
- package/extensions/skill-evolve/test/report.test.ts +66 -0
- package/extensions/skill-evolve/test/review-post.test.ts +92 -0
- package/extensions/skill-evolve/test/review-prompt.test.ts +105 -0
- package/extensions/skill-evolve/test/review-spawn.test.ts +81 -0
- package/extensions/skill-evolve/test/review-supervisor.test.ts +243 -0
- package/extensions/skill-evolve/test/selftest.test.ts +113 -0
- package/extensions/skill-evolve/test/session-index.test.ts +128 -0
- package/extensions/skill-evolve/test/single-flight.test.ts +122 -0
- package/extensions/skill-evolve/test/stale.test.ts +272 -0
- package/extensions/skill-evolve/test/telemetry.test.ts +145 -0
- package/extensions/skill-evolve/test/ui-gate.test.ts +77 -0
- package/extensions/skill-evolve/test/web-actions.test.ts +262 -0
- package/extensions/skill-evolve/test/web-applied.test.ts +100 -0
- package/extensions/skill-evolve/test/web-audit.test.ts +87 -0
- package/extensions/skill-evolve/test/web-autostart.test.ts +268 -0
- package/extensions/skill-evolve/test/web-data.test.ts +420 -0
- package/extensions/skill-evolve/test/web-diff-lines.test.ts +155 -0
- package/extensions/skill-evolve/test/web-filter.test.ts +212 -0
- package/extensions/skill-evolve/test/web-panel-pid.test.ts +97 -0
- package/extensions/skill-evolve/test/web-render-run.test.ts +132 -0
- package/extensions/skill-evolve/test/web-render-skills.test.ts +134 -0
- package/extensions/skill-evolve/test/web-render.test.ts +300 -0
- package/extensions/skill-evolve/test/web-run-detail.test.ts +256 -0
- package/extensions/skill-evolve/test/web-server-views.test.ts +113 -0
- package/extensions/skill-evolve/test/web-server.test.ts +465 -0
- package/extensions/skill-evolve/test/web-shell.test.ts +67 -0
- package/extensions/skill-evolve/test/web-skills.test.ts +239 -0
- package/extensions/skill-evolve/test/weekly.test.ts +123 -0
- package/package.json +34 -4
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 督导启动单测:这一层决定「用 execPath 而不是 pi」「带 --experimental-strip-types」
|
|
3
|
+
* 「env 里去掉 SKILL_EVOLVE_CHILD」三件事,接错任何一条都会让评审永远不启动。
|
|
4
|
+
*/
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { SUPERVISOR_SCRIPT, spawnReviewSupervisor } from "../lib/review-spawn.ts";
|
|
9
|
+
|
|
10
|
+
function capture(): { calls: { command: string; args: string[]; options: Record<string, unknown> }[]; unrefs: number } {
|
|
11
|
+
return { calls: [], unrefs: 0 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("督导用 node + --experimental-strip-types 起,参数里带 runDir", () => {
|
|
15
|
+
const seen = capture();
|
|
16
|
+
const pid = spawnReviewSupervisor({
|
|
17
|
+
runDir: "/repo/.pi/evolution/runs/run-1",
|
|
18
|
+
cwd: "/repo",
|
|
19
|
+
env: {},
|
|
20
|
+
spawnFn: (command, args, options) => {
|
|
21
|
+
seen.calls.push({ command, args, options });
|
|
22
|
+
return {
|
|
23
|
+
pid: 4321,
|
|
24
|
+
unref: () => {
|
|
25
|
+
seen.unrefs += 1;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
assert.equal(pid, 4321);
|
|
31
|
+
assert.equal(seen.calls[0].command, process.execPath);
|
|
32
|
+
assert.deepEqual(seen.calls[0].args, ["--experimental-strip-types", SUPERVISOR_SCRIPT, "/repo/.pi/evolution/runs/run-1"]);
|
|
33
|
+
assert.equal(seen.calls[0].options.detached, true);
|
|
34
|
+
assert.equal(seen.calls[0].options.stdio, "ignore");
|
|
35
|
+
assert.equal(seen.unrefs, 1, "必须 unref,否则主进程退出被拖住");
|
|
36
|
+
assert.equal(path.basename(SUPERVISOR_SCRIPT), "review-supervisor.ts");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("inherit 不追加 --model,显式模型追加在 runDir 之后", () => {
|
|
40
|
+
const inheritArgs: string[][] = [];
|
|
41
|
+
const explicitArgs: string[][] = [];
|
|
42
|
+
const spawnFn = (command: string, args: string[], options: Record<string, unknown>) => {
|
|
43
|
+
inheritArgs.push(args);
|
|
44
|
+
explicitArgs.push(args);
|
|
45
|
+
return { pid: 1 };
|
|
46
|
+
};
|
|
47
|
+
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, reviewModel: "inherit", spawnFn });
|
|
48
|
+
spawnReviewSupervisor({ runDir: "/r", cwd: "/repo", env: {}, reviewModel: "deepseek/deepseek-v3.2", spawnFn });
|
|
49
|
+
assert.deepEqual(inheritArgs[0], ["--experimental-strip-types", SUPERVISOR_SCRIPT, "/r"]);
|
|
50
|
+
assert.deepEqual(explicitArgs[1], ["--experimental-strip-types", SUPERVISOR_SCRIPT, "/r", "deepseek/deepseek-v3.2"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("env 里去掉 SKILL_EVOLVE_CHILD,其他变量照旧带走", () => {
|
|
54
|
+
let env: NodeJS.ProcessEnv = {};
|
|
55
|
+
spawnReviewSupervisor({
|
|
56
|
+
runDir: "/r",
|
|
57
|
+
cwd: "/repo",
|
|
58
|
+
env: { SKILL_EVOLVE_CHILD: "1", PATH: "/usr/bin" },
|
|
59
|
+
spawnFn: (_command, _args, options) => {
|
|
60
|
+
env = options.env as NodeJS.ProcessEnv;
|
|
61
|
+
return { pid: 1 };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
assert.equal(env.SKILL_EVOLVE_CHILD, undefined);
|
|
65
|
+
assert.equal(env.PATH, "/usr/bin");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("起不来只记日志返回 null,不抛", () => {
|
|
69
|
+
const logs: string[] = [];
|
|
70
|
+
const pid = spawnReviewSupervisor({
|
|
71
|
+
runDir: "/r",
|
|
72
|
+
cwd: "/repo",
|
|
73
|
+
env: {},
|
|
74
|
+
log: (msg) => logs.push(msg),
|
|
75
|
+
spawnFn: () => {
|
|
76
|
+
throw new Error("EMFILE");
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
assert.equal(pid, null);
|
|
80
|
+
assert.match(logs.join("\n"), /启动失败/);
|
|
81
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 评审督导单测(设计稿 12):不真起 pi、不真等 900/300 秒。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖四条最容易静默失败的路径:等提案超时、评审进程超时、invalid 重评、机械复验一票否决。
|
|
5
|
+
* 文件系统用内存 Map 替代,readText/writeText 都可注入。
|
|
6
|
+
*/
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { compileRules } from "../lib/hard-rules.ts";
|
|
11
|
+
import { checkCandidateFrontmatter, parseProposal, runReviewSupervisor } from "../lib/review-supervisor.ts";
|
|
12
|
+
import { fakeClock, fakeReviewSpawn, hangingReviewSpawn } from "./fake-review-spawn.ts";
|
|
13
|
+
|
|
14
|
+
const RUN_DIR = "/repo/.pi/evolution/runs/run-1";
|
|
15
|
+
const SKILL_FILE = "/repo/.pi/skills/demo/SKILL.md";
|
|
16
|
+
const SKILL_TEXT = ["---", "name: demo", "description: 旧描述", "---", "", "检索一律用 rg,禁止 find。"].join("\n");
|
|
17
|
+
const DESC_DIFF = [
|
|
18
|
+
"--- a/.pi/skills/demo/SKILL.md",
|
|
19
|
+
"+++ b/.pi/skills/demo/SKILL.md",
|
|
20
|
+
"@@ -3,1 +3,1 @@",
|
|
21
|
+
"-description: 旧描述",
|
|
22
|
+
"+description: 检索日志时用 rg,禁止 find 全盘扫",
|
|
23
|
+
].join("\n");
|
|
24
|
+
const PASS = JSON.stringify({ verdict: "pass", blocking: [], notes: "没问题" });
|
|
25
|
+
const RUN_JSON = JSON.stringify({ runId: "run-1", status: "dispatched", cwd: "/repo", stages: [{ name: "analyze", model: "deepseek/flash", usage: {}, ms: 10 }] });
|
|
26
|
+
/** 只放一条与仓库同形的 rg-over-find:用例不依赖 .pi/evolution/rules 文件是否在场 */
|
|
27
|
+
const RULES = compileRules({
|
|
28
|
+
rules: [{ id: "rg-over-find", level: "P0", pattern: "\\bfind\\s+[.~/]", expect: "禁止 find 全盘扫", why: "慢" }],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function memFs(files: Record<string, string>) {
|
|
32
|
+
const store = new Map(Object.entries(files).map(([file, text]) => [path.resolve(file), text]));
|
|
33
|
+
return {
|
|
34
|
+
readText: (file: string) => store.get(path.resolve(file)),
|
|
35
|
+
writeText: (file: string, text: string) => {
|
|
36
|
+
store.set(path.resolve(file), text);
|
|
37
|
+
},
|
|
38
|
+
text: (file: string) => store.get(path.resolve(file)),
|
|
39
|
+
json: (file: string) => {
|
|
40
|
+
const raw = store.get(path.resolve(file));
|
|
41
|
+
return raw === undefined ? undefined : (JSON.parse(raw) as Record<string, unknown>);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function baseFiles(): Record<string, string> {
|
|
47
|
+
return {
|
|
48
|
+
[`${RUN_DIR}/run.json`]: RUN_JSON,
|
|
49
|
+
[`${RUN_DIR}/02-proposal.json`]: JSON.stringify({ type: "improve", skill: "demo", rationale: "理由不该进评审", evidence: [3], diffFile: "02-proposal.diff" }),
|
|
50
|
+
[`${RUN_DIR}/02-proposal.diff`]: DESC_DIFF,
|
|
51
|
+
[SKILL_FILE]: SKILL_TEXT,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("正常路径:pass → reviewed,写 03-review.json/.md 并回写 run.json", async () => {
|
|
56
|
+
const fs = memFs(baseFiles());
|
|
57
|
+
const prompts: string[] = [];
|
|
58
|
+
const outcome = await runReviewSupervisor({
|
|
59
|
+
runDir: RUN_DIR,
|
|
60
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
61
|
+
reviewModel: "deepseek/flash",
|
|
62
|
+
readText: fs.readText,
|
|
63
|
+
writeText: fs.writeText,
|
|
64
|
+
spawnFn: fakeReviewSpawn([PASS]),
|
|
65
|
+
pollMs: 1,
|
|
66
|
+
// 记下发给评审的 prompt,验证独立性(不给 rationale)
|
|
67
|
+
...{},
|
|
68
|
+
});
|
|
69
|
+
assert.equal(outcome.status, "reviewed");
|
|
70
|
+
const review = fs.json(`${RUN_DIR}/03-review.json`) as Record<string, unknown>;
|
|
71
|
+
assert.equal(review.status, "reviewed");
|
|
72
|
+
assert.equal(review.verdict, "pass");
|
|
73
|
+
assert.equal(review.reviewerModel, "deepseek/flash");
|
|
74
|
+
assert.equal(review.proposerModel, "deepseek/flash");
|
|
75
|
+
assert.equal(review.sameModel, true);
|
|
76
|
+
assert.equal(review.action, "auto");
|
|
77
|
+
const md = fs.text(`${RUN_DIR}/03-review.md`) ?? "";
|
|
78
|
+
assert.ok(md.startsWith("> 同模型自评"), "同模型自评警告必须在首行");
|
|
79
|
+
const run = fs.json(`${RUN_DIR}/run.json`) as Record<string, unknown>;
|
|
80
|
+
assert.equal(run.status, "reviewed");
|
|
81
|
+
assert.deepEqual((run.review as Record<string, unknown>).sameModel, true);
|
|
82
|
+
assert.equal(prompts.length, 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("评审 prompt 里不含提案 rationale", async () => {
|
|
86
|
+
const fs = memFs(baseFiles());
|
|
87
|
+
let captured = "";
|
|
88
|
+
const spawnFn = ((_cmd: string, args: string[]) => {
|
|
89
|
+
captured = args[3] ?? "";
|
|
90
|
+
return fakeReviewSpawn([PASS])(_cmd, args, {});
|
|
91
|
+
}) as unknown as Parameters<typeof runReviewSupervisor>[0]["spawnFn"];
|
|
92
|
+
await runReviewSupervisor({
|
|
93
|
+
runDir: RUN_DIR,
|
|
94
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
95
|
+
readText: fs.readText,
|
|
96
|
+
writeText: fs.writeText,
|
|
97
|
+
...(spawnFn ? { spawnFn } : {}),
|
|
98
|
+
pollMs: 1,
|
|
99
|
+
});
|
|
100
|
+
assert.ok(captured.length > 0, "应当真的组装并传了 prompt");
|
|
101
|
+
assert.ok(!captured.includes("理由不该进评审"), "提案 rationale 不得进评审 prompt");
|
|
102
|
+
assert.ok(!captured.includes("01-findings"));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("提案永不出现:900s 后写 skipped/proposal-timeout", async () => {
|
|
106
|
+
const fs = memFs({ [`${RUN_DIR}/run.json`]: RUN_JSON });
|
|
107
|
+
const clock = fakeClock();
|
|
108
|
+
const outcome = await runReviewSupervisor({
|
|
109
|
+
runDir: RUN_DIR,
|
|
110
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
111
|
+
readText: fs.readText,
|
|
112
|
+
writeText: fs.writeText,
|
|
113
|
+
now: clock.now,
|
|
114
|
+
sleep: clock.sleep,
|
|
115
|
+
pollMs: 2000,
|
|
116
|
+
});
|
|
117
|
+
assert.equal(outcome.status, "skipped");
|
|
118
|
+
const review = fs.json(`${RUN_DIR}/03-review.json`) as Record<string, unknown>;
|
|
119
|
+
assert.equal(review.status, "skipped");
|
|
120
|
+
assert.equal(review.reason, "proposal-timeout");
|
|
121
|
+
assert.ok(clock.elapsed() >= 900_000, `应当等满 900s,实际推进 ${clock.elapsed()}ms`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("提案文件是坏 JSON:skipped/proposal-unreadable", async () => {
|
|
125
|
+
const fs = memFs({ ...baseFiles(), [`${RUN_DIR}/02-proposal.json`]: "{坏掉的" });
|
|
126
|
+
const clock = fakeClock();
|
|
127
|
+
const outcome = await runReviewSupervisor({
|
|
128
|
+
runDir: RUN_DIR,
|
|
129
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
130
|
+
readText: fs.readText,
|
|
131
|
+
writeText: fs.writeText,
|
|
132
|
+
now: clock.now,
|
|
133
|
+
sleep: clock.sleep,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(outcome.status, "skipped");
|
|
136
|
+
assert.equal((fs.json(`${RUN_DIR}/03-review.json`) as Record<string, unknown>).reason, "proposal-unreadable");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("评审进程超时 300s → review_failed 且 kill 被调用", async () => {
|
|
140
|
+
const fs = memFs(baseFiles());
|
|
141
|
+
const clock = fakeClock();
|
|
142
|
+
const outcome = await runReviewSupervisor({
|
|
143
|
+
runDir: RUN_DIR,
|
|
144
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
145
|
+
readText: fs.readText,
|
|
146
|
+
writeText: fs.writeText,
|
|
147
|
+
spawnFn: hangingReviewSpawn(),
|
|
148
|
+
now: clock.now,
|
|
149
|
+
sleep: clock.sleep,
|
|
150
|
+
pollMs: 250,
|
|
151
|
+
});
|
|
152
|
+
assert.equal(outcome.status, "review_failed");
|
|
153
|
+
assert.match(outcome.record.detail ?? "", /review-timeout/);
|
|
154
|
+
assert.equal(outcome.record.attempts, 1, "超时不重试");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("invalid 一次后重评成功:attempts=2 且 reviewed", async () => {
|
|
158
|
+
const fs = memFs(baseFiles());
|
|
159
|
+
const outcome = await runReviewSupervisor({
|
|
160
|
+
runDir: RUN_DIR,
|
|
161
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
162
|
+
readText: fs.readText,
|
|
163
|
+
writeText: fs.writeText,
|
|
164
|
+
spawnFn: fakeReviewSpawn([JSON.stringify({ verdict: "maybe", blocking: [] }), PASS]),
|
|
165
|
+
pollMs: 1,
|
|
166
|
+
});
|
|
167
|
+
assert.equal(outcome.status, "reviewed");
|
|
168
|
+
assert.equal(outcome.record.attempts, 2);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("两次都 invalid → review_failed,run.json 也是 review_failed", async () => {
|
|
172
|
+
const fs = memFs(baseFiles());
|
|
173
|
+
const bad = JSON.stringify({ verdict: "maybe", blocking: [] });
|
|
174
|
+
const outcome = await runReviewSupervisor({
|
|
175
|
+
runDir: RUN_DIR,
|
|
176
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
177
|
+
readText: fs.readText,
|
|
178
|
+
writeText: fs.writeText,
|
|
179
|
+
spawnFn: fakeReviewSpawn([bad, bad]),
|
|
180
|
+
pollMs: 1,
|
|
181
|
+
});
|
|
182
|
+
assert.equal(outcome.status, "review_failed");
|
|
183
|
+
assert.equal(outcome.record.attempts, 2);
|
|
184
|
+
assert.match(outcome.record.detail ?? "", /bad-verdict/);
|
|
185
|
+
assert.equal((fs.json(`${RUN_DIR}/run.json`) as Record<string, unknown>).status, "review_failed");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("机械复验:diff 新增 P0 命中时一票 reject(即使评审说 pass)", async () => {
|
|
189
|
+
const badDiff = [
|
|
190
|
+
"--- a/.pi/skills/demo/SKILL.md",
|
|
191
|
+
"+++ b/.pi/skills/demo/SKILL.md",
|
|
192
|
+
"@@ -6,1 +6,1 @@",
|
|
193
|
+
"-检索一律用 rg,禁止 find。",
|
|
194
|
+
"+找不到就 find . -name 文件名 全盘扫一遍。",
|
|
195
|
+
].join("\n");
|
|
196
|
+
const fs = memFs({ ...baseFiles(), [`${RUN_DIR}/02-proposal.diff`]: badDiff });
|
|
197
|
+
const outcome = await runReviewSupervisor({
|
|
198
|
+
runDir: RUN_DIR,
|
|
199
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
200
|
+
rules: RULES,
|
|
201
|
+
readText: fs.readText,
|
|
202
|
+
writeText: fs.writeText,
|
|
203
|
+
spawnFn: fakeReviewSpawn([PASS]),
|
|
204
|
+
pollMs: 1,
|
|
205
|
+
});
|
|
206
|
+
assert.equal(outcome.status, "rejected");
|
|
207
|
+
assert.equal(outcome.record.verdict, "reject");
|
|
208
|
+
assert.match(outcome.record.detail ?? "", /frontmatter-invalid: 新增 P0 命中/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("create 提案:candidate frontmatter 缺 description → reject", async () => {
|
|
212
|
+
const candidate = ["---", "name: demo", "---", "正文"].join("\n");
|
|
213
|
+
const fs = memFs({
|
|
214
|
+
[`${RUN_DIR}/run.json`]: RUN_JSON,
|
|
215
|
+
[`${RUN_DIR}/02-proposal.json`]: JSON.stringify({ type: "create", skill: "demo", candidateFile: "candidate/demo/SKILL.md" }),
|
|
216
|
+
[`${RUN_DIR}/candidate/demo/SKILL.md`]: candidate,
|
|
217
|
+
});
|
|
218
|
+
const outcome = await runReviewSupervisor({
|
|
219
|
+
runDir: RUN_DIR,
|
|
220
|
+
evolutionDir: "/repo/.pi/evolution",
|
|
221
|
+
rules: RULES,
|
|
222
|
+
readText: fs.readText,
|
|
223
|
+
writeText: fs.writeText,
|
|
224
|
+
spawnFn: fakeReviewSpawn([PASS]),
|
|
225
|
+
pollMs: 1,
|
|
226
|
+
});
|
|
227
|
+
assert.equal(outcome.status, "rejected");
|
|
228
|
+
assert.match(outcome.record.detail ?? "", /frontmatter 缺 description/);
|
|
229
|
+
assert.equal(outcome.record.action, "approval");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("parseProposal 只认真实字段,缺 type/skill 返回 undefined", () => {
|
|
233
|
+
assert.equal(parseProposal(undefined), undefined);
|
|
234
|
+
assert.equal(parseProposal("{}"), undefined);
|
|
235
|
+
assert.equal(parseProposal(JSON.stringify({ type: "improve", skill: "demo" }))?.skill, "demo");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("checkCandidateFrontmatter 三种坏法都能识别", () => {
|
|
239
|
+
assert.equal(checkCandidateFrontmatter("没有 frontmatter"), "缺 frontmatter");
|
|
240
|
+
assert.equal(checkCandidateFrontmatter("---\ndescription: x\n---\n"), "frontmatter 缺 name");
|
|
241
|
+
assert.equal(checkCandidateFrontmatter("---\nname: x\n---\n"), "frontmatter 缺 description");
|
|
242
|
+
assert.equal(checkCandidateFrontmatter("---\nname: x\ndescription: y\n---\n"), null);
|
|
243
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 必红自检单测(设计稿 12.4):三个坏样本必须被拦住,任何一条被 pass 就算评审器不可信。
|
|
3
|
+
*
|
|
4
|
+
* 假 spawn 按 prompt 里的「技能名」回不同结论,所以用例不受 fixtures 目录枚举顺序影响。
|
|
5
|
+
*/
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import type { ReviewSpawn } from "../lib/review-run.ts";
|
|
13
|
+
import { loadFixtures, renderSelftestReport, runSelftest, selftestFile, UNTRUSTED_NOTE } from "../lib/selftest.ts";
|
|
14
|
+
import { FakeReviewChild } from "./fake-review-spawn.ts";
|
|
15
|
+
|
|
16
|
+
const FIXTURES = fileURLToPath(new URL("../fixtures/selftest", import.meta.url));
|
|
17
|
+
|
|
18
|
+
function tmpDir(): string {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "sk-selftest-"));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 按 prompt 末尾的「技能名:x」挑结论 */
|
|
23
|
+
function spawnBySkill(verdicts: Record<string, string>): ReviewSpawn {
|
|
24
|
+
return (_command, args) => {
|
|
25
|
+
const prompt = args[3] ?? "";
|
|
26
|
+
const skill = Object.keys(verdicts).find((name) => prompt.includes(`技能名:${name}`));
|
|
27
|
+
const reply = JSON.stringify({ verdict: skill ? verdicts[skill] : "pass", blocking: [], notes: "自检" });
|
|
28
|
+
const child = new FakeReviewChild();
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
child.stdout.emit("data", reply);
|
|
31
|
+
child.emit("exit", 0);
|
|
32
|
+
}, 1);
|
|
33
|
+
return child;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("三个坏样本都能被读到,期望分别是 reject/reject/revise", () => {
|
|
38
|
+
const fixtures = loadFixtures(FIXTURES);
|
|
39
|
+
assert.deepEqual(fixtures.map((item) => item.name).sort(), ["delete-ban", "rg-to-find", "weak-description"]);
|
|
40
|
+
const expected = Object.fromEntries(fixtures.map((item) => [item.name, item.expected]));
|
|
41
|
+
assert.deepEqual(expected, { "delete-ban": "reject", "rg-to-find": "reject", "weak-description": "revise" });
|
|
42
|
+
for (const fixture of fixtures) {
|
|
43
|
+
assert.ok(fixture.diff.length > 0, `${fixture.name} 缺 diff`);
|
|
44
|
+
assert.ok(fixture.after.length > 0, `${fixture.name} 缺改后全文`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("三样本全部被正确拦住 → passed:true 并落盘 selftest.json", async () => {
|
|
49
|
+
const dir = tmpDir();
|
|
50
|
+
const report = await runSelftest({
|
|
51
|
+
evolutionDir: dir,
|
|
52
|
+
cwd: dir,
|
|
53
|
+
fixturesDir: FIXTURES,
|
|
54
|
+
spawnFn: spawnBySkill({ "ui-verify-lite": "reject", "rg-retrieval": "reject", "misc-helper": "revise" }),
|
|
55
|
+
pollMs: 1,
|
|
56
|
+
});
|
|
57
|
+
assert.equal(report.passed, true);
|
|
58
|
+
assert.equal(report.results.length, 3);
|
|
59
|
+
assert.deepEqual(report.results.map((item) => item.actual), ["reject", "reject", "revise"]);
|
|
60
|
+
const saved = JSON.parse(fs.readFileSync(selftestFile(dir), "utf8")) as { passed: boolean };
|
|
61
|
+
assert.equal(saved.passed, true);
|
|
62
|
+
assert.ok(!renderSelftestReport(report).includes("【红】"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("有一个被 pass → passed:false,报告首行是红字", async () => {
|
|
66
|
+
const dir = tmpDir();
|
|
67
|
+
const report = await runSelftest({
|
|
68
|
+
evolutionDir: dir,
|
|
69
|
+
cwd: dir,
|
|
70
|
+
fixturesDir: FIXTURES,
|
|
71
|
+
spawnFn: spawnBySkill({ "ui-verify-lite": "reject", "rg-retrieval": "reject", "misc-helper": "pass" }),
|
|
72
|
+
pollMs: 1,
|
|
73
|
+
});
|
|
74
|
+
assert.equal(report.passed, false);
|
|
75
|
+
const text = renderSelftestReport(report);
|
|
76
|
+
assert.ok(text.startsWith(`【红】${UNTRUSTED_NOTE}`), `红字必须在首行,实际:${text.slice(0, 40)}`);
|
|
77
|
+
assert.match(text, /✗ weak-description/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("单个样本就 pass(其余 reject)也判不可信", async () => {
|
|
81
|
+
const dir = tmpDir();
|
|
82
|
+
const report = await runSelftest({
|
|
83
|
+
evolutionDir: dir,
|
|
84
|
+
cwd: dir,
|
|
85
|
+
fixturesDir: FIXTURES,
|
|
86
|
+
spawnFn: spawnBySkill({ "ui-verify-lite": "pass", "rg-retrieval": "reject", "misc-helper": "revise" }),
|
|
87
|
+
pollMs: 1,
|
|
88
|
+
});
|
|
89
|
+
assert.equal(report.passed, false);
|
|
90
|
+
assert.equal(report.results.filter((item) => item.ok).length, 2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("fixture 缺 change.diff 被跳过,不猜也不补", () => {
|
|
94
|
+
const dir = tmpDir();
|
|
95
|
+
fs.mkdirSync(path.join(dir, "broken"), { recursive: true });
|
|
96
|
+
fs.writeFileSync(path.join(dir, "broken", "fixture.json"), JSON.stringify({ name: "broken", skill: "x", expected: "reject" }));
|
|
97
|
+
fs.writeFileSync(path.join(dir, "broken", "after.md"), "正文");
|
|
98
|
+
const logs: string[] = [];
|
|
99
|
+
const fixtures = loadFixtures(dir, (msg) => logs.push(msg));
|
|
100
|
+
assert.deepEqual(fixtures, []);
|
|
101
|
+
assert.match(logs.join("\n"), /缺 change\.diff/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("expected 非法同样跳过", () => {
|
|
105
|
+
const dir = tmpDir();
|
|
106
|
+
fs.mkdirSync(path.join(dir, "bad"), { recursive: true });
|
|
107
|
+
fs.writeFileSync(path.join(dir, "bad", "fixture.json"), JSON.stringify({ name: "bad", skill: "x", expected: "maybe" }));
|
|
108
|
+
fs.writeFileSync(path.join(dir, "bad", "change.diff"), "+++ b/x\n");
|
|
109
|
+
fs.writeFileSync(path.join(dir, "bad", "after.md"), "正文");
|
|
110
|
+
const logs: string[] = [];
|
|
111
|
+
assert.deepEqual(loadFixtures(dir, (msg) => logs.push(msg)), []);
|
|
112
|
+
assert.match(logs.join("\n"), /expected 非法/);
|
|
113
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话索引单测(零依赖,node --test)。
|
|
3
|
+
*
|
|
4
|
+
* 运行:
|
|
5
|
+
* cd .pi/extensions/skill-evolve
|
|
6
|
+
* node --experimental-strip-types --test test/session-index.test.ts
|
|
7
|
+
*
|
|
8
|
+
* 覆盖:行号 1 起算、compaction 标记、坏行只计数不崩、摘要 ≤80 字、
|
|
9
|
+
* 以及「索引里不出现会话原文」这条铁律(设计稿 5.4)。
|
|
10
|
+
*/
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
|
|
17
|
+
import { buildSessionIndex, excerptOf, writeSessionIndex } from "../lib/session-index.ts";
|
|
18
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
19
|
+
|
|
20
|
+
const LONG_TEXT = "这是一段用来撑长度的用户原话。".repeat(20);
|
|
21
|
+
|
|
22
|
+
/** 贴近真实会话的一小段 JSONL:头部 + 用户 + 助手带工具调用 + 工具结果 + compaction */
|
|
23
|
+
const SESSION_JSONL = [
|
|
24
|
+
JSON.stringify({ type: "session", version: 3, id: "sess-1", timestamp: "2026-11-08T02:48:52.675Z", cwd: "/repo" }),
|
|
25
|
+
JSON.stringify({ type: "model_change", id: "e0", timestamp: "2026-11-08T02:48:53.000Z" }),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
type: "message",
|
|
28
|
+
id: "e1",
|
|
29
|
+
parentId: "e0",
|
|
30
|
+
timestamp: "2026-11-08T02:49:00.000Z",
|
|
31
|
+
message: { role: "user", content: [{ type: "text", text: "帮我把需求 R-20261108 推进到开发中" }] },
|
|
32
|
+
}),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
type: "message",
|
|
35
|
+
id: "e2",
|
|
36
|
+
parentId: "e1",
|
|
37
|
+
timestamp: "2026-11-08T02:49:05.000Z",
|
|
38
|
+
message: {
|
|
39
|
+
role: "assistant",
|
|
40
|
+
content: [
|
|
41
|
+
{ type: "text", text: "先看流程" },
|
|
42
|
+
{ type: "toolCall", id: "call-1", name: "read", arguments: { path: ".pi/skills/kb-test-map/SKILL.md" } },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
type: "message",
|
|
48
|
+
id: "e3",
|
|
49
|
+
parentId: "e2",
|
|
50
|
+
timestamp: "2026-11-08T02:49:06.000Z",
|
|
51
|
+
message: { role: "toolResult", toolCallId: "call-1", toolName: "read", isError: false, content: [{ type: "text", text: "# kb-test-map" }] },
|
|
52
|
+
}),
|
|
53
|
+
JSON.stringify({ type: "compaction", id: "e4", timestamp: "2026-11-08T02:50:00.000Z", summary: "先前讨论了需求推进规则", firstKeptEntryId: "e2", tokensBefore: 42000 }),
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
test("buildSessionIndex:行号 1 起算、类型与角色正确", () => {
|
|
58
|
+
const index = buildSessionIndex(SESSION_JSONL);
|
|
59
|
+
assert.equal(index.stats.lines, 6);
|
|
60
|
+
assert.equal(index.stats.bytes, Buffer.byteLength(SESSION_JSONL, "utf8"));
|
|
61
|
+
assert.equal(index.lines[0].line, 1);
|
|
62
|
+
assert.equal(index.lines[0].type, "session");
|
|
63
|
+
assert.equal(index.lines[0].excerpt, "/repo");
|
|
64
|
+
assert.equal(index.lines[2].line, 3);
|
|
65
|
+
assert.equal(index.lines[2].role, "user");
|
|
66
|
+
assert.equal(index.lines[2].excerpt.includes("推进到开发中"), true);
|
|
67
|
+
assert.equal(index.lines[2].ts, "2026-11-08T02:49:00.000Z");
|
|
68
|
+
// 助手消息里的工具调用占位进摘要,方便复盘按行号回读
|
|
69
|
+
assert.equal(index.lines[3].excerpt.includes("[调用 read]"), true);
|
|
70
|
+
// 工具结果摘要前缀工具名
|
|
71
|
+
assert.equal(index.lines[4].excerpt.startsWith("[read]"), true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("buildSessionIndex:compaction 被标记且摘要进索引", () => {
|
|
75
|
+
const index = buildSessionIndex(SESSION_JSONL);
|
|
76
|
+
assert.equal(index.stats.hasCompaction, true);
|
|
77
|
+
const compaction = index.lines.find((line) => line.type === "compaction");
|
|
78
|
+
assert.equal(compaction?.line, 6);
|
|
79
|
+
assert.equal(compaction?.excerpt, "先前讨论了需求推进规则");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("buildSessionIndex:坏行只计数不进索引,不整份失败", () => {
|
|
83
|
+
const index = buildSessionIndex(`${SESSION_JSONL}\n{"type":"message","id":broken\n`);
|
|
84
|
+
assert.equal(index.stats.lines, 7);
|
|
85
|
+
assert.equal(index.lines.length, 6);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("buildSessionIndex:空行不计入 lines 统计", () => {
|
|
89
|
+
const index = buildSessionIndex("\n\n\n");
|
|
90
|
+
assert.equal(index.stats.lines, 0);
|
|
91
|
+
assert.deepEqual(index.lines, []);
|
|
92
|
+
assert.equal(index.stats.hasCompaction, false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("excerptOf:折叠空白并截到 80 字", () => {
|
|
96
|
+
assert.equal(excerptOf("a\n\n b\tc"), "a b c");
|
|
97
|
+
const short = excerptOf(LONG_TEXT);
|
|
98
|
+
assert.equal(short.length, 80);
|
|
99
|
+
assert.equal(short.endsWith("…"), true);
|
|
100
|
+
assert.equal(excerptOf("x".repeat(80)).length, 80);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("索引里不出现会话原文(长文本被截断,原文不入库)", () => {
|
|
104
|
+
const long = JSON.stringify({ type: "message", id: "x", message: { role: "user", content: [{ type: "text", text: LONG_TEXT }] } });
|
|
105
|
+
const index = buildSessionIndex(long);
|
|
106
|
+
assert.equal(index.lines.length, 1);
|
|
107
|
+
assert.equal(index.lines[0].chars, long.length);
|
|
108
|
+
assert.equal(JSON.stringify(index).includes(LONG_TEXT), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("writeSessionIndex:生成 index.json 并从原文取字节数", async () => {
|
|
112
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-index-"));
|
|
113
|
+
const sessionFile = path.join(dir, "session.jsonl");
|
|
114
|
+
fs.writeFileSync(sessionFile, SESSION_JSONL, "utf8");
|
|
115
|
+
const outDir = path.join(dir, "00-session");
|
|
116
|
+
const index = await writeSessionIndex({ sessionFile, outDir, queue: directQueue });
|
|
117
|
+
assert.equal(index?.stats.lines, 6);
|
|
118
|
+
const written = JSON.parse(fs.readFileSync(path.join(outDir, "index.json"), "utf8"));
|
|
119
|
+
assert.equal(written.stats.hasCompaction, true);
|
|
120
|
+
assert.equal(written.lines.length, 6);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("writeSessionIndex:原文读不到时返回 null(调用方降级 telemetry-only)", async () => {
|
|
124
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-index-"));
|
|
125
|
+
const index = await writeSessionIndex({ sessionFile: path.join(dir, "不存在.jsonl"), outDir: path.join(dir, "00-session"), queue: directQueue, log: () => {} });
|
|
126
|
+
assert.equal(index, null);
|
|
127
|
+
assert.equal(fs.existsSync(path.join(dir, "00-session", "index.json")), false);
|
|
128
|
+
});
|