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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 面板生命周期:跟着 pi 进程走(`/skill-evolve` 2026-11-08 需求「reload web 界面没了」)。
|
|
3
|
+
*
|
|
4
|
+
* 全部注入:`start` 是假实现,`sleep` 是记录器 —— 不起子进程、不占端口、不等真时长。
|
|
5
|
+
* 判据口径:`locks/web.json` 里 pid == 本进程算 `/reload` 残影(可重试),别的活 pid 才算抢手。
|
|
6
|
+
*/
|
|
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 test from "node:test";
|
|
12
|
+
import { configFile, DEFAULT_CONFIG, loadConfig, normalizeConfig } from "../lib/config.ts";
|
|
13
|
+
import { directQueue } from "../lib/telemetry.ts";
|
|
14
|
+
import { createWebLifecycle, maybeStartWeb, type WebLifecycle, type WebStartResult } from "../lib/web/autostart.ts";
|
|
15
|
+
import { writeWebPid } from "../lib/web/panel-pid.ts";
|
|
16
|
+
import type { WebPanelResult } from "../lib/web/server.ts";
|
|
17
|
+
|
|
18
|
+
/** 不存在的 pid:Linux 的 pid_max 远小于 INT32_MAX,这个号系统里不会有进程 */
|
|
19
|
+
const DEAD_PID = 2_147_483_646;
|
|
20
|
+
const URL_7788 = "http://127.0.0.1:7788";
|
|
21
|
+
const IN_USE: WebStartResult = { ok: false, code: "EADDRINUSE", message: "端口被占用" };
|
|
22
|
+
|
|
23
|
+
function workspace(): string {
|
|
24
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "skill-evolve-autostart-"));
|
|
25
|
+
const evolutionDir = path.join(cwd, ".pi", "evolution");
|
|
26
|
+
fs.mkdirSync(evolutionDir, { recursive: true });
|
|
27
|
+
return evolutionDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function webConfig(enabled: boolean, port = 7788) {
|
|
31
|
+
return normalizeConfig({ web: { enabled, port } });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 按顺序回放的启动器:超出长度就重复最后一条(「一直占着」用它),并记下每次请求的端口 */
|
|
35
|
+
function scripted(results: WebStartResult[]): { calls: number[]; start: (port: number) => Promise<WebStartResult> } {
|
|
36
|
+
const calls: number[] = [];
|
|
37
|
+
return {
|
|
38
|
+
calls,
|
|
39
|
+
start: async (port: number): Promise<WebStartResult> => {
|
|
40
|
+
calls.push(port);
|
|
41
|
+
const index = Math.min(calls.length - 1, results.length - 1);
|
|
42
|
+
return results[index]!;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sleepRecorder(): { delays: number[]; sleep: (ms: number) => Promise<void> } {
|
|
48
|
+
const delays: number[] = [];
|
|
49
|
+
return { delays, sleep: async (ms: number): Promise<void> => void delays.push(ms) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 假面板:记下 start 请求的端口与 close 调用,close 异步一跳(不 await 就会看到 closed=false) */
|
|
53
|
+
function fakeServer(): { starts: number[]; closes: number[]; closed: () => boolean; start: (port?: number) => Promise<WebPanelResult> } {
|
|
54
|
+
const starts: number[] = [];
|
|
55
|
+
const closes: number[] = [];
|
|
56
|
+
let closedYet = false;
|
|
57
|
+
return {
|
|
58
|
+
starts,
|
|
59
|
+
closes,
|
|
60
|
+
closed: () => closedYet,
|
|
61
|
+
start: async (port?: number): Promise<WebPanelResult> => {
|
|
62
|
+
const actual = port ?? 7788;
|
|
63
|
+
starts.push(actual);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
panel: {
|
|
67
|
+
url: `http://127.0.0.1:${actual}`,
|
|
68
|
+
host: "127.0.0.1",
|
|
69
|
+
port: actual,
|
|
70
|
+
csrf: "test-csrf",
|
|
71
|
+
close: async (): Promise<void> => {
|
|
72
|
+
await new Promise<void>((done) => setImmediate(done));
|
|
73
|
+
closedYet = true;
|
|
74
|
+
closes.push(actual);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function lifecycleOf(evolutionDir: string, logs: string[] = []): WebLifecycle & { fake: ReturnType<typeof fakeServer> } {
|
|
83
|
+
const fake = fakeServer();
|
|
84
|
+
const lifecycle = createWebLifecycle({ evolutionDir: () => evolutionDir, start: fake.start, queue: directQueue, log: (msg) => void logs.push(msg) });
|
|
85
|
+
return Object.assign(lifecycle, { fake });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test("旧配置没有 web 块:默认关闭 + 端口 7788,且不算无效配置", () => {
|
|
89
|
+
const evolutionDir = workspace();
|
|
90
|
+
// 2026-11-08 之前的 config.json 就长这样,没有 web 键
|
|
91
|
+
fs.writeFileSync(configFile(evolutionDir), JSON.stringify({ enabled: false, telemetry: false, idleMs: 90_000 }), "utf8");
|
|
92
|
+
const logs: string[] = [];
|
|
93
|
+
const config = loadConfig(evolutionDir, (msg) => logs.push(msg));
|
|
94
|
+
assert.deepEqual(config.web, { enabled: DEFAULT_CONFIG.web.enabled, port: DEFAULT_CONFIG.web.port });
|
|
95
|
+
assert.deepEqual([config.web.enabled, config.web.port], [true, 7788], "面板默认随 pi 启动(采集/派生仍关)");
|
|
96
|
+
assert.equal(config.idleMs, 90_000, "老字段照旧生效,缺 web 不等于配置坏了");
|
|
97
|
+
assert.deepEqual(logs, [], "缺 web 块是正常情况,不该刷日志");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("web.port 非法:0 / 70000 / \"8080\" 都回落 7788 并各留一行 warn", () => {
|
|
101
|
+
for (const bad of [0, 70_000, "8080"]) {
|
|
102
|
+
const logs: string[] = [];
|
|
103
|
+
const config = normalizeConfig({ web: { enabled: true, port: bad } }, (msg) => logs.push(msg));
|
|
104
|
+
assert.equal(config.web.port, 7788, `非法端口 ${String(bad)} 应回落 7788`);
|
|
105
|
+
assert.equal(config.web.enabled, true, "端口写错不该牵连 enabled");
|
|
106
|
+
assert.equal(logs.length, 1);
|
|
107
|
+
assert.match(logs[0]!, /web\.port 非法/);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("web.enabled=false:一次 start 都不调", async () => {
|
|
112
|
+
const launcher = scripted([{ ok: true, url: URL_7788 }]);
|
|
113
|
+
const result = await maybeStartWeb({ config: webConfig(false), evolutionDir: workspace(), start: launcher.start });
|
|
114
|
+
assert.deepEqual(result, { started: false, reason: "disabled" });
|
|
115
|
+
assert.deepEqual(launcher.calls, []);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("web.enabled=true:起一次就成,日志写「随会话启动」", async () => {
|
|
119
|
+
const launcher = scripted([{ ok: true, url: URL_7788 }]);
|
|
120
|
+
const logs: string[] = [];
|
|
121
|
+
const result = await maybeStartWeb({
|
|
122
|
+
config: webConfig(true, 7788),
|
|
123
|
+
evolutionDir: workspace(),
|
|
124
|
+
start: launcher.start,
|
|
125
|
+
log: (msg) => void logs.push(msg),
|
|
126
|
+
});
|
|
127
|
+
assert.deepEqual(result, { started: true, url: URL_7788 });
|
|
128
|
+
assert.deepEqual(launcher.calls, [7788]);
|
|
129
|
+
assert.match(logs[0]!, /web 面板随会话启动:http:\/\/127\.0\.0\.1:7788/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("端口占用但记录里是别的活进程:不重试、不抢", async () => {
|
|
133
|
+
const evolutionDir = workspace();
|
|
134
|
+
// 父进程一定活着且不是本进程:真实活 pid,不用起子进程也不用真占端口
|
|
135
|
+
writeWebPid(evolutionDir, { pid: process.ppid, host: "127.0.0.1", port: 7788, url: URL_7788, startedAt: Date.now() });
|
|
136
|
+
const launcher = scripted([IN_USE]);
|
|
137
|
+
const sleeps = sleepRecorder();
|
|
138
|
+
const logs: string[] = [];
|
|
139
|
+
const result = await maybeStartWeb({
|
|
140
|
+
config: webConfig(true),
|
|
141
|
+
evolutionDir,
|
|
142
|
+
start: launcher.start,
|
|
143
|
+
sleep: sleeps.sleep,
|
|
144
|
+
log: (msg) => void logs.push(msg),
|
|
145
|
+
});
|
|
146
|
+
assert.deepEqual(result, { started: false, reason: "owned-elsewhere", url: URL_7788 });
|
|
147
|
+
assert.equal(launcher.calls.length, 1, "别人的活面板:一次都不该重试");
|
|
148
|
+
assert.deepEqual(sleeps.delays, []);
|
|
149
|
+
assert.match(logs[0]!, new RegExp(`由 pid ${process.ppid} 持有`));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("端口占用但记录里的 pid 已死:重试一次就成功,间隔 300ms", async () => {
|
|
153
|
+
const evolutionDir = workspace();
|
|
154
|
+
writeWebPid(evolutionDir, { pid: DEAD_PID, host: "127.0.0.1", port: 7788, url: URL_7788, startedAt: Date.now() });
|
|
155
|
+
const launcher = scripted([IN_USE, { ok: true, url: URL_7788 }]);
|
|
156
|
+
const sleeps = sleepRecorder();
|
|
157
|
+
const result = await maybeStartWeb({ config: webConfig(true), evolutionDir, start: launcher.start, sleep: sleeps.sleep });
|
|
158
|
+
assert.deepEqual(result, { started: true, url: URL_7788 });
|
|
159
|
+
assert.equal(launcher.calls.length, 2);
|
|
160
|
+
assert.deepEqual(sleeps.delays, [300]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("记录 pid 就是本进程(/reload 残影):允许重试,不当成别的实例", async () => {
|
|
164
|
+
const evolutionDir = workspace();
|
|
165
|
+
// /reload 不换 pid:旧实例留下的记录 pid 与当前进程相同,只能靠这个判据区分
|
|
166
|
+
writeWebPid(evolutionDir, { pid: process.pid, host: "127.0.0.1", port: 7788, url: URL_7788, startedAt: Date.now() });
|
|
167
|
+
const launcher = scripted([IN_USE, { ok: true, url: URL_7788 }]);
|
|
168
|
+
const sleeps = sleepRecorder();
|
|
169
|
+
const logs: string[] = [];
|
|
170
|
+
const result = await maybeStartWeb({
|
|
171
|
+
config: webConfig(true),
|
|
172
|
+
evolutionDir,
|
|
173
|
+
start: launcher.start,
|
|
174
|
+
sleep: sleeps.sleep,
|
|
175
|
+
log: (msg) => void logs.push(msg),
|
|
176
|
+
});
|
|
177
|
+
assert.deepEqual(result, { started: true, url: URL_7788 });
|
|
178
|
+
assert.equal(launcher.calls.length, 2);
|
|
179
|
+
assert.deepEqual(sleeps.delays, [300]);
|
|
180
|
+
assert.match(logs.join("\n"), /重试 1\/3:端口 7788 仍被占用/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("一直 EADDRINUSE:重试 3 次(间隔 300/600/900)后判 failed", async () => {
|
|
184
|
+
const launcher = scripted([IN_USE]);
|
|
185
|
+
const sleeps = sleepRecorder();
|
|
186
|
+
const logs: string[] = [];
|
|
187
|
+
const result = await maybeStartWeb({
|
|
188
|
+
config: webConfig(true),
|
|
189
|
+
evolutionDir: workspace(),
|
|
190
|
+
start: launcher.start,
|
|
191
|
+
sleep: sleeps.sleep,
|
|
192
|
+
log: (msg) => void logs.push(msg),
|
|
193
|
+
});
|
|
194
|
+
assert.equal(result.started, false);
|
|
195
|
+
assert.equal(result.started === false ? result.reason : "", "failed");
|
|
196
|
+
assert.equal(launcher.calls.length, 4, "首次 + 3 次重试");
|
|
197
|
+
assert.deepEqual(sleeps.delays, [300, 600, 900]);
|
|
198
|
+
assert.match(logs.at(-1)!, /web 面板未启动(端口 7788)/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("EACCES 不重试:没权限换端口才有用", async () => {
|
|
202
|
+
const launcher = scripted([{ ok: false, code: "EACCES", message: "没有权限监听" }]);
|
|
203
|
+
const sleeps = sleepRecorder();
|
|
204
|
+
const result = await maybeStartWeb({ config: webConfig(true), evolutionDir: workspace(), start: launcher.start, sleep: sleeps.sleep });
|
|
205
|
+
assert.equal(result.started === false ? result.reason : "", "failed");
|
|
206
|
+
assert.equal(launcher.calls.length, 1);
|
|
207
|
+
assert.deepEqual(sleeps.delays, []);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("session_start 时配置没开:一个 start 都不发", async () => {
|
|
211
|
+
const evolutionDir = workspace();
|
|
212
|
+
fs.mkdirSync(evolutionDir, { recursive: true });
|
|
213
|
+
fs.writeFileSync(path.join(evolutionDir, "config.json"), JSON.stringify({ web: { enabled: false } }), "utf8");
|
|
214
|
+
const lifecycle = lifecycleOf(evolutionDir);
|
|
215
|
+
await lifecycle.startForSession();
|
|
216
|
+
assert.equal(lifecycle.url(), undefined);
|
|
217
|
+
assert.deepEqual(lifecycle.fake.starts, []);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("web 命令成功:把 web.enabled=true 与端口写回配置,重复执行只回「已在」", async () => {
|
|
221
|
+
const evolutionDir = workspace();
|
|
222
|
+
const logs: string[] = [];
|
|
223
|
+
const lifecycle = lifecycleOf(evolutionDir, logs);
|
|
224
|
+
const text = await lifecycle.command({ once: false, port: 7799 });
|
|
225
|
+
assert.match(text, /面板已起:http:\/\/127\.0\.0\.1:7799(已在配置里开启,pi 启动时会自动起;换端口 --port 7799)/);
|
|
226
|
+
const raw = JSON.parse(fs.readFileSync(configFile(evolutionDir), "utf8"));
|
|
227
|
+
assert.deepEqual([raw.web.enabled, raw.web.port], [true, 7799], "端口一起记,下次自动起才落同一地址");
|
|
228
|
+
assert.equal(lifecycle.url(), "http://127.0.0.1:7799");
|
|
229
|
+
assert.match(await lifecycle.command({ once: false }), /面板已在 http:\/\/127\.0\.0\.1:7799 运行/);
|
|
230
|
+
assert.deepEqual(lifecycle.fake.starts, [7799], "单例:第二次不该再起");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("web --once:起来了但不写进配置", async () => {
|
|
234
|
+
const evolutionDir = workspace();
|
|
235
|
+
fs.writeFileSync(configFile(evolutionDir), JSON.stringify({ enabled: false, telemetry: false }), "utf8");
|
|
236
|
+
const lifecycle = lifecycleOf(evolutionDir);
|
|
237
|
+
const text = await lifecycle.command({ once: true });
|
|
238
|
+
assert.match(text, /面板已起:http:\/\/127\.0\.0\.1:7788(--once:只本次会话有效,没写进配置)/);
|
|
239
|
+
const raw = JSON.parse(fs.readFileSync(configFile(evolutionDir), "utf8"));
|
|
240
|
+
assert.notEqual(raw.web?.enabled, true, "--once 不能把下次启动也带上");
|
|
241
|
+
assert.equal(lifecycle.url(), URL_7788);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("off / disable:关面板(等 close 完成)并把 web.enabled 写回 false", async () => {
|
|
245
|
+
const evolutionDir = workspace();
|
|
246
|
+
fs.writeFileSync(configFile(evolutionDir), JSON.stringify({ enabled: false, telemetry: false, web: { enabled: true, port: 7900 } }), "utf8");
|
|
247
|
+
const lifecycle = lifecycleOf(evolutionDir);
|
|
248
|
+
await lifecycle.startForSession();
|
|
249
|
+
assert.equal(lifecycle.url(), "http://127.0.0.1:7900", "配置开着就在会话启动时自动起");
|
|
250
|
+
const text = await lifecycle.disable();
|
|
251
|
+
assert.deepEqual(lifecycle.fake.closes, [7900]);
|
|
252
|
+
assert.equal(lifecycle.url(), undefined);
|
|
253
|
+
const raw = JSON.parse(fs.readFileSync(configFile(evolutionDir), "utf8"));
|
|
254
|
+
assert.deepEqual([raw.web.enabled, raw.web.port], [false, 7900], "只关开关,端口原样保留");
|
|
255
|
+
assert.match(text, /web\.enabled=false/);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("session_shutdown:等面板 close 完成再返回(/reload 后新实例才能复用同一端口)", async () => {
|
|
259
|
+
const evolutionDir = workspace();
|
|
260
|
+
fs.writeFileSync(configFile(evolutionDir), JSON.stringify({ web: { enabled: true, port: 7788 } }), "utf8");
|
|
261
|
+
const logs: string[] = [];
|
|
262
|
+
const lifecycle = lifecycleOf(evolutionDir, logs);
|
|
263
|
+
await lifecycle.startForSession();
|
|
264
|
+
await lifecycle.shutdown("reload");
|
|
265
|
+
assert.equal(lifecycle.fake.closed(), true, "shutdown 返回时必须已经关完");
|
|
266
|
+
assert.deepEqual(lifecycle.fake.closes, [7788]);
|
|
267
|
+
assert.match(logs.join("\n"), /web 面板关闭(reason=reload)/);
|
|
268
|
+
});
|