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,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skill-audit` 文本单测(零依赖,node --test)。
|
|
3
|
+
*
|
|
4
|
+
* 运行:
|
|
5
|
+
* cd .pi/extensions/skill-evolve
|
|
6
|
+
* node --experimental-strip-types --test test/audit.test.ts
|
|
7
|
+
*
|
|
8
|
+
* 覆盖:六类都出现(0 也要列)、attributed 事件按次数累计、
|
|
9
|
+
* 无归因记录的历史 missed 走现算并计入来源、硬禁令 P0 列表、加载率注解文案。
|
|
10
|
+
*/
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { fingerprintOf } from "../lib/baseline.ts";
|
|
14
|
+
|
|
15
|
+
import { AUDIT_LOAD_RATE_NOTE, activeToolsFromEvents, auditText, buildAuditModel, exposedFromEvents } from "../lib/audit.ts";
|
|
16
|
+
import type { TelemetryEvent } from "../lib/telemetry.ts";
|
|
17
|
+
|
|
18
|
+
function event(partial: Partial<TelemetryEvent> & { kind: TelemetryEvent["kind"] }): TelemetryEvent {
|
|
19
|
+
return { ts: 1_700_000_000_000, iso: "2023-11-14T22:13:20.000Z", ...partial } as TelemetryEvent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const EVENTS: TelemetryEvent[] = [
|
|
23
|
+
event({
|
|
24
|
+
kind: "advertised",
|
|
25
|
+
sessionId: "s1",
|
|
26
|
+
skills: [
|
|
27
|
+
{ name: "alpha", filePath: "/r/.pi/skills/alpha/SKILL.md", disabled: false, source: "project" },
|
|
28
|
+
{ name: "beta", filePath: "/r/.pi/skills/beta/SKILL.md", disabled: false, source: "project" },
|
|
29
|
+
],
|
|
30
|
+
activeTools: ["read", "bash"],
|
|
31
|
+
}),
|
|
32
|
+
event({ kind: "loaded", sessionId: "s1", skill: "alpha" }),
|
|
33
|
+
event({ kind: "missed", sessionId: "s1", skill: "beta" }),
|
|
34
|
+
event({ kind: "missed", sessionId: "s1", skill: "beta" }),
|
|
35
|
+
event({ kind: "attributed", sessionId: "s1", skill: "beta", category: "B", reason: "frontmatter 坏" }),
|
|
36
|
+
event({ kind: "attributed", sessionId: "s1", skill: "beta", category: "B", reason: "frontmatter 坏" }),
|
|
37
|
+
// 历史数据(M2 之前):missed 有记录,但没有任何 attributed
|
|
38
|
+
event({ kind: "missed", sessionId: "s0", skill: "gamma" }),
|
|
39
|
+
event({ kind: "missed", sessionId: "s0", skill: "gamma" }),
|
|
40
|
+
event({ kind: "missed", sessionId: "s0", skill: "gamma" }),
|
|
41
|
+
event({ kind: "missed", sessionId: "s0", skill: "delta" }),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
test("buildAuditModel:attributed 按次数累计,历史 missed 走现算", () => {
|
|
45
|
+
const model = buildAuditModel({
|
|
46
|
+
days: 7,
|
|
47
|
+
events: EVENTS,
|
|
48
|
+
classify: (skill) => (skill === "gamma" ? "E" : "semantic"),
|
|
49
|
+
conflicts: [],
|
|
50
|
+
invalidRules: [],
|
|
51
|
+
});
|
|
52
|
+
const byCategory = new Map(model.rows.map((row) => [row.category, row.count]));
|
|
53
|
+
assert.equal(byCategory.get("B"), 2);
|
|
54
|
+
assert.equal(byCategory.get("E"), 3);
|
|
55
|
+
assert.equal(byCategory.get("semantic"), 1);
|
|
56
|
+
assert.equal(model.fromTelemetry, 2);
|
|
57
|
+
assert.equal(model.fromLive, 4);
|
|
58
|
+
assert.deepEqual(model.unattributed, []);
|
|
59
|
+
assert.equal(model.summary.advertised, 2);
|
|
60
|
+
assert.equal(model.summary.loaded, 1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("buildAuditModel:不给 classify 时历史 missed 进「未归因」,不硬编类别", () => {
|
|
64
|
+
const model = buildAuditModel({ days: 7, events: EVENTS, conflicts: [], invalidRules: [] });
|
|
65
|
+
assert.deepEqual(model.unattributed, [{ skill: "gamma", count: 3 }, { skill: "delta", count: 1 }]);
|
|
66
|
+
const total = model.rows.reduce((sum, row) => sum + row.count, 0);
|
|
67
|
+
assert.equal(total, 2); // 只有 attributed 的 2 条
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("auditText:六类全列、P0 列表、行号、加载率注解都在", () => {
|
|
71
|
+
const text = auditText(
|
|
72
|
+
buildAuditModel({
|
|
73
|
+
days: 7,
|
|
74
|
+
events: EVENTS,
|
|
75
|
+
classify: (skill) => (skill === "gamma" ? "E" : "semantic"),
|
|
76
|
+
conflicts: [
|
|
77
|
+
{ ruleId: "rg-over-find", level: "P0", skill: "deploy-test", line: 129, quote: "grep -rho 对账" },
|
|
78
|
+
],
|
|
79
|
+
invalidRules: ["broken-rule: 正则编译失败"],
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
for (const category of ["A", "B", "C", "D", "E", "semantic"]) {
|
|
83
|
+
assert.match(text, new RegExp(`^\\s+${category === "semantic" ? "semantic" : category}\\s`, "m"), `缺类别 ${category}`);
|
|
84
|
+
}
|
|
85
|
+
assert.match(text, /加载率 50\.0%/);
|
|
86
|
+
assert.match(text, /B\s+frontmatter 坏\s+2/);
|
|
87
|
+
assert.match(text, /rg-over-find\s+deploy-test:129\s+P0/);
|
|
88
|
+
assert.match(text, /规则不可用:/);
|
|
89
|
+
assert.ok(text.includes(AUDIT_LOAD_RATE_NOTE));
|
|
90
|
+
assert.match(text, /加载率低 ≠ 技能没用/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("auditText:无冲突、无数据时也要有明确结论而不是空白", () => {
|
|
94
|
+
const text = auditText(buildAuditModel({ days: 7, events: [], conflicts: [], invalidRules: [] }));
|
|
95
|
+
assert.match(text, /P0 命中 0 条/);
|
|
96
|
+
assert.match(text, /无曝光数据/);
|
|
97
|
+
assert.match(text, /没有可归因的未命中/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("exposedFromEvents / activeToolsFromEvents 取最近一次 advertised", () => {
|
|
101
|
+
const events = [
|
|
102
|
+
...EVENTS,
|
|
103
|
+
event({
|
|
104
|
+
kind: "advertised",
|
|
105
|
+
sessionId: "s2",
|
|
106
|
+
skills: [{ name: "omega", filePath: "", disabled: true, source: "embedded" }],
|
|
107
|
+
activeTools: ["edit"],
|
|
108
|
+
}),
|
|
109
|
+
];
|
|
110
|
+
assert.deepEqual(exposedFromEvents(events), [{ name: "omega", filePath: "", disabled: true }]);
|
|
111
|
+
assert.deepEqual(activeToolsFromEvents(events), ["edit"]);
|
|
112
|
+
// 老数据没有 activeTools 字段:返回空数组,调用方据此不判 E
|
|
113
|
+
assert.deepEqual(activeToolsFromEvents(EVENTS), ["read", "bash"]);
|
|
114
|
+
assert.deepEqual(activeToolsFromEvents([event({ kind: "missed", skill: "x" })]), []);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("auditText:存量命中与新增命中分两段,只有新增算失败信号", () => {
|
|
118
|
+
const stockHit = { ruleId: "rg-over-find", level: "P0", skill: "deploy-test", line: 129, quote: "老问题" };
|
|
119
|
+
const addedHit = { ruleId: "file-size-limit", level: "P0", skill: "big-skill", line: 12, quote: "600 行" };
|
|
120
|
+
const text = auditText(
|
|
121
|
+
buildAuditModel({
|
|
122
|
+
days: 7,
|
|
123
|
+
events: [],
|
|
124
|
+
conflicts: [stockHit, addedHit],
|
|
125
|
+
knownFingerprints: new Set([fingerprintOf(stockHit)]),
|
|
126
|
+
invalidRules: [],
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
assert.match(text, /P0 命中 2 条:存量(baseline)1 条 · 新增 1 条/);
|
|
130
|
+
assert.match(text, /新增\s+file-size-limit\s+big-skill:12\s+P0/);
|
|
131
|
+
assert.match(text, /存量\s+rg-over-find\s+deploy-test:129\s+P0/);
|
|
132
|
+
assert.match(text, /确认无害后跑 \/skill-evolve baseline 接受/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("auditText:全是存量时不出现「新增命中」提示", () => {
|
|
136
|
+
const hits = [
|
|
137
|
+
{ ruleId: "rg-over-find", level: "P0", skill: "deploy-test", line: 129, quote: "老问题一" },
|
|
138
|
+
{ ruleId: "rg-over-find", level: "P0", skill: "deploy-test", line: 196, quote: "老问题二" },
|
|
139
|
+
];
|
|
140
|
+
const text = auditText(
|
|
141
|
+
buildAuditModel({
|
|
142
|
+
days: 7,
|
|
143
|
+
events: [],
|
|
144
|
+
conflicts: hits,
|
|
145
|
+
knownFingerprints: new Set(hits.map((hit) => fingerprintOf(hit))),
|
|
146
|
+
invalidRules: [],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
assert.match(text, /P0 命中 2 条:存量(baseline)2 条 · 新增 0 条/);
|
|
150
|
+
assert.doesNotMatch(text, /新增命中:/);
|
|
151
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 存量债基线单测(设计稿 10.4):指纹不含行号、首次审计 bootstrap、新命中算新增、--reset 清空。
|
|
3
|
+
*
|
|
4
|
+
* 全部用临时目录 + directQueue(直写)跑真实 IO —— 这段逻辑的坑在「文件在不在、指纹对不对」,
|
|
5
|
+
* 只测纯函数测不出 bootstrap 到底有没有落盘。
|
|
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 {
|
|
13
|
+
buildBaseline,
|
|
14
|
+
createBaselineGate,
|
|
15
|
+
fingerprintOf,
|
|
16
|
+
fingerprintsOf,
|
|
17
|
+
readBaseline,
|
|
18
|
+
saveBaseline,
|
|
19
|
+
splitHits,
|
|
20
|
+
} from "../lib/baseline.ts";
|
|
21
|
+
import type { HardRuleHit } from "../lib/hard-rules.ts";
|
|
22
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
23
|
+
|
|
24
|
+
const NOW = 1_760_000_000_000;
|
|
25
|
+
|
|
26
|
+
function hit(over: Partial<HardRuleHit> = {}): HardRuleHit {
|
|
27
|
+
return {
|
|
28
|
+
ruleId: "rg-over-find",
|
|
29
|
+
level: "P0",
|
|
30
|
+
skill: "deploy-test",
|
|
31
|
+
line: 129,
|
|
32
|
+
quote: "grep -rho 'env::var(\"[A-Z_]*\"' rust_server/src",
|
|
33
|
+
...over,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function tempDir(): string {
|
|
38
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-baseline-"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function gateFor(dir: string, hits: HardRuleHit[]) {
|
|
42
|
+
const file = path.join(dir, "baseline.json");
|
|
43
|
+
return {
|
|
44
|
+
file,
|
|
45
|
+
gate: createBaselineGate({ file, hits: () => hits, now: () => NOW, queue: directQueue, log: () => {} }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("fingerprintOf:行号不进指纹,quote 只算前 120 字", () => {
|
|
50
|
+
const base = hit();
|
|
51
|
+
assert.equal(fingerprintOf(base), fingerprintOf(hit({ line: 999 })));
|
|
52
|
+
const longQuote = "长".repeat(200);
|
|
53
|
+
assert.equal(fingerprintOf(hit({ quote: longQuote })), fingerprintOf(hit({ quote: `${longQuote}追加字符` })));
|
|
54
|
+
assert.notEqual(fingerprintOf(base), fingerprintOf(hit({ skill: "other" })));
|
|
55
|
+
assert.notEqual(fingerprintOf(base), fingerprintOf(hit({ ruleId: "file-size-limit" })));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("首次审计:文件不存在时 bootstrap,把当时全部命中都当存量并落盘", async () => {
|
|
59
|
+
const dir = tempDir();
|
|
60
|
+
const { file, gate } = gateFor(dir, [hit(), hit({ line: 196, quote: "ssh x \"grep -rl '文案' dist\"" })]);
|
|
61
|
+
|
|
62
|
+
const first = gate.baseline();
|
|
63
|
+
assert.equal(first.bootstrap, true);
|
|
64
|
+
assert.equal(first.violations.length, 2);
|
|
65
|
+
await gate.settled();
|
|
66
|
+
|
|
67
|
+
const onDisk = readBaseline(file);
|
|
68
|
+
assert.ok(onDisk);
|
|
69
|
+
assert.equal(onDisk?.bootstrap, true);
|
|
70
|
+
assert.equal(onDisk?.violations.length, 2);
|
|
71
|
+
|
|
72
|
+
// 第一次审计后:两条全是存量,新增 0
|
|
73
|
+
const { stock, added } = splitHits(
|
|
74
|
+
[hit(), hit({ line: 196, quote: "ssh x \"grep -rl '文案' dist\"" })],
|
|
75
|
+
fingerprintsOf(first),
|
|
76
|
+
);
|
|
77
|
+
assert.equal(stock.length, 2);
|
|
78
|
+
assert.equal(added.length, 0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("新命中算新增:基线里已有的算存量,多出来的一条算新增", async () => {
|
|
82
|
+
const dir = tempDir();
|
|
83
|
+
const { file, gate } = gateFor(dir, [hit()]);
|
|
84
|
+
const baseline = buildBaseline([hit()], NOW, false);
|
|
85
|
+
await saveBaseline(file, baseline, directQueue);
|
|
86
|
+
|
|
87
|
+
const current = [hit(), hit({ ruleId: "file-size-limit", quote: "12.3K 行" })];
|
|
88
|
+
const { stock, added } = splitHits(current, fingerprintsOf(gate.baseline()));
|
|
89
|
+
assert.deepEqual(stock.map((item) => item.ruleId), ["rg-over-find"]);
|
|
90
|
+
assert.deepEqual(added.map((item) => item.ruleId), ["file-size-limit"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("行号漂移不算新增:同一条问题从 129 行挪到 140 行仍是存量", async () => {
|
|
94
|
+
const dir = tempDir();
|
|
95
|
+
const { gate, file } = gateFor(dir, [hit({ line: 140 })]);
|
|
96
|
+
await saveBaseline(file, buildBaseline([hit({ line: 129 })], NOW, false), directQueue);
|
|
97
|
+
|
|
98
|
+
assert.equal(gate.isStock(hit({ line: 140 })), true);
|
|
99
|
+
assert.equal(splitHits([hit({ line: 140 })], fingerprintsOf(gate.baseline())).added.length, 0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("acceptAll:把当前全部命中写进基线,bootstrap 标 false", async () => {
|
|
103
|
+
const dir = tempDir();
|
|
104
|
+
const { gate, file } = gateFor(dir, [hit(), hit({ quote: "新增的一条" })]);
|
|
105
|
+
|
|
106
|
+
const message = await gate.acceptAll();
|
|
107
|
+
assert.match(message, /已接受 2 条/);
|
|
108
|
+
const onDisk = readBaseline(file);
|
|
109
|
+
assert.equal(onDisk?.bootstrap, false);
|
|
110
|
+
assert.equal(onDisk?.violations.length, 2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("reset:清空清单但不删文件(删了下次审计又会自动 bootstrap 回存量)", async () => {
|
|
114
|
+
const dir = tempDir();
|
|
115
|
+
const { gate, file } = gateFor(dir, [hit()]);
|
|
116
|
+
await saveBaseline(file, buildBaseline([hit()], NOW, false), directQueue);
|
|
117
|
+
|
|
118
|
+
const message = await gate.reset();
|
|
119
|
+
assert.match(message, /已清空存量基线/);
|
|
120
|
+
const onDisk = readBaseline(file);
|
|
121
|
+
assert.ok(onDisk, "reset 之后文件仍在");
|
|
122
|
+
assert.deepEqual(onDisk?.violations, []);
|
|
123
|
+
assert.equal(splitHits([hit()], fingerprintsOf(gate.baseline())).added.length, 1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("基线文件坏了当不存在处理,不抛", () => {
|
|
127
|
+
const dir = tempDir();
|
|
128
|
+
const file = path.join(dir, "baseline.json");
|
|
129
|
+
fs.writeFileSync(file, "{ 坏 json", "utf8");
|
|
130
|
+
const { gate } = gateFor(dir, [hit()]);
|
|
131
|
+
assert.equal(gate.baseline().bootstrap, true);
|
|
132
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分级采纳的代码判定单测(13.1):只看 diff 真改了什么,不看模型写的 action。
|
|
3
|
+
*
|
|
4
|
+
* 重点是边界:只改 description 才能 auto;同时碰正文就退成 approval;
|
|
5
|
+
* 路径越界 / 删文件 / 改 frontmatter 语义字段一律 forbidden。
|
|
6
|
+
*/
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { classifyAction, classifyChange, parseSections } from "../lib/classify-action.ts";
|
|
10
|
+
|
|
11
|
+
function diff(path: string, body: string, oldPath = path, created = false): string {
|
|
12
|
+
const head = `--- ${created ? "/dev/null" : `a/${oldPath}`}\n+++ b/${path}\n@@ -1,3 +1,3 @@\n`;
|
|
13
|
+
return `${head}${body}\n`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SKILL = ".pi/skills/deploy-test/SKILL.md";
|
|
17
|
+
|
|
18
|
+
test("只改 description 一行 → auto", () => {
|
|
19
|
+
const result = classifyChange({
|
|
20
|
+
diffText: diff(SKILL, "-description: 部署测试服\n+description: 部署测服并给出验收清单"),
|
|
21
|
+
proposalType: "improve",
|
|
22
|
+
skill: "deploy-test",
|
|
23
|
+
});
|
|
24
|
+
assert.equal(result.level, "auto");
|
|
25
|
+
assert.deepEqual(result.targets, [SKILL]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("description 改完再碰一条正文条款 → approval(不能只因为有一行 description 就放过)", () => {
|
|
29
|
+
const result = classifyChange({
|
|
30
|
+
diffText: diff(SKILL, "-description: 部署测试服\n+description: 部署测服并给出验收清单\n-禁止 pkill -f\n+禁止用 pkill -f"),
|
|
31
|
+
proposalType: "improve",
|
|
32
|
+
skill: "deploy-test",
|
|
33
|
+
});
|
|
34
|
+
assert.equal(result.level, "approval");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("description 续行(缩进 ≥2)与纯空白改动仍算 auto", () => {
|
|
38
|
+
const result = classifyChange({
|
|
39
|
+
diffText: diff(SKILL, "-description: 部署测试服\n+description: 部署测试服,\n+ 并在成功后给出验收清单\n- \n+ "),
|
|
40
|
+
proposalType: "improve",
|
|
41
|
+
skill: "deploy-test",
|
|
42
|
+
});
|
|
43
|
+
assert.equal(result.level, "auto");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("路径越界(技能目录下的非 SKILL.md)→ forbidden", () => {
|
|
47
|
+
const result = classifyChange({
|
|
48
|
+
diffText: diff(".pi/skills/deploy-test/notes.md", "-旧\n+新"),
|
|
49
|
+
proposalType: "improve",
|
|
50
|
+
skill: "deploy-test",
|
|
51
|
+
});
|
|
52
|
+
assert.equal(result.level, "forbidden");
|
|
53
|
+
assert.match(result.reasons[0] ?? "", /路径越界/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("删除技能文件(+++ /dev/null)→ forbidden", () => {
|
|
57
|
+
const text = `--- a/${SKILL}\n+++ /dev/null\n@@ -1,1 +0,0 @@\n--description: x\n`;
|
|
58
|
+
assert.equal(classifyChange({ diffText: text, proposalType: "improve", skill: "deploy-test" }).level, "forbidden");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("改 frontmatter 的 name / allowed-tools / disable-model-invocation → forbidden", () => {
|
|
62
|
+
for (const field of ["name: 别的名字", "allowed-tools: bash", "disable-model-invocation: true"]) {
|
|
63
|
+
const result = classifyChange({
|
|
64
|
+
diffText: diff(SKILL, `-旧值\n+${field}`),
|
|
65
|
+
proposalType: "improve",
|
|
66
|
+
skill: "deploy-test",
|
|
67
|
+
});
|
|
68
|
+
assert.equal(result.level, "forbidden", `${field} 应 forbidden`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("动插件自身目录 → forbidden", () => {
|
|
73
|
+
const result = classifyChange({
|
|
74
|
+
diffText: diff(".pi/extensions/skill-evolve/prompts/复盘.md", "-旧\n+新"),
|
|
75
|
+
proposalType: "improve",
|
|
76
|
+
skill: "skill-evolve",
|
|
77
|
+
});
|
|
78
|
+
assert.equal(result.level, "forbidden");
|
|
79
|
+
assert.match(result.reasons[0] ?? "", /插件自身/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("prune(删技能提案)不论 diff 内容都 forbidden:插件只出清单不真删", () => {
|
|
83
|
+
const result = classifyChange({ diffText: diff(SKILL, "-旧\n+新"), proposalType: "prune", skill: "deploy-test" });
|
|
84
|
+
assert.equal(result.level, "forbidden");
|
|
85
|
+
assert.match(result.reasons[0] ?? "", /prune/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("新建 .cursor 路由 stub → auto", () => {
|
|
89
|
+
const text = `--- /dev/null\n+++ b/.cursor/skills/new-skill/SKILL.md\n@@ -0,0 +1,3 @@\n+唯一 SSOT:.pi/skills/new-skill/SKILL.md\n+动手前先 Read 它\n+# 路由\n`;
|
|
90
|
+
const result = classifyChange({ diffText: text, proposalType: "create", skill: "new-skill" });
|
|
91
|
+
assert.equal(result.level, "auto");
|
|
92
|
+
assert.deepEqual(result.targets, [".cursor/skills/new-skill/SKILL.md"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("AGENTS.md 索引表只增一行 → auto;增两行 → approval", () => {
|
|
96
|
+
const one = diff("AGENTS.md", "+- `new-skill` —— `.pi/skills/new-skill/SKILL.md`");
|
|
97
|
+
assert.equal(classifyChange({ diffText: one, proposalType: "create", skill: "new-skill" }).level, "auto");
|
|
98
|
+
const two = diff("AGENTS.md", "+- `new-skill` —— `.pi/skills/new-skill/SKILL.md`\n+- `other` —— `.pi/skills/other/SKILL.md`");
|
|
99
|
+
assert.equal(classifyChange({ diffText: two, proposalType: "create", skill: "new-skill" }).level, "approval");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("新增技能正文(.pi/skills/<新名>/SKILL.md 纯新建)→ approval", () => {
|
|
103
|
+
const text = `--- /dev/null\n+++ b/.pi/skills/new-skill/SKILL.md\n@@ -0,0 +1,2 @@\n+---\n+description: 新技能\n`;
|
|
104
|
+
assert.equal(classifyChange({ diffText: text, proposalType: "create", skill: "new-skill" }).level, "approval");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("空 diff / 无增删行 → approval(判不动就不许自动写盘)", () => {
|
|
108
|
+
assert.equal(classifyChange({ diffText: "", proposalType: "improve", skill: "x" }).level, "approval");
|
|
109
|
+
const onlyContext = `--- a/${SKILL}\n+++ b/${SKILL}\n@@ -1,1 +1,1 @@\n 只有上下文\n`;
|
|
110
|
+
assert.equal(classifyChange({ diffText: onlyContext, proposalType: "improve", skill: "x" }).level, "approval");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("多文件 diff 逐段判:技能文件 + 同 skill 的 .cursor stub 都只改描述/新建 → 仍 auto 需逐段合规", () => {
|
|
114
|
+
const text =
|
|
115
|
+
`--- a/${SKILL}\n+++ b/${SKILL}\n@@ -1,1 +1,1 @@\n-description: 旧\n+description: 新\n` +
|
|
116
|
+
`--- /dev/null\n+++ b/.cursor/skills/deploy-test/SKILL.md\n@@ -0,0 +1,1 @@\n+唯一 SSOT\n`;
|
|
117
|
+
const result = classifyChange({ diffText: text, proposalType: "improve", skill: "deploy-test" });
|
|
118
|
+
assert.equal(result.level, "auto");
|
|
119
|
+
assert.deepEqual(result.targets, [SKILL, ".cursor/skills/deploy-test/SKILL.md"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("parseSections 跳过头部元信息与 @@ / \\ 行,保留改动的符号", () => {
|
|
123
|
+
const text = `diff --git a/${SKILL} b/${SKILL}\nindex 111..222 100644\n--- a/${SKILL}\n+++ b/${SKILL}\n@@ -1,2 +1,2 @@\n-old\n+new\n\\ No newline\n`;
|
|
124
|
+
const sections = parseSections(text);
|
|
125
|
+
assert.equal(sections.length, 1);
|
|
126
|
+
assert.deepEqual(sections[0]?.lines.map((line) => line.sign), ["-", "+"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("classifyAction 兼容旧调用面(只看级别)", () => {
|
|
130
|
+
assert.equal(classifyAction(diff(SKILL, "-description: 旧\n+description: 新"), "improve"), "auto");
|
|
131
|
+
assert.equal(classifyAction(diff(SKILL, "-正文\n+正文2"), "improve"), "approval");
|
|
132
|
+
assert.equal(classifyAction(diff(SKILL, "-a\n+b"), "prune"), "forbidden");
|
|
133
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /skill-evolve 子命令路由单测:只验证「参数 → 展示文本」,用假 Evolution 对象隔离 lib/evolution。
|
|
3
|
+
*/
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { matchSubcommand, parseWebPort, runCommand } from "../lib/command.ts";
|
|
7
|
+
import type { Evolution } from "../lib/evolution.ts";
|
|
8
|
+
|
|
9
|
+
const TARGET = { sessionId: "sess-1", sessionFile: "/tmp/session.jsonl" };
|
|
10
|
+
|
|
11
|
+
function fakeEvolution(calls: string[]): Evolution {
|
|
12
|
+
const stub = {
|
|
13
|
+
reportText: (days: number) => {
|
|
14
|
+
calls.push(`report:${days}`);
|
|
15
|
+
return `报告 ${days} 天`;
|
|
16
|
+
},
|
|
17
|
+
statusText: () => {
|
|
18
|
+
calls.push("status");
|
|
19
|
+
return "状态文本";
|
|
20
|
+
},
|
|
21
|
+
setEnabled: (enabled: boolean) => {
|
|
22
|
+
calls.push(`enabled:${enabled}`);
|
|
23
|
+
return `总开关:${enabled ? "开" : "关"}`;
|
|
24
|
+
},
|
|
25
|
+
setFlag: (field: string, value: boolean) => {
|
|
26
|
+
calls.push(`flag:${field}=${value}`);
|
|
27
|
+
return `开关:${field}=${value}`;
|
|
28
|
+
},
|
|
29
|
+
runNow: (target: { sessionId: string; sessionFile?: string }) => {
|
|
30
|
+
calls.push(`run:${target.sessionId}`);
|
|
31
|
+
return `已派生复盘:${target.sessionId}`;
|
|
32
|
+
},
|
|
33
|
+
acceptBaseline: (mode: "accept" | "reset") => {
|
|
34
|
+
calls.push(`baseline:${mode}`);
|
|
35
|
+
return `基线:${mode}`;
|
|
36
|
+
},
|
|
37
|
+
flush: async () => {},
|
|
38
|
+
model: {
|
|
39
|
+
config: () => ({ propose: "inherit", review: "inherit" }),
|
|
40
|
+
reviewModel: () => "inherit",
|
|
41
|
+
setModel: async (kind: string, spec: string) => {
|
|
42
|
+
calls.push(`model:${kind}=${spec}`);
|
|
43
|
+
return `已设 ${kind}=${spec}`;
|
|
44
|
+
},
|
|
45
|
+
selftestText: async () => {
|
|
46
|
+
calls.push("selftest");
|
|
47
|
+
return "自检:通过";
|
|
48
|
+
},
|
|
49
|
+
selftestFailed: () => false,
|
|
50
|
+
lastSelftest: () => undefined,
|
|
51
|
+
},
|
|
52
|
+
apply: {
|
|
53
|
+
sessionStart: () => {},
|
|
54
|
+
settle: () => {},
|
|
55
|
+
approve: async (runId: string, prune: boolean) => {
|
|
56
|
+
calls.push(`approve:${runId}${prune ? ":prune" : ""}`);
|
|
57
|
+
return `采纳 ${runId}`;
|
|
58
|
+
},
|
|
59
|
+
queueText: () => {
|
|
60
|
+
calls.push("queue");
|
|
61
|
+
return "队列文本";
|
|
62
|
+
},
|
|
63
|
+
autoApplyPending: async () => 0,
|
|
64
|
+
enqueueReady: async () => 0,
|
|
65
|
+
},
|
|
66
|
+
observe: {
|
|
67
|
+
refresh: () => ({ reviewing: 0, staleCount: 0, pendingApprovals: 0, lastAppliedAt: null, rollbackRate: 0, appliedSamples: 0 }),
|
|
68
|
+
noteDispatched: () => {},
|
|
69
|
+
weekReportText: async () => {
|
|
70
|
+
calls.push("week");
|
|
71
|
+
return "周报文本";
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return stub as unknown as Evolution;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("补全:空前缀给全量,命中前缀只给命中项,乱前缀也给全量", () => {
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
matchSubcommand("").map((item) => item.value),
|
|
81
|
+
["status", "report", "run", "baseline", "selftest", "approve", "queue", "web", "config", "collect", "review", "on", "off"],
|
|
82
|
+
);
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
matchSubcommand("re").map((item) => item.value),
|
|
85
|
+
["report", "review"],
|
|
86
|
+
);
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
matchSubcommand("co").map((item) => item.value),
|
|
89
|
+
["config", "collect"],
|
|
90
|
+
);
|
|
91
|
+
assert.deepEqual(
|
|
92
|
+
matchSubcommand("qu").map((item) => item.value),
|
|
93
|
+
["queue"],
|
|
94
|
+
);
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
matchSubcommand("ap").map((item) => item.value),
|
|
97
|
+
["approve"],
|
|
98
|
+
);
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
matchSubcommand("o").map((item) => item.value),
|
|
101
|
+
["on", "off"],
|
|
102
|
+
);
|
|
103
|
+
assert.deepEqual(
|
|
104
|
+
matchSubcommand("b").map((item) => item.value),
|
|
105
|
+
["baseline"],
|
|
106
|
+
);
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
matchSubcommand("se").map((item) => item.value),
|
|
109
|
+
["selftest"],
|
|
110
|
+
);
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
matchSubcommand("zzz").map((item) => item.value),
|
|
113
|
+
["status", "report", "run", "baseline", "selftest", "approve", "queue", "web", "config", "collect", "review", "on", "off"],
|
|
114
|
+
);
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
matchSubcommand("w").map((item) => item.value),
|
|
117
|
+
["web"],
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("空参数默认走 status", async () => {
|
|
122
|
+
const calls: string[] = [];
|
|
123
|
+
assert.equal(await runCommand("", fakeEvolution(calls), TARGET), "状态文本");
|
|
124
|
+
assert.deepEqual(calls, ["status"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("report 带天数用天数,非法天数回落 7 天", async () => {
|
|
128
|
+
const calls: string[] = [];
|
|
129
|
+
const evo = fakeEvolution(calls);
|
|
130
|
+
assert.equal(await runCommand("report 3", evo, TARGET), "报告 3 天");
|
|
131
|
+
assert.equal(await runCommand("report abc", evo, TARGET), "报告 7 天");
|
|
132
|
+
assert.equal(await runCommand("report 0", evo, TARGET), "报告 7 天");
|
|
133
|
+
assert.equal(await runCommand("report", evo, TARGET), "报告 7 天");
|
|
134
|
+
assert.deepEqual(calls, ["report:3", "report:7", "report:7", "report:7"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("on / off 透传布尔", async () => {
|
|
138
|
+
const calls: string[] = [];
|
|
139
|
+
const evo = fakeEvolution(calls);
|
|
140
|
+
assert.equal(await runCommand("on", evo, TARGET), "总开关:开");
|
|
141
|
+
assert.equal(await runCommand("off", evo, TARGET), "总开关:关");
|
|
142
|
+
assert.deepEqual(calls, ["enabled:true", "enabled:false"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("run 子命令把手机会话透传给 runNow", async () => {
|
|
146
|
+
const calls: string[] = [];
|
|
147
|
+
assert.equal(await runCommand("run", fakeEvolution(calls), TARGET), "已派生复盘:sess-1");
|
|
148
|
+
assert.deepEqual(calls, ["run:sess-1"]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("baseline 子命令:默认接受,--reset 清空", async () => {
|
|
152
|
+
const calls: string[] = [];
|
|
153
|
+
const evo = fakeEvolution(calls);
|
|
154
|
+
assert.equal(await runCommand("baseline", evo, TARGET), "基线:accept");
|
|
155
|
+
assert.equal(await runCommand("baseline --reset", evo, TARGET), "基线:reset");
|
|
156
|
+
assert.deepEqual(calls, ["baseline:accept", "baseline:reset"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("selftest 走 model.selftestText", async () => {
|
|
160
|
+
const calls: string[] = [];
|
|
161
|
+
assert.equal(await runCommand("selftest", fakeEvolution(calls), TARGET), "自检:通过");
|
|
162
|
+
assert.deepEqual(calls, ["selftest"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("非 TUI 的 config 子命令打印当前值而不改配置", async () => {
|
|
166
|
+
const calls: string[] = [];
|
|
167
|
+
const text = await runCommand("config", fakeEvolution(calls), TARGET);
|
|
168
|
+
assert.match(text, /无法交互选择/);
|
|
169
|
+
assert.deepEqual(calls, []);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("queue 子命令列采纳队列", async () => {
|
|
173
|
+
const calls: string[] = [];
|
|
174
|
+
assert.equal(await runCommand("queue", fakeEvolution(calls), TARGET), "队列文本");
|
|
175
|
+
assert.deepEqual(calls, ["queue"]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("approve 传 runId;带 --prune 时标记 prune", async () => {
|
|
179
|
+
const calls: string[] = [];
|
|
180
|
+
const evo = fakeEvolution(calls);
|
|
181
|
+
assert.equal(await runCommand("approve run-abc", evo, TARGET), "采纳 run-abc");
|
|
182
|
+
assert.equal(await runCommand("approve run-abc --prune", evo, TARGET), "采纳 run-abc");
|
|
183
|
+
assert.deepEqual(calls, ["approve:run-abc", "approve:run-abc:prune"]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("approve 缺 runId 时仍走 approve(由实现给用法串)", async () => {
|
|
187
|
+
const calls: string[] = [];
|
|
188
|
+
await runCommand("approve", fakeEvolution(calls), TARGET);
|
|
189
|
+
assert.deepEqual(calls, ["approve:"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("未知子命令返回用法串且不碰 Evolution", async () => {
|
|
193
|
+
const calls: string[] = [];
|
|
194
|
+
const text = await runCommand("propose", fakeEvolution(calls), TARGET);
|
|
195
|
+
assert.match(text, /未知子命令:propose/);
|
|
196
|
+
assert.match(text, /status\|report\|run\|baseline\|selftest\|approve\|queue\|web\|config\|collect\|review\|on\|off/);
|
|
197
|
+
assert.deepEqual(calls, []);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("collect on|off:只动采集(telemetry),不碰复盘", async () => {
|
|
201
|
+
const calls: string[] = [];
|
|
202
|
+
const evo = fakeEvolution(calls);
|
|
203
|
+
assert.equal(await runCommand("collect on", evo, TARGET), "开关:telemetry=true");
|
|
204
|
+
assert.equal(await runCommand("collect off", evo, TARGET), "开关:telemetry=false");
|
|
205
|
+
assert.deepEqual(calls, ["flag:telemetry=true", "flag:telemetry=false"]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("review on|off:只动复盘(enabled),不碰采集", async () => {
|
|
209
|
+
const calls: string[] = [];
|
|
210
|
+
const evo = fakeEvolution(calls);
|
|
211
|
+
assert.equal(await runCommand("review on", evo, TARGET), "开关:enabled=true");
|
|
212
|
+
assert.equal(await runCommand("review off", evo, TARGET), "开关:enabled=false");
|
|
213
|
+
assert.deepEqual(calls, ["flag:enabled=true", "flag:enabled=false"]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("collect / review 参数非法(缺失或写错):给用法串且不改配置", async () => {
|
|
217
|
+
const calls: string[] = [];
|
|
218
|
+
const evo = fakeEvolution(calls);
|
|
219
|
+
for (const args of ["collect", "collect yes", "review", "review 1"]) {
|
|
220
|
+
const text = await runCommand(args, evo, TARGET);
|
|
221
|
+
assert.match(text, /用法:\/skill-evolve (collect|review) on\|off/, args);
|
|
222
|
+
assert.match(text, /配置未改动/, args);
|
|
223
|
+
}
|
|
224
|
+
assert.deepEqual(calls, [], "非法参数不该调 setFlag");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("report --week 走周报,不把 --week 当天数", async () => {
|
|
228
|
+
const calls: string[] = [];
|
|
229
|
+
const evo = fakeEvolution(calls);
|
|
230
|
+
assert.equal(await runCommand("report --week", evo, TARGET), "周报文本");
|
|
231
|
+
assert.deepEqual(calls, ["week"]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("web:注入启动器时透传端口与 --once,没注入时给用法串", async () => {
|
|
235
|
+
const calls: string[] = [];
|
|
236
|
+
const evo = fakeEvolution(calls);
|
|
237
|
+
const withoutLauncher = await runCommand("web", evo, TARGET);
|
|
238
|
+
assert.match(withoutLauncher, /用法:\/skill-evolve web \[--port N\]/);
|
|
239
|
+
assert.match(withoutLauncher, /没有注入面板启动器/);
|
|
240
|
+
|
|
241
|
+
const requests: { port?: number; once: boolean }[] = [];
|
|
242
|
+
const launcher = async (request: { port?: number; once: boolean }): Promise<string> => {
|
|
243
|
+
requests.push(request);
|
|
244
|
+
return `面板:${request.port ?? "默认"}${request.once ? "(once)" : ""}`;
|
|
245
|
+
};
|
|
246
|
+
const withLauncher = { ...TARGET, web: launcher };
|
|
247
|
+
assert.equal(await runCommand("web", evo, withLauncher), "面板:默认");
|
|
248
|
+
assert.equal(await runCommand("web --port 7799", evo, withLauncher), "面板:7799");
|
|
249
|
+
assert.equal(await runCommand("web 7800", evo, withLauncher), "面板:7800");
|
|
250
|
+
assert.equal(await runCommand("web --once", evo, withLauncher), "面板:默认(once)");
|
|
251
|
+
assert.equal(await runCommand("web --port 7799 --once", evo, withLauncher), "面板:7799(once)");
|
|
252
|
+
assert.deepEqual(requests, [
|
|
253
|
+
{ once: false },
|
|
254
|
+
{ once: false, port: 7799 },
|
|
255
|
+
{ once: false, port: 7800 },
|
|
256
|
+
{ once: true },
|
|
257
|
+
{ once: true, port: 7799 },
|
|
258
|
+
]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("parseWebPort:只认 1~65535 的整数", () => {
|
|
262
|
+
assert.equal(parseWebPort(undefined), undefined);
|
|
263
|
+
assert.equal(parseWebPort("7788"), 7788);
|
|
264
|
+
assert.equal(parseWebPort("0"), undefined);
|
|
265
|
+
assert.equal(parseWebPort("65536"), undefined);
|
|
266
|
+
assert.equal(parseWebPort("abc"), undefined);
|
|
267
|
+
});
|