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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 采纳队列单测:幂等、superseded 去重、并发入队不丢条目、坏文件容错。
|
|
3
|
+
*
|
|
4
|
+
* 用临时目录 + directQueue 跑真实 IO:这段逻辑的坑全在「文件里到底写了什么」。
|
|
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 { enqueue, findItem, markDone, queueFile, readQueue, takePending } from "../lib/queue.ts";
|
|
12
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
13
|
+
|
|
14
|
+
const NOW = 1_760_000_000_000;
|
|
15
|
+
|
|
16
|
+
function tempEvolutionDir(): string {
|
|
17
|
+
return path.join(fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-queue-")), ".pi", "evolution");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deps(dir: string, at = NOW) {
|
|
21
|
+
return { evolutionDir: dir, queue: directQueue, now: () => at };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function input(runId: string, over: Partial<Parameters<typeof enqueue>[1]> = {}) {
|
|
25
|
+
return { runId, level: "approval" as const, skill: "deploy-test", targets: [".pi/skills/deploy-test/SKILL.md"], ...over };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("首次入队是 pending,createdAt 用注入时间", async () => {
|
|
29
|
+
const dir = tempEvolutionDir();
|
|
30
|
+
const result = await enqueue(deps(dir), input("run-1"));
|
|
31
|
+
assert.equal(result.item.status, "pending");
|
|
32
|
+
assert.equal(result.item.createdAt, NOW);
|
|
33
|
+
assert.equal(result.supersededBy, undefined);
|
|
34
|
+
assert.equal(readQueue(dir).length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("同一个 runId 重复入队幂等:条数不变、不刷 createdAt", async () => {
|
|
38
|
+
const dir = tempEvolutionDir();
|
|
39
|
+
await enqueue(deps(dir), input("run-1"));
|
|
40
|
+
const again = await enqueue(deps(dir, NOW + 5000), input("run-1"));
|
|
41
|
+
assert.equal(readQueue(dir).length, 1);
|
|
42
|
+
assert.equal(again.item.createdAt, NOW);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("同一技能已有 pending 时,新条目直接 superseded 并指出被谁顶掉", async () => {
|
|
46
|
+
const dir = tempEvolutionDir();
|
|
47
|
+
await enqueue(deps(dir), input("run-1"));
|
|
48
|
+
const second = await enqueue(deps(dir, NOW + 1000), input("run-2"));
|
|
49
|
+
assert.equal(second.item.status, "superseded");
|
|
50
|
+
assert.equal(second.supersededBy, "run-1");
|
|
51
|
+
assert.match(second.item.detail ?? "", /run-1/);
|
|
52
|
+
// 前一条仍是 pending,takePending 只给它
|
|
53
|
+
assert.deepEqual(takePending(readQueue(dir)).map((item) => item.runId), ["run-1"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("同一技能的前一条已 applied 后,新条目可以再成为 pending", async () => {
|
|
57
|
+
const dir = tempEvolutionDir();
|
|
58
|
+
await enqueue(deps(dir), input("run-1"));
|
|
59
|
+
await markDone(deps(dir), "run-1", "applied");
|
|
60
|
+
const second = await enqueue(deps(dir, NOW + 1000), input("run-2"));
|
|
61
|
+
assert.equal(second.item.status, "pending");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("不同技能互不影响,takePending 按入队时间升序且可按级别过滤", async () => {
|
|
65
|
+
const dir = tempEvolutionDir();
|
|
66
|
+
await enqueue(deps(dir, NOW + 2000), input("run-late", { skill: "hard-rules" }));
|
|
67
|
+
await enqueue(deps(dir, NOW), input("run-early", { skill: "deploy-test", level: "auto" }));
|
|
68
|
+
assert.deepEqual(takePending(readQueue(dir)).map((item) => item.runId), ["run-early", "run-late"]);
|
|
69
|
+
assert.deepEqual(takePending(readQueue(dir), "auto").map((item) => item.runId), ["run-early"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("并发入队不丢条目:两个 runId 同时写,队列里两条都在", async () => {
|
|
73
|
+
const dir = tempEvolutionDir();
|
|
74
|
+
await Promise.all([
|
|
75
|
+
enqueue(deps(dir), input("run-a", { skill: "skill-a" })),
|
|
76
|
+
enqueue(deps(dir), input("run-b", { skill: "skill-b" })),
|
|
77
|
+
]);
|
|
78
|
+
assert.deepEqual(readQueue(dir).map((item) => item.runId).sort(), ["run-a", "run-b"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("markDone 改状态并写 detail;找不到 runId 时返回 false 且不改文件", async () => {
|
|
82
|
+
const dir = tempEvolutionDir();
|
|
83
|
+
await enqueue(deps(dir), input("run-1"));
|
|
84
|
+
assert.equal(await markDone(deps(dir), "run-1", "applied", "五道闸全绿"), true);
|
|
85
|
+
const item = findItem(readQueue(dir), "run-1");
|
|
86
|
+
assert.equal(item?.status, "applied");
|
|
87
|
+
assert.equal(item?.detail, "五道闸全绿");
|
|
88
|
+
assert.equal(await markDone(deps(dir), "run-404", "failed"), false);
|
|
89
|
+
assert.equal(readQueue(dir).length, 1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("损坏文件容错:坏 JSON 当空队列,单条非法字段只丢那一条", async () => {
|
|
93
|
+
const dir = tempEvolutionDir();
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
fs.writeFileSync(queueFile(dir), "{不是 JSON", "utf8");
|
|
96
|
+
assert.deepEqual(readQueue(dir), []);
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
queueFile(dir),
|
|
99
|
+
JSON.stringify([
|
|
100
|
+
{ runId: "ok", level: "auto", skill: "s", targets: [], createdAt: NOW, status: "pending" },
|
|
101
|
+
{ runId: "bad", level: "whatever", skill: "s", targets: [], createdAt: NOW, status: "pending" },
|
|
102
|
+
]),
|
|
103
|
+
"utf8",
|
|
104
|
+
);
|
|
105
|
+
assert.deepEqual(readQueue(dir).map((item) => item.runId), ["ok"]);
|
|
106
|
+
// 坏文件之后仍能继续入队(不会被坏内容卡死)
|
|
107
|
+
const result = await enqueue(deps(dir), input("run-new", { skill: "other" }));
|
|
108
|
+
assert.equal(result.item.status, "pending");
|
|
109
|
+
assert.deepEqual(readQueue(dir).map((item) => item.runId).sort(), ["ok", "run-new"]);
|
|
110
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 拒绝胶水单测:Web 面板「拒绝」只改状态,不碰技能文件。
|
|
3
|
+
*
|
|
4
|
+
* 走的是 index.ts 注入的同一条路径(createApplyTools(...).reject),
|
|
5
|
+
* 全部临时目录 + 注入的 git/md-check 假实现:不起任何真实子进程,也不碰仓库里的 `.pi/skills`。
|
|
6
|
+
*/
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { createApplyTools } from "../lib/evolution-apply.ts";
|
|
13
|
+
import { gate0Path, readGate0, saveGate0 } from "../lib/gate0.ts";
|
|
14
|
+
import { readQueue } from "../lib/queue.ts";
|
|
15
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
16
|
+
|
|
17
|
+
const NOW = 1_760_000_000_000;
|
|
18
|
+
const SKILL_REL = ".pi/skills/demo/SKILL.md";
|
|
19
|
+
const SKILL_TEXT = "---\nname: demo\ndescription: 演示技能\n---\n\n## 步骤\n1. 先读文档\n";
|
|
20
|
+
/** 正文改动 → approval 级:正是需要人批的那种提案 */
|
|
21
|
+
const BODY_DIFF = `--- a/${SKILL_REL}\n+++ b/${SKILL_REL}\n@@ -7,1 +7,1 @@\n-1. 先读文档\n+1. 先读文档,再确认\n`;
|
|
22
|
+
|
|
23
|
+
interface Fixture {
|
|
24
|
+
cwd: string;
|
|
25
|
+
evolutionDir: string;
|
|
26
|
+
skillAbs: string;
|
|
27
|
+
runDir: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setup(runId = "run-body"): Fixture {
|
|
31
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-reject-"));
|
|
32
|
+
const skillAbs = path.join(cwd, SKILL_REL);
|
|
33
|
+
fs.mkdirSync(path.dirname(skillAbs), { recursive: true });
|
|
34
|
+
fs.writeFileSync(skillAbs, SKILL_TEXT, "utf8");
|
|
35
|
+
const evolutionDir = path.join(cwd, ".pi", "evolution");
|
|
36
|
+
const runDir = path.join(evolutionDir, "runs", runId);
|
|
37
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(runDir, "run.json"), JSON.stringify({ runId, status: "reviewed", dispatchedAt: NOW }), "utf8");
|
|
39
|
+
fs.writeFileSync(path.join(runDir, "02-proposal.json"), JSON.stringify({ type: "improve", skill: "demo" }), "utf8");
|
|
40
|
+
fs.writeFileSync(path.join(runDir, "02-proposal.diff"), BODY_DIFF, "utf8");
|
|
41
|
+
return { cwd, evolutionDir, skillAbs, runDir };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tools(fixture: Fixture) {
|
|
45
|
+
return createApplyTools({
|
|
46
|
+
evolutionDir: fixture.evolutionDir,
|
|
47
|
+
cwd: fixture.cwd,
|
|
48
|
+
queue: directQueue,
|
|
49
|
+
now: () => NOW,
|
|
50
|
+
log: () => {},
|
|
51
|
+
git: () => ({ code: 0, out: "" }),
|
|
52
|
+
loadDiagnostics: () => [],
|
|
53
|
+
runMdCheck: () => ({ code: 0, output: "" }),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function seedQueue(fixture: Fixture): Promise<void> {
|
|
58
|
+
await saveGate0(fixture.evolutionDir, { at: NOW, status: "", diff: "" }, directQueue);
|
|
59
|
+
const added = await tools(fixture).enqueueReady();
|
|
60
|
+
assert.equal(added, 1, "前置条件:正文改动应入队成一条 approval");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runStatus(fixture: Fixture): string {
|
|
64
|
+
return JSON.parse(fs.readFileSync(path.join(fixture.runDir, "run.json"), "utf8")).status as string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test("reject:队列条目转 rejected,run.json 转 rejected,技能文件一字未动", async () => {
|
|
68
|
+
const fixture = setup();
|
|
69
|
+
await seedQueue(fixture);
|
|
70
|
+
assert.equal(readQueue(fixture.evolutionDir)[0]?.level, "approval");
|
|
71
|
+
|
|
72
|
+
const text = await tools(fixture).reject("run-body");
|
|
73
|
+
assert.match(text, /已拒绝 run-body/);
|
|
74
|
+
assert.match(text, /技能文件未改动/);
|
|
75
|
+
|
|
76
|
+
const item = readQueue(fixture.evolutionDir)[0];
|
|
77
|
+
assert.equal(item?.status, "rejected");
|
|
78
|
+
assert.match(item?.detail ?? "", /人主动拒绝/);
|
|
79
|
+
assert.equal(runStatus(fixture), "rejected");
|
|
80
|
+
// 拒绝的核心承诺:技能文件一个字节都不变
|
|
81
|
+
assert.equal(fs.readFileSync(fixture.skillAbs, "utf8"), SKILL_TEXT);
|
|
82
|
+
// 闸0 快照也不该被拒绝动作刷新
|
|
83
|
+
assert.equal((await readGate0(fixture.evolutionDir))?.at, NOW);
|
|
84
|
+
assert.equal(fs.existsSync(gate0Path(fixture.evolutionDir)), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("reject:队列里没有这个 runId → 回人话,不动任何状态", async () => {
|
|
88
|
+
const fixture = setup();
|
|
89
|
+
await seedQueue(fixture);
|
|
90
|
+
const text = await tools(fixture).reject("run-nope");
|
|
91
|
+
assert.match(text, /队列里没有 run-nope/);
|
|
92
|
+
assert.equal(readQueue(fixture.evolutionDir)[0]?.status, "pending");
|
|
93
|
+
assert.equal(runStatus(fixture), "reviewed");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("reject:空 runId → 只回用法,不猜目标", async () => {
|
|
97
|
+
const fixture = setup();
|
|
98
|
+
await seedQueue(fixture);
|
|
99
|
+
const text = await tools(fixture).reject("");
|
|
100
|
+
assert.match(text, /用法/);
|
|
101
|
+
assert.equal(readQueue(fixture.evolutionDir)[0]?.status, "pending");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("reject:已 applied 的条目不能再拒绝(幂等不靠猜)", async () => {
|
|
105
|
+
const fixture = setup();
|
|
106
|
+
await seedQueue(fixture);
|
|
107
|
+
const queueFile = path.join(fixture.evolutionDir, "queue.json");
|
|
108
|
+
const items = JSON.parse(fs.readFileSync(queueFile, "utf8")) as Record<string, unknown>[];
|
|
109
|
+
items[0]!.status = "applied";
|
|
110
|
+
fs.writeFileSync(queueFile, JSON.stringify(items), "utf8");
|
|
111
|
+
|
|
112
|
+
const text = await tools(fixture).reject("run-body");
|
|
113
|
+
assert.match(text, /当前状态是 applied,只有 pending 能拒绝/);
|
|
114
|
+
assert.equal(runStatus(fixture), "reviewed", "run.json 保持评审通过,不被一次无效拒绝改写");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("reject:run.json 丢了也能拒(队列先转 rejected,不留下永远 pending 的条目)", async () => {
|
|
118
|
+
const fixture = setup();
|
|
119
|
+
await seedQueue(fixture);
|
|
120
|
+
fs.rmSync(path.join(fixture.runDir, "run.json"));
|
|
121
|
+
|
|
122
|
+
const text = await tools(fixture).reject("run-body");
|
|
123
|
+
assert.match(text, /只改了队列状态/);
|
|
124
|
+
assert.equal(readQueue(fixture.evolutionDir)[0]?.status, "rejected");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("queueText:拒绝后条目归到 rejected 组,不再算待批", async () => {
|
|
128
|
+
const fixture = setup();
|
|
129
|
+
await seedQueue(fixture);
|
|
130
|
+
await tools(fixture).reject("run-body");
|
|
131
|
+
const text = await tools(fixture).queueText();
|
|
132
|
+
assert.match(text, /rejected 1 条/);
|
|
133
|
+
assert.equal(/pending 1 条/.test(text), false);
|
|
134
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M6 端到端复演:六段状态齐全、产物落盘、评审不通过时停在评审段。
|
|
3
|
+
* 用例编号对应设计稿 15 章(M6 验收硬指标:整轮可复演)。
|
|
4
|
+
* 这里一律用假评审 + 假 git:测试里绝不起真子进程(真跑用 scripts/replay.ts --real)。
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import { FAKE_REVIEW_TEXT } from "../lib/replay-fixture.ts";
|
|
12
|
+
import { REPLAY_STAGES, runReplay } from "../lib/replay.ts";
|
|
13
|
+
|
|
14
|
+
function tmpDir(): string {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-replay-test-"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fakeGit = () => ({ code: 0, out: "" });
|
|
19
|
+
|
|
20
|
+
test("复演:六段全通,run.json status 序列完整", async () => {
|
|
21
|
+
const dir = tmpDir();
|
|
22
|
+
const outcome = await runReplay({
|
|
23
|
+
cwd: dir,
|
|
24
|
+
evolutionDir: path.join(dir, ".pi", "evolution"),
|
|
25
|
+
fakeReview: true,
|
|
26
|
+
git: fakeGit,
|
|
27
|
+
now: () => Date.UTC(2026, 10, 8, 12),
|
|
28
|
+
});
|
|
29
|
+
assert.deepEqual(outcome.failures, []);
|
|
30
|
+
assert.equal(outcome.ok, true);
|
|
31
|
+
for (const stage of REPLAY_STAGES) {
|
|
32
|
+
assert.ok(!outcome.stages[stage].startsWith("未执行"), `${stage} 段必须真的执行过`);
|
|
33
|
+
assert.ok(!outcome.stages[stage].startsWith("失败"), `${stage} 段不应失败:${outcome.stages[stage]}`);
|
|
34
|
+
}
|
|
35
|
+
assert.deepEqual(outcome.statuses, ["dispatched", "analyzed", "proposed", "reviewed", "applied"]);
|
|
36
|
+
assert.equal(outcome.lines.length, 6);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("复演:产物齐全(00-session / 提案 / 评审 / 决策 / 采纳 diff / 复验)", async () => {
|
|
40
|
+
const dir = tmpDir();
|
|
41
|
+
const outcome = await runReplay({
|
|
42
|
+
cwd: dir,
|
|
43
|
+
evolutionDir: path.join(dir, ".pi", "evolution"),
|
|
44
|
+
fakeReview: true,
|
|
45
|
+
git: fakeGit,
|
|
46
|
+
now: () => Date.UTC(2026, 10, 8, 12),
|
|
47
|
+
});
|
|
48
|
+
const runDir = path.join(dir, ".pi", "evolution", "runs", outcome.runId);
|
|
49
|
+
for (const file of [
|
|
50
|
+
"run.json",
|
|
51
|
+
"00-session/index.json",
|
|
52
|
+
"02-proposal.json",
|
|
53
|
+
"02-proposal.diff",
|
|
54
|
+
"03-review.json",
|
|
55
|
+
"04-decision.json",
|
|
56
|
+
"05-applied.diff",
|
|
57
|
+
"06-verify.json",
|
|
58
|
+
]) {
|
|
59
|
+
assert.ok(fs.existsSync(path.join(runDir, file)), `${file} 必须存在`);
|
|
60
|
+
}
|
|
61
|
+
// 05 是真实落盘的 diff,06 里五道闸全 ok
|
|
62
|
+
assert.ok(fs.readFileSync(path.join(runDir, "05-applied.diff"), "utf8").includes("+"));
|
|
63
|
+
const verify = JSON.parse(fs.readFileSync(path.join(runDir, "06-verify.json"), "utf8")) as {
|
|
64
|
+
status: string;
|
|
65
|
+
gates: { gate: string; ok: boolean }[];
|
|
66
|
+
};
|
|
67
|
+
assert.equal(verify.status, "applied");
|
|
68
|
+
assert.equal(verify.gates.length, 5);
|
|
69
|
+
assert.ok(verify.gates.every((gate) => gate.ok));
|
|
70
|
+
// 技能文件真的被改了(复演的本质:碰了技能文件)
|
|
71
|
+
const skillText = fs.readFileSync(path.join(dir, ".pi", "skills", "replay-demo", "SKILL.md"), "utf8");
|
|
72
|
+
assert.match(skillText, /index\.json 再读会话原文/);
|
|
73
|
+
// 会话索引不含原文正文:只留 ≤80 字摘录
|
|
74
|
+
const index = JSON.parse(fs.readFileSync(path.join(runDir, "00-session", "index.json"), "utf8")) as {
|
|
75
|
+
lines: { excerpt: string }[];
|
|
76
|
+
stats: { lines: number; hasCompaction: boolean };
|
|
77
|
+
};
|
|
78
|
+
assert.ok(index.lines.length > 0);
|
|
79
|
+
assert.ok(index.lines.every((line) => line.excerpt.length <= 80));
|
|
80
|
+
assert.equal(index.stats.hasCompaction, false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("复演:评审 reject 时停在评审段,不改技能文件", async () => {
|
|
84
|
+
const dir = tmpDir();
|
|
85
|
+
const reject = JSON.stringify({
|
|
86
|
+
verdict: "reject",
|
|
87
|
+
blocking: [{ file: ".pi/skills/replay-demo/SKILL.md", line: 9, quote: "+- 复演时必须先看…", why: "新增条款与硬约束冲突" }],
|
|
88
|
+
evidence: [{ file: ".pi/skills/replay-demo/SKILL.md", line: 9, quote: "index.json" }],
|
|
89
|
+
});
|
|
90
|
+
const outcome = await runReplay({
|
|
91
|
+
cwd: dir,
|
|
92
|
+
evolutionDir: path.join(dir, ".pi", "evolution"),
|
|
93
|
+
fakeReview: true,
|
|
94
|
+
fakeReviewText: reject,
|
|
95
|
+
git: fakeGit,
|
|
96
|
+
now: () => Date.UTC(2026, 10, 8, 12),
|
|
97
|
+
});
|
|
98
|
+
assert.equal(outcome.ok, false);
|
|
99
|
+
assert.equal(outcome.failures.length, 1);
|
|
100
|
+
assert.match(outcome.failures[0]!, /^review:/);
|
|
101
|
+
assert.equal(outcome.stages.approve, "未执行");
|
|
102
|
+
const skillText = fs.readFileSync(path.join(dir, ".pi", "skills", "replay-demo", "SKILL.md"), "utf8");
|
|
103
|
+
assert.ok(!skillText.includes("index.json 再读会话原文"), "评审没过就不许动技能文件");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("复演假评审文本本身要过字段校验,否则流水线验的是假结论", async () => {
|
|
107
|
+
const { postProcessReview } = await import("../lib/review-post.ts");
|
|
108
|
+
const result = postProcessReview(FAKE_REVIEW_TEXT);
|
|
109
|
+
assert.equal(result.status, "ok");
|
|
110
|
+
assert.equal(result.verdict, "pass");
|
|
111
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status / report 文本单测:提案摘要只显示不应用,且没提案的 run 不该多出空行。
|
|
3
|
+
*/
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { renderReport, renderStatus } from "../lib/report.ts";
|
|
7
|
+
import type { EvolutionConfig } from "../lib/config.ts";
|
|
8
|
+
import { summarize } from "../lib/telemetry.ts";
|
|
9
|
+
|
|
10
|
+
const CONFIG: EvolutionConfig = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
idleMs: 90_000,
|
|
13
|
+
maxRuntimeMs: 1_200_000,
|
|
14
|
+
perSessionCap: 2,
|
|
15
|
+
telemetry: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const NOW = 1_760_000_000_000;
|
|
19
|
+
|
|
20
|
+
test("status:run 列表带提案摘要(只显示,不应用)", () => {
|
|
21
|
+
const text = renderStatus({
|
|
22
|
+
config: CONFIG,
|
|
23
|
+
configPath: "/repo/.pi/evolution/config.json",
|
|
24
|
+
telemetryDir: "/repo/.pi/evolution/telemetry",
|
|
25
|
+
lockPid: undefined,
|
|
26
|
+
runs: [
|
|
27
|
+
{
|
|
28
|
+
runId: "abc12345-1",
|
|
29
|
+
status: "proposed",
|
|
30
|
+
dispatchedAt: NOW,
|
|
31
|
+
sessionId: "s1",
|
|
32
|
+
proposal: { type: "improve", skill: "deploy-test", status: "ok", added: 2, removed: 1, file: "02-proposal.diff" },
|
|
33
|
+
},
|
|
34
|
+
{ runId: "abc12345-0", status: "analyzed", dispatchedAt: NOW - 1000, sessionId: "s1" },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
assert.match(text, /abc12345-1\s+proposed\s+.*提案 improve deploy-test \[ok\] \+2\/-1/);
|
|
38
|
+
assert.match(text, /abc12345-0\s+analyzed/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("report:有提案时多一句「不应用」说明,没提案时不出现", () => {
|
|
42
|
+
const summary = summarize([]);
|
|
43
|
+
const withProposal = renderReport({
|
|
44
|
+
days: 7,
|
|
45
|
+
summary,
|
|
46
|
+
runs: [
|
|
47
|
+
{
|
|
48
|
+
runId: "abc12345-1",
|
|
49
|
+
status: "proposed",
|
|
50
|
+
dispatchedAt: NOW,
|
|
51
|
+
sessionId: "s1",
|
|
52
|
+
proposal: { type: "create", skill: "deploy-check", status: "ok", added: 0, removed: 0, file: "candidate/deploy-check/SKILL.md" },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
assert.match(withProposal, /提案 create deploy-check \[ok\]/);
|
|
57
|
+
assert.match(withProposal, /提案只生成不应用/);
|
|
58
|
+
|
|
59
|
+
const withoutProposal = renderReport({
|
|
60
|
+
days: 7,
|
|
61
|
+
summary,
|
|
62
|
+
runs: [{ runId: "abc12345-0", status: "analyzed", dispatchedAt: NOW, sessionId: "s1" }],
|
|
63
|
+
});
|
|
64
|
+
assert.doesNotMatch(withoutProposal, /提案只生成不应用/);
|
|
65
|
+
assert.match(withoutProposal, /加载率:无曝光数据/);
|
|
66
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 评审后处理单测(设计稿 12.3):结构、证据、一致性一律代码判,不信 LLM 自觉。
|
|
3
|
+
* 每条用例对应「模型输出的一种坏法」,覆盖判 invalid / reject 的每一道关。
|
|
4
|
+
*/
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { parseReviewJson, postProcessReview } from "../lib/review-post.ts";
|
|
8
|
+
|
|
9
|
+
const GOOD_PASS = JSON.stringify({ verdict: "pass", blocking: [], notes: "没问题" });
|
|
10
|
+
const withBlocking = (verdict: string, evidence: unknown): string =>
|
|
11
|
+
JSON.stringify({ verdict, blocking: [{ issue: "有问题", evidence }], notes: "" });
|
|
12
|
+
const EVIDENCE = { file: ".pi/skills/demo/SKILL.md", line: 12, quote: "允许用 find" };
|
|
13
|
+
|
|
14
|
+
test("合法 pass 返回 ok", () => {
|
|
15
|
+
const result = postProcessReview(GOOD_PASS);
|
|
16
|
+
assert.equal(result.status, "ok");
|
|
17
|
+
assert.equal(result.verdict, "pass");
|
|
18
|
+
assert.deepEqual(result.blocking, []);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("带 ```json 围栏与前后废话也能抠出 JSON", () => {
|
|
22
|
+
const text = `好的,结论如下:\n\`\`\`json\n${GOOD_PASS}\n\`\`\`\n以上。`;
|
|
23
|
+
assert.equal(parseReviewJson(text).json?.verdict, "pass");
|
|
24
|
+
assert.equal(postProcessReview(text).status, "ok");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("坏 verdict 判 invalid:bad-verdict", () => {
|
|
28
|
+
for (const verdict of ["approved", "", 42, null]) {
|
|
29
|
+
const result = postProcessReview(JSON.stringify({ verdict, blocking: [] }));
|
|
30
|
+
assert.equal(result.status, "invalid");
|
|
31
|
+
assert.match(result.detail ?? "", /bad-verdict/);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("根本不是 JSON 判 invalid:bad-json", () => {
|
|
36
|
+
const result = postProcessReview("我觉得还行,可以过。");
|
|
37
|
+
assert.equal(result.status, "invalid");
|
|
38
|
+
assert.match(result.detail ?? "", /bad-json/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("blocking 不是数组判 invalid", () => {
|
|
42
|
+
const result = postProcessReview(JSON.stringify({ verdict: "reject", blocking: "missing-evidence" }));
|
|
43
|
+
assert.equal(result.status, "invalid");
|
|
44
|
+
assert.match(result.detail ?? "", /bad-blocking/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("evidence 缺字段逐种判 invalid:file / line / quote", () => {
|
|
48
|
+
const cases: [unknown, RegExp][] = [
|
|
49
|
+
[{ line: 1, quote: "x" }, /evidence.file 为空/],
|
|
50
|
+
[{ file: ".pi/skills/demo/SKILL.md", quote: "x" }, /evidence.line 不是正整数/],
|
|
51
|
+
[{ file: ".pi/skills/demo/SKILL.md", line: 0, quote: "x" }, /evidence.line 不是正整数/],
|
|
52
|
+
[{ file: ".pi/skills/demo/SKILL.md", line: -3, quote: "x" }, /evidence.line 不是正整数/],
|
|
53
|
+
[{ file: ".pi/skills/demo/SKILL.md", line: 2.5, quote: "x" }, /evidence.line 不是正整数/],
|
|
54
|
+
[{ file: ".pi/skills/demo/SKILL.md", line: "12", quote: "x" }, /evidence.line 不是正整数/],
|
|
55
|
+
[{ file: ".pi/skills/demo/SKILL.md", line: 12, quote: " " }, /evidence.quote 为空/],
|
|
56
|
+
[null, /evidence 不是对象/],
|
|
57
|
+
];
|
|
58
|
+
for (const [evidence, pattern] of cases) {
|
|
59
|
+
const result = postProcessReview(withBlocking("reject", evidence));
|
|
60
|
+
assert.equal(result.status, "invalid", `evidence=${JSON.stringify(evidence)}`);
|
|
61
|
+
assert.match(result.detail ?? "", /missing-evidence/);
|
|
62
|
+
assert.match(result.detail ?? "", pattern);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("pass 却带 blocking 判 invalid:contradiction", () => {
|
|
67
|
+
const result = postProcessReview(withBlocking("pass", EVIDENCE));
|
|
68
|
+
assert.equal(result.status, "invalid");
|
|
69
|
+
assert.match(result.detail ?? "", /contradiction/);
|
|
70
|
+
assert.equal(result.blocking.length, 1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("reject 带完整证据返回 ok 且保留 blocking/notes", () => {
|
|
74
|
+
const result = postProcessReview(JSON.stringify({ verdict: "reject", blocking: [{ issue: "引入 find", evidence: EVIDENCE }], notes: "违反 P0" }));
|
|
75
|
+
assert.equal(result.status, "ok");
|
|
76
|
+
assert.equal(result.verdict, "reject");
|
|
77
|
+
assert.equal(result.blocking[0].evidence.line, 12);
|
|
78
|
+
assert.equal(result.notes, "违反 P0");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("机械复验失败一票 reject,覆盖 LLM 的 pass", () => {
|
|
82
|
+
const result = postProcessReview(GOOD_PASS, { recheck: () => "frontmatter 缺 name" });
|
|
83
|
+
assert.equal(result.status, "reject");
|
|
84
|
+
assert.equal(result.verdict, "reject");
|
|
85
|
+
assert.match(result.detail ?? "", /frontmatter-invalid: frontmatter 缺 name/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("机械复验通过时不影响 LLM 结论", () => {
|
|
89
|
+
const result = postProcessReview(GOOD_PASS, { recheck: () => null });
|
|
90
|
+
assert.equal(result.status, "ok");
|
|
91
|
+
assert.equal(result.verdict, "pass");
|
|
92
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 评审 prompt 组装单测:独立性靠「给什么」保证,所以这里断言的是「没给什么」。
|
|
3
|
+
*
|
|
4
|
+
* 设计稿 12.1 硬约束②:只给改后全文 + diff + 硬禁令 + 验收标准,
|
|
5
|
+
* findings / rationale 一旦混进来,评审就会顺着提案者的话往下点头。
|
|
6
|
+
*/
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { ACCEPTANCE_STANDARDS, buildReviewPrompt } from "../lib/review-prompt.ts";
|
|
10
|
+
|
|
11
|
+
const DIFF = [
|
|
12
|
+
"--- a/.pi/skills/demo/SKILL.md",
|
|
13
|
+
"+++ b/.pi/skills/demo/SKILL.md",
|
|
14
|
+
"@@ -3,1 +3,1 @@",
|
|
15
|
+
"-description: 处理各种事情",
|
|
16
|
+
"+description: 检索日志时用 rg,禁止 find 全盘扫",
|
|
17
|
+
].join("\n");
|
|
18
|
+
|
|
19
|
+
const SKILL_TEXT = ["---", "name: demo", "description: 检索日志时用 rg,禁止 find 全盘扫", "---", "正文"].join("\n");
|
|
20
|
+
|
|
21
|
+
const RULES = [
|
|
22
|
+
{ id: "rg-over-find", level: "P0", expect: "禁止用 find 全盘扫描" },
|
|
23
|
+
{ id: "no-golden-midscene", level: "P0", expect: "不写 golden 截图" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
test("prompt 里不含复盘产物,也不含 rationale 字段", () => {
|
|
27
|
+
const prompt = buildReviewPrompt({
|
|
28
|
+
skillName: "demo",
|
|
29
|
+
skillText: SKILL_TEXT,
|
|
30
|
+
diffApplied: true,
|
|
31
|
+
diff: DIFF,
|
|
32
|
+
hardRules: RULES,
|
|
33
|
+
});
|
|
34
|
+
assert.ok(!prompt.includes("01-findings"), "不得出现 01-findings");
|
|
35
|
+
assert.ok(!prompt.includes("rationale"), "不得出现 rationale 字样");
|
|
36
|
+
assert.ok(!prompt.includes("02-proposal"), "不得出现 02-proposal 路径");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("prompt 含全部硬禁令 ruleId 与 expect,且 diff 原样出现", () => {
|
|
40
|
+
const prompt = buildReviewPrompt({
|
|
41
|
+
skillName: "demo",
|
|
42
|
+
skillText: SKILL_TEXT,
|
|
43
|
+
diffApplied: true,
|
|
44
|
+
diff: DIFF,
|
|
45
|
+
hardRules: RULES,
|
|
46
|
+
});
|
|
47
|
+
for (const rule of RULES) {
|
|
48
|
+
assert.ok(prompt.includes(rule.id), `缺规则 ${rule.id}`);
|
|
49
|
+
assert.ok(prompt.includes(rule.expect), `缺规则说明 ${rule.id}`);
|
|
50
|
+
}
|
|
51
|
+
assert.ok(prompt.includes(DIFF), "diff 必须原样出现");
|
|
52
|
+
assert.ok(prompt.includes("rg-over-find") && prompt.includes("P0"));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("验收标准四条全部写入 prompt", () => {
|
|
56
|
+
const prompt = buildReviewPrompt({
|
|
57
|
+
skillName: "demo",
|
|
58
|
+
skillText: SKILL_TEXT,
|
|
59
|
+
diffApplied: true,
|
|
60
|
+
diff: DIFF,
|
|
61
|
+
hardRules: RULES,
|
|
62
|
+
});
|
|
63
|
+
for (const line of ACCEPTANCE_STANDARDS) assert.ok(prompt.includes(line), `缺验收标准:${line}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("apply 失败时明说「只有 diff 可信」,不冒充改后全文", () => {
|
|
67
|
+
const prompt = buildReviewPrompt({
|
|
68
|
+
skillName: "demo",
|
|
69
|
+
skillText: SKILL_TEXT,
|
|
70
|
+
diffApplied: false,
|
|
71
|
+
diff: DIFF,
|
|
72
|
+
hardRules: RULES,
|
|
73
|
+
});
|
|
74
|
+
assert.match(prompt, /改动前/);
|
|
75
|
+
assert.match(prompt, /只有上面的 diff 可信/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("CompiledRule 形态也能渲染出匹配模式", () => {
|
|
79
|
+
const prompt = buildReviewPrompt({
|
|
80
|
+
skillName: "demo",
|
|
81
|
+
skillText: SKILL_TEXT,
|
|
82
|
+
diffApplied: true,
|
|
83
|
+
diff: DIFF,
|
|
84
|
+
hardRules: [{ rule: { id: "rg-over-find", level: "P0", pattern: "\\bfind\\b", expect: "禁止 find" }, regex: /x/ }],
|
|
85
|
+
});
|
|
86
|
+
assert.ok(prompt.includes("rg-over-find"));
|
|
87
|
+
assert.ok(prompt.includes("\\bfind\\b"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("空禁令列表给出明确文案而不是留白", () => {
|
|
91
|
+
const prompt = buildReviewPrompt({
|
|
92
|
+
skillName: "demo",
|
|
93
|
+
skillText: SKILL_TEXT,
|
|
94
|
+
diffApplied: true,
|
|
95
|
+
diff: DIFF,
|
|
96
|
+
hardRules: [],
|
|
97
|
+
});
|
|
98
|
+
assert.match(prompt, /没有可注入的硬约束/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("verdict 语义写进 prompt:能改的小毛病不许判 reject(真实自检第一跑就栽在这)", () => {
|
|
102
|
+
const prompt = buildReviewPrompt({ skillName: "demo", skillText: "正文", diffApplied: true, diff: "+++ b/x\n", hardRules: [] });
|
|
103
|
+
assert.match(prompt, /revise:方向可以接受/);
|
|
104
|
+
assert.match(prompt, /reject:删掉了原有的明确禁止项/);
|
|
105
|
+
});
|