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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话送达:三类事件各发一条、同 runId 不重复、send 抛错不炸主流程、无 UI 时降级为日志。
|
|
3
|
+
*/
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { collectNotifications, deliverNotifications, NOTIFIED_FILE } from "../lib/notify.ts";
|
|
10
|
+
|
|
11
|
+
const NOW = Date.UTC(2026, 10, 8, 12);
|
|
12
|
+
|
|
13
|
+
function workspace(): string {
|
|
14
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-notify-"));
|
|
15
|
+
const evolutionDir = path.join(cwd, ".pi", "evolution");
|
|
16
|
+
fs.mkdirSync(path.join(evolutionDir, "runs"), { recursive: true });
|
|
17
|
+
return evolutionDir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeRun(evolutionDir: string, runId: string, status: string): string {
|
|
21
|
+
const runDir = path.join(evolutionDir, "runs", runId);
|
|
22
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
23
|
+
const record = { runId, status, dispatchedAt: NOW, sessionId: "s", trigger: "auto", pid: 1 };
|
|
24
|
+
fs.writeFileSync(path.join(runDir, "run.json"), JSON.stringify(record), "utf8");
|
|
25
|
+
return runDir;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeQueue(evolutionDir: string, items: Record<string, unknown>[]): void {
|
|
29
|
+
fs.writeFileSync(path.join(evolutionDir, "queue.json"), JSON.stringify(items), "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function item(runId: string, status: string, level: string, skill = "demo"): Record<string, unknown> {
|
|
33
|
+
return { runId, level, skill, targets: [`.pi/skills/${skill}/SKILL.md`], createdAt: NOW, status };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("stale:一条消息,带原因与日志末行", () => {
|
|
37
|
+
const evolutionDir = workspace();
|
|
38
|
+
const runDir = writeRun(evolutionDir, "run-dead", "stale");
|
|
39
|
+
fs.writeFileSync(
|
|
40
|
+
path.join(runDir, "run-error.json"),
|
|
41
|
+
JSON.stringify({ reason: "timeout", at: NOW, childLogTail: ["准备中", "子进程 12 分钟没动静"] }),
|
|
42
|
+
"utf8",
|
|
43
|
+
);
|
|
44
|
+
const sent: string[] = [];
|
|
45
|
+
deliverNotifications({ evolutionDir, send: (text) => sent.push(text) });
|
|
46
|
+
assert.equal(sent.length, 1);
|
|
47
|
+
assert.match(sent[0]!, /^技能复盘失败:run-dead(超时)/);
|
|
48
|
+
assert.match(sent[0]!, /日志末行:子进程 12 分钟没动静/);
|
|
49
|
+
assert.equal(sent[0]!.split("\n").length, 2, "最多两行");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("待批:一条消息,附采纳命令", () => {
|
|
53
|
+
const evolutionDir = workspace();
|
|
54
|
+
writeRun(evolutionDir, "run-wait", "reviewed");
|
|
55
|
+
writeQueue(evolutionDir, [item("run-wait", "pending", "approval", "rust")]);
|
|
56
|
+
const sent: string[] = [];
|
|
57
|
+
deliverNotifications({ evolutionDir, send: (text) => sent.push(text) });
|
|
58
|
+
assert.equal(sent.length, 1);
|
|
59
|
+
assert.match(sent[0]!, /^待批:run-wait 改动 rust/);
|
|
60
|
+
assert.match(sent[0]!, /\/skill-evolve approve run-wait/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("auto 采纳:一条消息,没有 diff", () => {
|
|
64
|
+
const evolutionDir = workspace();
|
|
65
|
+
writeRun(evolutionDir, "run-auto", "applied");
|
|
66
|
+
writeQueue(evolutionDir, [item("run-auto", "applied", "auto")]);
|
|
67
|
+
const sent: string[] = [];
|
|
68
|
+
deliverNotifications({ evolutionDir, send: (text) => sent.push(text) });
|
|
69
|
+
assert.deepEqual(sent, ["已自动采纳:run-auto(demo)"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("去重:同一 runId 的同一类事件只发一次,跨调用也认(notified.json)", () => {
|
|
73
|
+
const evolutionDir = workspace();
|
|
74
|
+
writeRun(evolutionDir, "run-once", "stale");
|
|
75
|
+
const first: string[] = [];
|
|
76
|
+
const second: string[] = [];
|
|
77
|
+
assert.equal(deliverNotifications({ evolutionDir, now: NOW, send: (text) => first.push(text) }).length, 1);
|
|
78
|
+
assert.equal(deliverNotifications({ evolutionDir, now: NOW + 1, send: (text) => second.push(text) }).length, 0);
|
|
79
|
+
assert.equal(first.length, 1);
|
|
80
|
+
assert.deepEqual(second, []);
|
|
81
|
+
const notified = JSON.parse(fs.readFileSync(path.join(evolutionDir, "runs", "run-once", NOTIFIED_FILE), "utf8"));
|
|
82
|
+
assert.deepEqual(notified.kinds, ["stale"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("send 抛错不炸主流程,且不记 notified(下一轮还能重试)", () => {
|
|
86
|
+
const evolutionDir = workspace();
|
|
87
|
+
writeRun(evolutionDir, "run-throw", "stale");
|
|
88
|
+
const logs: string[] = [];
|
|
89
|
+
const delivered = deliverNotifications({
|
|
90
|
+
evolutionDir,
|
|
91
|
+
send: () => {
|
|
92
|
+
throw new Error("no ui");
|
|
93
|
+
},
|
|
94
|
+
log: (msg) => logs.push(msg),
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual(delivered, [], "这条没送出去,不算已送达");
|
|
97
|
+
assert.equal(logs.length, 1);
|
|
98
|
+
assert.match(logs[0]!, /会话消息发送失败(run-throw)/);
|
|
99
|
+
assert.equal(fs.existsSync(path.join(evolutionDir, "runs", "run-throw", NOTIFIED_FILE)), false);
|
|
100
|
+
|
|
101
|
+
const retried: string[] = [];
|
|
102
|
+
assert.equal(deliverNotifications({ evolutionDir, send: (text) => retried.push(text) }).length, 1);
|
|
103
|
+
assert.equal(retried.length, 1, "重试成功");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("拿不到 send(老版本 / 无 UI):降级成日志,且不再重复刷", () => {
|
|
107
|
+
const evolutionDir = workspace();
|
|
108
|
+
writeRun(evolutionDir, "run-log", "stale");
|
|
109
|
+
const logs: string[] = [];
|
|
110
|
+
assert.equal(deliverNotifications({ evolutionDir, log: (msg) => logs.push(msg) }).length, 1);
|
|
111
|
+
assert.equal(logs.length, 1);
|
|
112
|
+
assert.match(logs[0]!, /技能复盘失败:run-log/, "多行文本压成一行");
|
|
113
|
+
assert.equal(logs[0]!.includes("\n"), false);
|
|
114
|
+
assert.equal(deliverNotifications({ evolutionDir, log: (msg) => logs.push(msg) }).length, 0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("队列里没有对应 run 目录的条目(手工塞的)不推送", () => {
|
|
118
|
+
const evolutionDir = workspace();
|
|
119
|
+
writeQueue(evolutionDir, [item("run-ghost", "pending", "approval")]);
|
|
120
|
+
assert.deepEqual(collectNotifications(evolutionDir), []);
|
|
121
|
+
const sent: string[] = [];
|
|
122
|
+
assert.deepEqual(deliverNotifications({ evolutionDir, send: (text) => sent.push(text) }), []);
|
|
123
|
+
assert.deepEqual(sent, []);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("三类事件混在一起时各发一条,顺序稳定", () => {
|
|
127
|
+
const evolutionDir = workspace();
|
|
128
|
+
writeRun(evolutionDir, "run-mixed", "stale");
|
|
129
|
+
writeRun(evolutionDir, "run-ok", "applied");
|
|
130
|
+
writeQueue(evolutionDir, [item("run-ok", "applied", "auto")]);
|
|
131
|
+
const kinds = collectNotifications(evolutionDir).map((event) => `${event.runId}:${event.kind}`);
|
|
132
|
+
assert.deepEqual(kinds, ["run-mixed:stale", "run-ok:auto-applied"]);
|
|
133
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M6 可观测胶水:面板刷新把状态变迁转成条目、派发立刻记一条、周报顺带落指标。
|
|
3
|
+
* 不 import pi 包,也不需要真子进程(周报在没有候选技能时不会去跑 rg)。
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import { createObserver } from "../lib/evolution-observe.ts";
|
|
11
|
+
import { metricsFile, readMetrics } from "../lib/metrics.ts";
|
|
12
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
13
|
+
|
|
14
|
+
const NOW = Date.UTC(2026, 10, 8, 12);
|
|
15
|
+
|
|
16
|
+
function tmpWorkspace(): { cwd: string; evolutionDir: string; telemetryDir: string } {
|
|
17
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-observe-"));
|
|
18
|
+
const evolutionDir = path.join(cwd, ".pi", "evolution");
|
|
19
|
+
fs.mkdirSync(path.join(cwd, ".pi", "skills", "demo"), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(cwd, ".pi", "skills", "demo", "SKILL.md"), "---\nname: demo\ndescription: 演示\n---\n", "utf8");
|
|
21
|
+
return { cwd, evolutionDir, telemetryDir: path.join(evolutionDir, "telemetry") };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeRun(evolutionDir: string, runId: string, status: string): void {
|
|
25
|
+
const runDir = path.join(evolutionDir, "runs", runId);
|
|
26
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(runDir, "run.json"), JSON.stringify({ runId, status, dispatchedAt: NOW, sessionId: "s" }), "utf8");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("refresh:状态变迁发条目并推面板状态", () => {
|
|
31
|
+
const { evolutionDir, cwd, telemetryDir } = tmpWorkspace();
|
|
32
|
+
writeRun(evolutionDir, "r1", "reviewed");
|
|
33
|
+
writeRun(evolutionDir, "r2", "dispatched");
|
|
34
|
+
const entries: string[] = [];
|
|
35
|
+
const states: number[] = [];
|
|
36
|
+
const observer = createObserver({
|
|
37
|
+
cwd,
|
|
38
|
+
evolutionDir,
|
|
39
|
+
telemetryDir,
|
|
40
|
+
queue: directQueue,
|
|
41
|
+
now: () => NOW,
|
|
42
|
+
panel: {
|
|
43
|
+
onEntry: (entry) => entries.push(`${entry.kind}:${entry.runId}`),
|
|
44
|
+
onState: (state) => states.push(state.reviewing),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
observer.refresh();
|
|
48
|
+
assert.deepEqual(entries, ["reviewed:r1"], "只有出了结论的 run 发条目");
|
|
49
|
+
assert.deepEqual(states, [1], "dispatched 的 run 算进行中");
|
|
50
|
+
observer.refresh();
|
|
51
|
+
assert.deepEqual(entries, ["reviewed:r1"], "重复刷新不重复发");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("noteDispatched:派发立刻记一条,下一轮 refresh 不会重复记同一状态", () => {
|
|
55
|
+
const { evolutionDir, cwd, telemetryDir } = tmpWorkspace();
|
|
56
|
+
writeRun(evolutionDir, "r9", "dispatched");
|
|
57
|
+
const entries: string[] = [];
|
|
58
|
+
const observer = createObserver({
|
|
59
|
+
cwd,
|
|
60
|
+
evolutionDir,
|
|
61
|
+
telemetryDir,
|
|
62
|
+
queue: directQueue,
|
|
63
|
+
now: () => NOW,
|
|
64
|
+
panel: { onEntry: (entry) => entries.push(`${entry.kind}:${entry.runId}`) },
|
|
65
|
+
});
|
|
66
|
+
observer.noteDispatched("r9");
|
|
67
|
+
assert.deepEqual(entries, ["dispatched:r9"]);
|
|
68
|
+
observer.refresh();
|
|
69
|
+
assert.deepEqual(entries, ["dispatched:r9"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("weekReportText:出周报并把当周指标落一行(重复调用不重复写)", async () => {
|
|
73
|
+
const { evolutionDir, cwd, telemetryDir } = tmpWorkspace();
|
|
74
|
+
fs.mkdirSync(telemetryDir, { recursive: true });
|
|
75
|
+
const line = {
|
|
76
|
+
ts: NOW,
|
|
77
|
+
iso: new Date(NOW).toISOString(),
|
|
78
|
+
kind: "advertised",
|
|
79
|
+
sessionId: "s",
|
|
80
|
+
skills: [{ name: "demo", filePath: "/x/demo/SKILL.md", disabled: false, source: "project" }],
|
|
81
|
+
};
|
|
82
|
+
fs.writeFileSync(path.join(telemetryDir, "2026-11-08.jsonl"), `${JSON.stringify(line)}\n`, "utf8");
|
|
83
|
+
const observer = createObserver({ cwd, evolutionDir, telemetryDir, queue: directQueue, now: () => NOW });
|
|
84
|
+
const first = await observer.weekReportText();
|
|
85
|
+
assert.match(first, /加载率 0\.0%(加载 0 \/ 曝光 1)/);
|
|
86
|
+
const rows = readMetrics(evolutionDir);
|
|
87
|
+
assert.equal(rows.length, 1);
|
|
88
|
+
assert.equal(rows[0]!.advertised, 1);
|
|
89
|
+
await observer.weekReportText();
|
|
90
|
+
assert.equal(readMetrics(evolutionDir).length, 1, "同周同 runId 不重复写");
|
|
91
|
+
assert.equal(fs.existsSync(metricsFile(evolutionDir)), true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("weekReportText:没有任何遥测也不抛,报告写样本不足", async () => {
|
|
95
|
+
const { evolutionDir, cwd, telemetryDir } = tmpWorkspace();
|
|
96
|
+
const observer = createObserver({ cwd, evolutionDir, telemetryDir, queue: directQueue, now: () => NOW });
|
|
97
|
+
const text = await observer.weekReportText();
|
|
98
|
+
assert.match(text, /技能进化周报:2026-W45/);
|
|
99
|
+
assert.match(text, /候选表只是提醒,不是判决/);
|
|
100
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「谁持有 Web 面板」的记录文件(`.pi/evolution/locks/web.json`):
|
|
3
|
+
* 坏数据当没有、写失败不抛、删记录要比 pid。只碰临时目录,不起任何子进程。
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import { clearWebPid, isAlive, readWebPid, WEB_PID_FILE, webPidFile, writeWebPid } from "../lib/web/panel-pid.ts";
|
|
11
|
+
|
|
12
|
+
function workspace(): string {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-panelpid-"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 不存在的 pid:Linux 的 pid_max 远小于 INT32_MAX,这个号系统里不会有进程 */
|
|
17
|
+
const DEAD_PID = 2_147_483_646;
|
|
18
|
+
|
|
19
|
+
function record(pid: number): { pid: number; host: string; port: number; url: string; startedAt: number } {
|
|
20
|
+
return { pid, host: "127.0.0.1", port: 7788, url: "http://127.0.0.1:7788", startedAt: 1_760_000_000_000 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("记录落在 locks/web.json:writeWebPid 建目录、原子写、能原样读回", () => {
|
|
24
|
+
const dir = workspace();
|
|
25
|
+
const info = record(process.pid);
|
|
26
|
+
writeWebPid(dir, info);
|
|
27
|
+
assert.equal(WEB_PID_FILE, path.join("locks", "web.json"));
|
|
28
|
+
assert.deepEqual(readWebPid(dir), info);
|
|
29
|
+
// 原子写的临时文件不能留在目录里
|
|
30
|
+
assert.deepEqual(fs.readdirSync(path.join(dir, "locks")), ["web.json"]);
|
|
31
|
+
assert.equal(fs.existsSync(`${webPidFile(dir)}.tmp`), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("坏 JSON / 字段不全 / 不是对象 → readWebPid 一律 null", () => {
|
|
35
|
+
const dir = workspace();
|
|
36
|
+
const file = webPidFile(dir);
|
|
37
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
38
|
+
const good = record(process.pid);
|
|
39
|
+
const bad = [
|
|
40
|
+
"{ 不是 JSON",
|
|
41
|
+
"",
|
|
42
|
+
"[]",
|
|
43
|
+
"null",
|
|
44
|
+
JSON.stringify({ ...good, pid: 0 }),
|
|
45
|
+
JSON.stringify({ ...good, port: "7788" }),
|
|
46
|
+
JSON.stringify({ ...good, url: "" }),
|
|
47
|
+
JSON.stringify({ ...good, startedAt: "昨天" }),
|
|
48
|
+
JSON.stringify({ pid: good.pid, host: good.host }),
|
|
49
|
+
];
|
|
50
|
+
for (const raw of bad) {
|
|
51
|
+
fs.writeFileSync(file, raw, "utf8");
|
|
52
|
+
assert.equal(readWebPid(dir), null, `坏数据必须当没有:${raw}`);
|
|
53
|
+
}
|
|
54
|
+
fs.unlinkSync(file);
|
|
55
|
+
assert.equal(readWebPid(dir), null, "文件不存在同样是 null");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("isAlive:本进程算活,不存在的 pid / 非法 pid 算死", () => {
|
|
59
|
+
assert.equal(isAlive(process.pid), true);
|
|
60
|
+
assert.equal(isAlive(DEAD_PID), false);
|
|
61
|
+
assert.equal(isAlive(0), false);
|
|
62
|
+
assert.equal(isAlive(-1), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("写记录失败只记日志不抛:locks 位置被文件占住时面板照常运行", () => {
|
|
66
|
+
const dir = workspace();
|
|
67
|
+
fs.writeFileSync(path.join(dir, "locks"), "占位", "utf8");
|
|
68
|
+
const logs: string[] = [];
|
|
69
|
+
assert.doesNotThrow(() => writeWebPid(dir, record(process.pid), (msg) => logs.push(msg)));
|
|
70
|
+
assert.equal(logs.length, 1);
|
|
71
|
+
assert.match(logs[0]!, /写 Web 面板记录失败/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("clearWebPid:pid 对上才删,别人的记录与读不出来的坏记录都留着", () => {
|
|
75
|
+
const dir = workspace();
|
|
76
|
+
writeWebPid(dir, record(4321));
|
|
77
|
+
clearWebPid(dir, process.pid);
|
|
78
|
+
assert.equal(readWebPid(dir)?.pid, 4321, "不是自己写的记录不能删");
|
|
79
|
+
clearWebPid(dir, 4321);
|
|
80
|
+
assert.equal(fs.existsSync(webPidFile(dir)), false);
|
|
81
|
+
fs.writeFileSync(webPidFile(dir), "{坏", "utf8");
|
|
82
|
+
clearWebPid(dir, process.pid);
|
|
83
|
+
assert.equal(fs.existsSync(webPidFile(dir)), true, "坏记录无法证明归属,也不清");
|
|
84
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M6 面板:没数据不显示 0、状态栏只放最紧急一件、状态变迁只发一次 `/tree` 条目。
|
|
3
|
+
* 用例编号对应设计稿 14.1(四层可见性)。
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import { collectPanelState, entryLines, panelWidget, statusEntries, statusLine } from "../lib/panel.ts";
|
|
11
|
+
|
|
12
|
+
const NOW = Date.UTC(2026, 10, 8, 12);
|
|
13
|
+
|
|
14
|
+
function tmpDir(): string {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-panel-"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeRun(dir: string, runId: string, status: string, dispatchedAt = NOW, extra: Record<string, unknown> = {}): void {
|
|
19
|
+
const runDir = path.join(dir, "runs", runId);
|
|
20
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(runDir, "run.json"), JSON.stringify({ runId, status, dispatchedAt, sessionId: "s", ...extra }), "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeQueue(dir: string, items: unknown[]): void {
|
|
25
|
+
fs.writeFileSync(path.join(dir, "queue.json"), JSON.stringify(items), "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("panelWidget:全零返回 null,不显示「复盘 0 / 待批 0」", () => {
|
|
29
|
+
assert.equal(panelWidget({ reviewing: 0, staleCount: 0, pendingApprovals: 0, lastAppliedAt: null, rollbackRate: 0, appliedSamples: 0 }), null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("panelWidget:有哪项就拼哪项,缺项不占位", () => {
|
|
33
|
+
const state = { reviewing: 3, staleCount: 0, pendingApprovals: 0, lastAppliedAt: Date.UTC(2026, 10, 7), rollbackRate: 0, appliedSamples: 2 };
|
|
34
|
+
assert.deepEqual(panelWidget(state), ["复盘 3 / 上次采纳 11-07"]);
|
|
35
|
+
assert.deepEqual(panelWidget({ ...state, pendingApprovals: 2, reviewing: 0 }), ["待批 2 / 上次采纳 11-07"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("statusLine:待批优先于回滚率告警,只显示一件事", () => {
|
|
39
|
+
const state = { reviewing: 4, staleCount: 0, pendingApprovals: 2, lastAppliedAt: NOW, rollbackRate: 0.5, appliedSamples: 10 };
|
|
40
|
+
assert.equal(statusLine(state), "待批 2");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("statusLine:样本少于 5 次不下回滚率结论", () => {
|
|
44
|
+
assert.equal(statusLine({ reviewing: 0, staleCount: 0, pendingApprovals: 0, lastAppliedAt: NOW, rollbackRate: 0.4, appliedSamples: 4 }), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("statusLine:样本够且超 5% 才告警(含建议关 auto)", () => {
|
|
48
|
+
const state = { reviewing: 1, staleCount: 0, pendingApprovals: 0, lastAppliedAt: NOW, rollbackRate: 0.06, appliedSamples: 6 };
|
|
49
|
+
assert.equal(statusLine(state), "回滚率 6% 超 5%,建议关 auto");
|
|
50
|
+
assert.equal(statusLine({ ...state, rollbackRate: 0.05 }), null, "正好 5% 不算「超」");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("collectPanelState:进行中的 run 算「复盘」,待批只数 approval 级 pending", () => {
|
|
54
|
+
const dir = tmpDir();
|
|
55
|
+
writeRun(dir, "r1", "dispatched");
|
|
56
|
+
writeRun(dir, "r2", "proposed", NOW, { supervisorPid: 1 });
|
|
57
|
+
writeRun(dir, "r5", "proposed", NOW, { pid: 999999 });
|
|
58
|
+
writeRun(dir, "r3", "reviewed");
|
|
59
|
+
writeQueue(dir, [
|
|
60
|
+
{ runId: "r3", level: "approval", skill: "a", targets: [], proposalType: "improve", status: "pending", createdAt: NOW },
|
|
61
|
+
{ runId: "r4", level: "auto", skill: "b", targets: [], proposalType: "improve", status: "pending", createdAt: NOW },
|
|
62
|
+
]);
|
|
63
|
+
const state = collectPanelState({ evolutionDir: dir, now: NOW, alive: (pid) => pid === 1 });
|
|
64
|
+
assert.equal(state.reviewing, 2, "dispatched 宽限期内 + 督导活着的 proposed 算;结论已出且进程不在的不算");
|
|
65
|
+
assert.equal(state.pendingApprovals, 1, "auto 条不等审批");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("collectPanelState:上次采纳取 04-decision.json 的 at,采纳 / 回滚率按队列算", () => {
|
|
69
|
+
const dir = tmpDir();
|
|
70
|
+
const appliedAt = Date.UTC(2026, 10, 7, 8);
|
|
71
|
+
writeRun(dir, "r1", "applied");
|
|
72
|
+
fs.writeFileSync(path.join(dir, "runs", "r1", "04-decision.json"), JSON.stringify({ at: appliedAt }), "utf8");
|
|
73
|
+
writeRun(dir, "r2", "rolled-back");
|
|
74
|
+
writeQueue(dir, [
|
|
75
|
+
{ runId: "r1", level: "approval", skill: "a", targets: [], proposalType: "improve", status: "applied", createdAt: NOW },
|
|
76
|
+
{ runId: "r2", level: "approval", skill: "a", targets: [], proposalType: "improve", status: "rolled-back", createdAt: NOW },
|
|
77
|
+
]);
|
|
78
|
+
const state = collectPanelState({ evolutionDir: dir, now: NOW });
|
|
79
|
+
assert.equal(state.lastAppliedAt, appliedAt);
|
|
80
|
+
assert.equal(state.appliedSamples, 2);
|
|
81
|
+
assert.equal(state.rollbackRate, 0.5);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("statusEntries:同一状态只发一次,dispatched 由调用方单独记", () => {
|
|
85
|
+
const seen = new Map<string, string>();
|
|
86
|
+
const runs = [
|
|
87
|
+
{ runId: "r1", status: "dispatched" },
|
|
88
|
+
{ runId: "r2", status: "reviewed" },
|
|
89
|
+
{ runId: "r3", status: "applied" },
|
|
90
|
+
{ runId: "r4", status: "rolled-back" },
|
|
91
|
+
{ runId: "r5", status: "rejected" },
|
|
92
|
+
];
|
|
93
|
+
const first = statusEntries(runs, seen, NOW);
|
|
94
|
+
assert.deepEqual(
|
|
95
|
+
first.map((entry) => [entry.runId, entry.kind]),
|
|
96
|
+
[
|
|
97
|
+
["r2", "reviewed"],
|
|
98
|
+
["r3", "applied"],
|
|
99
|
+
["r4", "rolled-back"],
|
|
100
|
+
["r5", "reviewed"],
|
|
101
|
+
],
|
|
102
|
+
);
|
|
103
|
+
assert.equal(first.find((entry) => entry.runId === "r5")?.detail, "rejected", "非 pass 的结论要带上原因");
|
|
104
|
+
assert.deepEqual(statusEntries(runs, seen, NOW), [], "同状态不重复发");
|
|
105
|
+
assert.deepEqual(
|
|
106
|
+
statusEntries([...runs, { runId: "r6", status: "applied" }], seen, NOW).map((entry) => entry.runId),
|
|
107
|
+
["r6"],
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("entryLines:/tree 条目单行、带短日期与动作", () => {
|
|
112
|
+
const lines = entryLines({ kind: "applied", runId: "run-1", at: NOW, detail: "approval · a" });
|
|
113
|
+
assert.equal(lines.length, 1);
|
|
114
|
+
assert.match(lines[0]!, /\[skill-evolve\] 采纳 run-1 · 11-08 · approval · a/);
|
|
115
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提案解析与校验单测(设计稿 九、十):正常 diff、超 40 行、越界路径、candidate frontmatter。
|
|
3
|
+
*
|
|
4
|
+
* 全部走临时 run 目录:校验函数本来就是「读盘 + 判定」,只测纯函数会漏掉 diff 文件名约定。
|
|
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 {
|
|
12
|
+
MAX_DIFF_LINES,
|
|
13
|
+
parseUnifiedDiff,
|
|
14
|
+
readProposalSummary,
|
|
15
|
+
validateCandidate,
|
|
16
|
+
validateProposal,
|
|
17
|
+
} from "../lib/proposal.ts";
|
|
18
|
+
|
|
19
|
+
const DIFF_OK = [
|
|
20
|
+
"--- a/.pi/skills/deploy-test/SKILL.md",
|
|
21
|
+
"+++ b/.pi/skills/deploy-test/SKILL.md",
|
|
22
|
+
"@@ -1,4 +1,5 @@",
|
|
23
|
+
" name: deploy-test",
|
|
24
|
+
"-description: 老的描述",
|
|
25
|
+
"+description: 部署验收、验收清单、测服部署",
|
|
26
|
+
"+路由:部署验收",
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
function runDir(files: Record<string, string>): string {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-proposal-"));
|
|
31
|
+
for (const [name, content] of Object.entries(files)) {
|
|
32
|
+
const file = path.join(dir, name);
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
fs.writeFileSync(file, content, "utf8");
|
|
35
|
+
}
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function proposalJson(over: Record<string, unknown> = {}): string {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
type: "improve",
|
|
42
|
+
skill: "deploy-test",
|
|
43
|
+
rationale: "description 缺「验收」路由词",
|
|
44
|
+
evidence: [1234],
|
|
45
|
+
diffFile: "02-proposal.diff",
|
|
46
|
+
...over,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("parseUnifiedDiff:统计增删行与文件路径", () => {
|
|
51
|
+
const parsed = parseUnifiedDiff(DIFF_OK);
|
|
52
|
+
assert.equal(parsed.ok, true);
|
|
53
|
+
assert.equal(parsed.added, 2);
|
|
54
|
+
assert.equal(parsed.removed, 1);
|
|
55
|
+
assert.deepEqual(parsed.files.map((item) => item.path), [".pi/skills/deploy-test/SKILL.md"]);
|
|
56
|
+
assert.equal(parsed.files[0].hunks, 1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("parseUnifiedDiff:空文本判空,不是 ok", () => {
|
|
60
|
+
const parsed = parseUnifiedDiff("");
|
|
61
|
+
assert.equal(parsed.ok, false);
|
|
62
|
+
assert.equal(parsed.reason, "empty");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("validateProposal:正常 diff 判 ok 并带增删行数", () => {
|
|
66
|
+
const dir = runDir({ "02-proposal.json": proposalJson(), "02-proposal.diff": DIFF_OK });
|
|
67
|
+
const check = validateProposal(dir);
|
|
68
|
+
assert.equal(check.status, "ok");
|
|
69
|
+
assert.equal(check.type, "improve");
|
|
70
|
+
assert.equal(check.skill, "deploy-test");
|
|
71
|
+
assert.equal(check.added, 2);
|
|
72
|
+
assert.equal(check.removed, 1);
|
|
73
|
+
assert.equal(check.file, "02-proposal.diff");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("validateProposal:没有 02-proposal.json 判 missing", () => {
|
|
77
|
+
const dir = runDir({});
|
|
78
|
+
assert.equal(validateProposal(dir).status, "missing");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("validateProposal:增删超过 40 行判 too_large(不拒绝生成,标给人工)", () => {
|
|
82
|
+
const long = [
|
|
83
|
+
"--- a/.pi/skills/deploy-test/SKILL.md",
|
|
84
|
+
"+++ b/.pi/skills/deploy-test/SKILL.md",
|
|
85
|
+
"@@ -1,60 +1,60 @@",
|
|
86
|
+
...Array.from({ length: MAX_DIFF_LINES }, (_, index) => `-旧行 ${index}`),
|
|
87
|
+
...Array.from({ length: MAX_DIFF_LINES }, (_, index) => `+新行 ${index}`),
|
|
88
|
+
].join("\n");
|
|
89
|
+
const dir = runDir({ "02-proposal.json": proposalJson(), "02-proposal.diff": long });
|
|
90
|
+
const check = validateProposal(dir);
|
|
91
|
+
assert.equal(check.status, "too_large");
|
|
92
|
+
assert.equal(check.added, MAX_DIFF_LINES);
|
|
93
|
+
assert.equal(check.removed, MAX_DIFF_LINES);
|
|
94
|
+
assert.match(check.detail, /转人工/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("validateProposal:diff 动了技能目录外的路径判 bad_path", () => {
|
|
98
|
+
const diff = [
|
|
99
|
+
"--- a/rust_server/src/x.rs",
|
|
100
|
+
"+++ b/rust_server/src/x.rs",
|
|
101
|
+
"@@ -1,1 +1,2 @@",
|
|
102
|
+
" fn main() {}",
|
|
103
|
+
"+// 顺手改一下",
|
|
104
|
+
].join("\n");
|
|
105
|
+
const dir = runDir({ "02-proposal.json": proposalJson(), "02-proposal.diff": diff });
|
|
106
|
+
const check = validateProposal(dir);
|
|
107
|
+
assert.equal(check.status, "bad_path");
|
|
108
|
+
assert.match(check.detail, /rust_server\/src\/x\.rs/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("validateProposal:create 缺 candidate 目录判 invalid_candidate", () => {
|
|
112
|
+
const dir = runDir({ "02-proposal.json": proposalJson({ type: "create", skill: "deploy-check" }) });
|
|
113
|
+
const check = validateProposal(dir);
|
|
114
|
+
assert.equal(check.status, "invalid_candidate");
|
|
115
|
+
assert.match(check.detail, /缺 SKILL\.md/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("validateProposal:create 的 candidate 合规判 ok", () => {
|
|
119
|
+
const dir = runDir({
|
|
120
|
+
"02-proposal.json": proposalJson({ type: "create", skill: "deploy-check" }),
|
|
121
|
+
"candidate/deploy-check/SKILL.md": "---\nname: deploy-check\ndescription: 部署后逐项验收\n---\n\n正文\n",
|
|
122
|
+
});
|
|
123
|
+
const check = validateProposal(dir);
|
|
124
|
+
assert.equal(check.status, "ok");
|
|
125
|
+
assert.equal(check.file, "candidate/deploy-check/SKILL.md");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("validateCandidate:name 不合规 / 缺 description / 与目录名不一致都判坏", () => {
|
|
129
|
+
const badName = runDir({ "SKILL.md": "---\nname: Deploy_Check\ndescription: x\n---\n" });
|
|
130
|
+
assert.match(validateCandidate(badName).detail, /name 不合规/);
|
|
131
|
+
|
|
132
|
+
const noDesc = runDir({ "SKILL.md": "---\nname: deploy-check\n---\n" });
|
|
133
|
+
assert.equal(validateCandidate(noDesc).ok, false);
|
|
134
|
+
assert.match(validateCandidate(noDesc).detail, /缺 description/);
|
|
135
|
+
|
|
136
|
+
const mismatch = runDir({ "SKILL.md": "---\nname: other-name\ndescription: x\n---\n" });
|
|
137
|
+
assert.match(validateCandidate(mismatch).detail, /不一致/);
|
|
138
|
+
|
|
139
|
+
const okDir = path.join(runDir({}), "deploy-check");
|
|
140
|
+
fs.mkdirSync(okDir, { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(okDir, "SKILL.md"), "---\nname: deploy-check\ndescription: 部署后逐项验收\n---\n", "utf8");
|
|
142
|
+
const ok = validateCandidate(okDir);
|
|
143
|
+
assert.equal(ok.ok, true);
|
|
144
|
+
assert.equal(ok.name, "deploy-check");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("readProposalSummary:没提案返回 undefined,有提案返回摘要(报告只显示不应用)", () => {
|
|
148
|
+
assert.equal(readProposalSummary(runDir({})), undefined);
|
|
149
|
+
const dir = runDir({ "02-proposal.json": proposalJson(), "02-proposal.diff": DIFF_OK });
|
|
150
|
+
const summary = readProposalSummary(dir);
|
|
151
|
+
assert.deepEqual(summary, {
|
|
152
|
+
type: "improve",
|
|
153
|
+
skill: "deploy-test",
|
|
154
|
+
status: "ok",
|
|
155
|
+
added: 2,
|
|
156
|
+
removed: 1,
|
|
157
|
+
file: "02-proposal.diff",
|
|
158
|
+
});
|
|
159
|
+
});
|