dsh-prompt-customizer 0.9.2 → 2.1.2
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/README.en.md +49 -234
- package/README.md +45 -310
- package/build.mjs +27 -10
- package/client/client.js +2446 -1364
- package/client/client.js.map +4 -4
- package/docs/adr/0001-remove-catalog-pool.md +27 -0
- package/docs/adr/0002-hide-identity-name-source.md +23 -0
- package/docs/adr/0003-destructive-updates-allowed.md +18 -0
- package/docs/adr/0004-generalized-filter-model.md +17 -0
- package/docs/adr/0005-universal-prompt-takeover.md +109 -0
- package/docs/design/prompt-control-consensus.md +103 -0
- package/docs/guide.md +280 -0
- package/docs/images/prompt.png +0 -0
- package/docs/images/tool.png +0 -0
- package/docs/testing/feature-matrix.md +142 -0
- package/docs/upgrades/1.0.0-sqlite-breaking.md +26 -0
- package/docs/upgrades/1.4.0-generalized-model.md +23 -0
- package/docs/upgrades/1.4.1-web-preview-fixes.md +15 -0
- package/docs/upgrades/2.0.0-remove-workspace-level.md +16 -0
- package/docs/upgrades/2.0.1-context-fixes.md +6 -0
- package/docs/upgrades/2.0.10-import-and-skill.md +6 -0
- package/docs/upgrades/2.0.11-regression-fixes.md +7 -0
- package/docs/upgrades/2.0.12-generative-tool-tests.md +5 -0
- package/docs/upgrades/2.0.13-runtime-contract.md +8 -0
- package/docs/upgrades/2.0.14-tool-execution-guard.md +5 -0
- package/docs/upgrades/2.0.15-mask-pool-regression.md +5 -0
- package/docs/upgrades/2.0.16-regression-cleanup.md +6 -0
- package/docs/upgrades/2.0.17-unhide-source.md +5 -0
- package/docs/upgrades/2.0.18-unhide-cleanup.md +4 -0
- package/docs/upgrades/2.0.19-mcp-json-edit.md +4 -0
- package/docs/upgrades/2.0.2-generic-context-filter.md +6 -0
- package/docs/upgrades/2.0.20-preset-isolation.md +4 -0
- package/docs/upgrades/2.0.21-isolation-restore-test.md +4 -0
- package/docs/upgrades/2.0.22-source-immutability.md +3 -0
- package/docs/upgrades/2.0.3-skill-summary.md +6 -0
- package/docs/upgrades/2.0.4-tool-draft.md +5 -0
- package/docs/upgrades/2.0.5-tool-catalog-robustness.md +13 -0
- package/docs/upgrades/2.0.6-tool-restrict.md +6 -0
- package/docs/upgrades/2.0.7-tool-stability.md +5 -0
- package/docs/upgrades/2.0.8-catalog-row-stability.md +4 -0
- package/docs/upgrades/2.0.9-tool-and-write.md +8 -0
- package/docs/upgrades/2.1.0-universal-prompt-takeover.md +41 -0
- package/docs/upgrades/2.1.1-tool-shadow-detection.md +48 -0
- package/docs/upgrades/2.1.2-tool-catalog-scope.md +55 -0
- package/docs/upgrades/README.md +298 -0
- package/docs/upgrades/dsh-0.2.2-rc1.md +49 -0
- package/docs/upgrades/rc2-migration.md +70 -0
- package/lib/index.js +4697 -1701
- package/lib/index.js.map +4 -4
- package/package.json +7 -5
- package/scripts/e2e-report.json +221 -0
- package/scripts/e2e-terminal.mjs +210 -0
- package/scripts/simulate-rescan.mjs +184 -0
- package/scripts/smoke-host.mjs +1 -1
- package/scripts/verify-apply-all.mjs +64 -0
- package/scripts/verify-effect.mjs +153 -0
- package/scripts/verify-mcp-edit-ext.mjs +48 -0
- package/scripts/verify-mcp-edit.mjs +45 -0
- package/scripts/verify-panel-browser.mjs +298 -0
- package/scripts/verify-workspace-mcp.mjs +62 -0
- package/scripts/test-mcp-layering.mjs +0 -441
- package/scripts/test-mcp-tool-disable.mjs +0 -420
- package/scripts/test-rc2-runtime.mjs +0 -42
- package/scripts/test-skill-toggles.mjs +0 -100
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-prompt-customizer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "DeepSeek Harness 的三合一能力台:提示词定制(按名称屏蔽 / 替换 / 注入 / 排序,按会话阶段与 agent 预设分层)+ 技能集合管理(导入 / 分组 / 开关)+ MCP Server 接入(分层遮蔽与工具级启停)。纯插件注入,不动官方源码。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"lib",
|
|
39
39
|
"client",
|
|
40
40
|
"scripts",
|
|
41
|
+
"docs",
|
|
41
42
|
"build.mjs",
|
|
42
43
|
"cordis.patch.yml",
|
|
43
44
|
"README.md",
|
|
@@ -62,18 +63,19 @@
|
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"build": "node build.mjs",
|
|
65
|
-
"test": "node --test",
|
|
66
|
+
"test": "node --import ./test/_db-setup.mjs --test",
|
|
66
67
|
"smoke": "node scripts/smoke-host.mjs && node scripts/smoke-client.mjs",
|
|
67
68
|
"check": "npm run build && npm test && npm run smoke",
|
|
68
69
|
"prepack": "npm run build"
|
|
69
70
|
},
|
|
70
71
|
"engines": {
|
|
71
|
-
"node": ">=
|
|
72
|
+
"node": ">=24.9",
|
|
73
|
+
"dsh": "^0.2.2-rc.1"
|
|
72
74
|
},
|
|
73
75
|
"peerDependencies": {
|
|
74
76
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
76
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
77
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.2-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-settings": "^0.2.2-rc.1",
|
|
77
79
|
"react": "^18.2.0",
|
|
78
80
|
"react-dom": "^18.2.0"
|
|
79
81
|
},
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
{
|
|
2
|
+
"when": "2026-11-07T13:07:27.619Z",
|
|
3
|
+
"url": "http://127.0.0.1:3134",
|
|
4
|
+
"destructive": true,
|
|
5
|
+
"passed": 43,
|
|
6
|
+
"failed": 0,
|
|
7
|
+
"results": [
|
|
8
|
+
{
|
|
9
|
+
"name": "握手 /?token 获取 auth cookie",
|
|
10
|
+
"ok": true,
|
|
11
|
+
"detail": "status=303 cookie=dsh-auth-ofppdFcJozve_bX…"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "inventory 清单",
|
|
15
|
+
"ok": true,
|
|
16
|
+
"detail": "GET /api/prompt-customizer/inventory status=200"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "config 配置",
|
|
20
|
+
"ok": true,
|
|
21
|
+
"detail": "GET /api/prompt-customizer/config status=200"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "agent-presets 预设名单",
|
|
25
|
+
"ok": true,
|
|
26
|
+
"detail": "GET /api/prompt-customizer/agent-presets status=200"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "preview 三阶段(bootstrap)",
|
|
30
|
+
"ok": true,
|
|
31
|
+
"detail": "GET /api/prompt-customizer/preview?phase=bootstrap status=200"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "session 当前会话",
|
|
35
|
+
"ok": true,
|
|
36
|
+
"detail": "GET /api/prompt-customizer/session status=200"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "skill-manager list",
|
|
40
|
+
"ok": true,
|
|
41
|
+
"detail": "GET /api/skill-manager/list status=200"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "skill-toggles status",
|
|
45
|
+
"ok": true,
|
|
46
|
+
"detail": "GET /api/skill-toggles/status status=200"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "skill-toggles presets",
|
|
50
|
+
"ok": true,
|
|
51
|
+
"detail": "GET /api/skill-toggles/presets status=200"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "skill-toggles workspaces",
|
|
55
|
+
"ok": true,
|
|
56
|
+
"detail": "GET /api/skill-toggles/workspaces status=200"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "skill-health",
|
|
60
|
+
"ok": true,
|
|
61
|
+
"detail": "GET /api/skill-health status=200"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "MCP status",
|
|
65
|
+
"ok": true,
|
|
66
|
+
"detail": "GET /api/triad/mcp-status status=200"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "MCP workspace scope status",
|
|
70
|
+
"ok": true,
|
|
71
|
+
"detail": "GET /api/triad/mcp-workspace-scope/status?cwd=C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU status=200"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "MCP 已注册 Server 数 >= 1",
|
|
75
|
+
"ok": true,
|
|
76
|
+
"detail": "servers=anysearch, codegraph"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "MCP Server 存在: anysearch",
|
|
80
|
+
"ok": true,
|
|
81
|
+
"detail": "tools=5"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "MCP Server 存在: codegraph",
|
|
85
|
+
"ok": true,
|
|
86
|
+
"detail": "tools=1"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "备份整库 /backup",
|
|
90
|
+
"ok": true,
|
|
91
|
+
"detail": "docs keys=mcp/tool-disable"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "config/apply 预设级写入",
|
|
95
|
+
"ok": true,
|
|
96
|
+
"detail": "POST /api/prompt-customizer/config/apply status=200"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "config 反映预设级 sections=__e2e_probe__",
|
|
100
|
+
"ok": true,
|
|
101
|
+
"detail": "sections=[\"__e2e_probe__\"]"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "config/apply 预设级还原",
|
|
105
|
+
"ok": true,
|
|
106
|
+
"detail": "POST /api/prompt-customizer/config/apply status=200"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "config/apply 工作区级写入",
|
|
110
|
+
"ok": true,
|
|
111
|
+
"detail": "POST /api/prompt-customizer/config/apply status=200"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "config 反映工作区级 workspaceOverrides",
|
|
115
|
+
"ok": true,
|
|
116
|
+
"detail": "ws=[\"__e2e_ws__\"]"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "config/apply 工作区级还原",
|
|
120
|
+
"ok": true,
|
|
121
|
+
"detail": "POST /api/prompt-customizer/config/apply status=200"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "config/set forceSections=false",
|
|
125
|
+
"ok": true,
|
|
126
|
+
"detail": "POST /api/prompt-customizer/config/set status=200"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "config 反映 forceSections=false",
|
|
130
|
+
"ok": true
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "config/unset forceSections",
|
|
134
|
+
"ok": true,
|
|
135
|
+
"detail": "POST /api/prompt-customizer/config/unset status=200"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "export 当前预设定制",
|
|
139
|
+
"ok": true,
|
|
140
|
+
"detail": "GET /api/prompt-customizer/export?scope=standard status=200"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "SKILL 工作区级关闭 diagnose-windows-sandbox-acl",
|
|
144
|
+
"ok": true,
|
|
145
|
+
"detail": "PUT /api/skill-toggles/workspaces/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/skills/diagnose-windows-sandbox-acl status=200"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "SKILL workspaces 反映关闭",
|
|
149
|
+
"ok": true
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "SKILL 工作区级还原 diagnose-windows-sandbox-acl",
|
|
153
|
+
"ok": true,
|
|
154
|
+
"detail": "PUT /api/skill-toggles/workspaces/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/skills/diagnose-windows-sandbox-acl status=200"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "SKILL 预设级关闭 diagnose-windows-sandbox-acl@standard",
|
|
158
|
+
"ok": true,
|
|
159
|
+
"detail": "PUT /api/skill-toggles/presets/standard/skills/diagnose-windows-sandbox-acl status=200"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"name": "SKILL 预设级还原 diagnose-windows-sandbox-acl@standard",
|
|
163
|
+
"ok": true,
|
|
164
|
+
"detail": "PUT /api/skill-toggles/presets/standard/skills/diagnose-windows-sandbox-acl status=200"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "MCP 工作区级工具禁用 mcp__anysearch__batch_search",
|
|
168
|
+
"ok": true,
|
|
169
|
+
"detail": "PUT /api/triad/mcp-workspace-scope/workspace/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/tools status=200"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "MCP status 反映工具禁用",
|
|
173
|
+
"ok": true,
|
|
174
|
+
"detail": "toolDisabled=[\"mcp__anysearch__batch_search\"]"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "MCP 工作区级工具还原 mcp__anysearch__batch_search",
|
|
178
|
+
"ok": true,
|
|
179
|
+
"detail": "PUT /api/triad/mcp-workspace-scope/workspace/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/tools status=200"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "MCP 工作区级关闭 server anysearch",
|
|
183
|
+
"ok": true,
|
|
184
|
+
"detail": "PUT /api/triad/mcp-workspace-scope/workspace/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/servers/anysearch status=200"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "MCP status 反映 server 关闭",
|
|
188
|
+
"ok": true
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "MCP 工作区级启用 server anysearch",
|
|
192
|
+
"ok": true,
|
|
193
|
+
"detail": "PUT /api/triad/mcp-workspace-scope/workspace/C%3A%5CUsers%5CADMINI~1%5CAppData%5CLocal%5CTemp%5Cpc-e2e-ws-YJLVdU/servers/anysearch status=200"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "MCP 预设级遮蔽 anysearch@standard",
|
|
197
|
+
"ok": true,
|
|
198
|
+
"detail": "PUT /api/triad/mcp-masks/standard/anysearch status=200"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "MCP 预设级遮蔽还原 anysearch@standard",
|
|
202
|
+
"ok": true,
|
|
203
|
+
"detail": "PUT /api/triad/mcp-masks/standard/anysearch status=200"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "catalog/rescan 可调用",
|
|
207
|
+
"ok": true,
|
|
208
|
+
"detail": "status=200 "
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "恢复整库 /restore",
|
|
212
|
+
"ok": true,
|
|
213
|
+
"detail": "status=200"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "恢复后 overrides 与备份一致",
|
|
217
|
+
"ok": true,
|
|
218
|
+
"detail": ""
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-prompt-customizer 终端级 E2E 终测脚本。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/e2e-terminal.mjs --url "http://127.0.0.1:PORT/?token=XXXX" [--destructive]
|
|
7
|
+
*
|
|
8
|
+
* 说明:
|
|
9
|
+
* - 先对 `/?token=` 做一次握手拿 dsh-auth cookie,再带 cookie 访问各 API。
|
|
10
|
+
* - 默认只跑只读检查;`--destructive` 会跑写入场景,且**测试前整库备份、结束时恢复**,
|
|
11
|
+
* 保护真实数据。任何异常都会在 finally 里尝试恢复。
|
|
12
|
+
* - 退出码:全部通过 0,否则 1。结果同时写到 scripts/e2e-report.json。
|
|
13
|
+
*
|
|
14
|
+
* 状态(2.1.0):`--destructive` 里的「config/apply 工作区级写入 / 还原」场景仍用
|
|
15
|
+
* 2.0.0 已移除的 `level=workspace` 与 `workspaceOverrides`,与当前契约不相容;只读
|
|
16
|
+
* 检查仍可用。`scripts/e2e-report.json` 与 `docs/testing/feature-matrix.md` 都是历史
|
|
17
|
+
* 运行结果,不是当前验证结论(当前门禁见 `npm run check`)。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import os from 'node:os'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
const argv = process.argv.slice(2)
|
|
24
|
+
const arg = (name, fallback) => {
|
|
25
|
+
const i = argv.indexOf(name)
|
|
26
|
+
return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : fallback
|
|
27
|
+
}
|
|
28
|
+
const TOKEN_URL = arg('--url')
|
|
29
|
+
const DESTRUCTIVE = argv.includes('--destructive')
|
|
30
|
+
if (TOKEN_URL === undefined) {
|
|
31
|
+
console.error('缺少 --url "http://127.0.0.1:PORT/?token=XXXX"')
|
|
32
|
+
process.exit(2)
|
|
33
|
+
}
|
|
34
|
+
const U = new URL(TOKEN_URL)
|
|
35
|
+
const ORIGIN = U.origin
|
|
36
|
+
|
|
37
|
+
const results = []
|
|
38
|
+
const record = (name, ok, detail) => {
|
|
39
|
+
results.push({ name, ok, detail })
|
|
40
|
+
console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
41
|
+
}
|
|
42
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
43
|
+
|
|
44
|
+
let cookie = ''
|
|
45
|
+
async function handshake() {
|
|
46
|
+
const res = await fetch(TOKEN_URL, { redirect: 'manual' })
|
|
47
|
+
const sc = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : []
|
|
48
|
+
cookie = sc.map((c) => c.split(';')[0]).join('; ')
|
|
49
|
+
record('握手 /?token 获取 auth cookie', res.status < 400 && cookie !== '', `status=${res.status} cookie=${cookie.slice(0, 24)}…`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function req(method, p, body) {
|
|
53
|
+
const headers = { accept: 'application/json' }
|
|
54
|
+
if (cookie !== '') headers.cookie = cookie
|
|
55
|
+
if (body !== undefined) headers['content-type'] = 'application/json'
|
|
56
|
+
const res = await fetch(ORIGIN + p, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) })
|
|
57
|
+
let json = null
|
|
58
|
+
try { json = await res.json() } catch { json = null }
|
|
59
|
+
return { status: res.status, json }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function okGet(name, p, validate) {
|
|
63
|
+
try {
|
|
64
|
+
const { status, json } = await req('GET', p)
|
|
65
|
+
const good = status === 200 && json !== null && json.ok !== false && (validate === undefined || validate(json))
|
|
66
|
+
record(name, good, `GET ${p} status=${status}${good ? '' : ` body=${JSON.stringify(json).slice(0, 160)}`}`)
|
|
67
|
+
return { status, json }
|
|
68
|
+
} catch (e) {
|
|
69
|
+
record(name, false, `GET ${p} 异常: ${e instanceof Error ? e.message : String(e)}`)
|
|
70
|
+
return { status: 0, json: null }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function expect(name, p, method, body, pred) {
|
|
75
|
+
try {
|
|
76
|
+
const { status, json } = await req(method, p, body)
|
|
77
|
+
const good = (pred ? pred(status, json) : status === 200 && json !== null && json.ok === true)
|
|
78
|
+
record(name, good, `${method} ${p} status=${status}${good ? '' : ` body=${JSON.stringify(json).slice(0, 200)}`}`)
|
|
79
|
+
return { status, json }
|
|
80
|
+
} catch (e) {
|
|
81
|
+
record(name, false, `${method} ${p} 异常: ${e instanceof Error ? e.message : String(e)}`)
|
|
82
|
+
return { status: 0, json: null }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const WS = fs.mkdtempSync(path.join(os.tmpdir(), 'pc-e2e-ws-')) // 隔离的临时工作区 cwd
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
await handshake()
|
|
90
|
+
|
|
91
|
+
// ── 只读接口全量 ──────────────────────────────────────────────
|
|
92
|
+
await okGet('inventory 清单', '/api/prompt-customizer/inventory', (j) => Array.isArray(j.tools) || Array.isArray(j.sections) || j.counts !== undefined)
|
|
93
|
+
const cfg = await okGet('config 配置', '/api/prompt-customizer/config', (j) => j.config !== undefined)
|
|
94
|
+
await okGet('agent-presets 预设名单', '/api/prompt-customizer/agent-presets', (j) => Array.isArray(j.presets))
|
|
95
|
+
await okGet('preview 三阶段(bootstrap)', '/api/prompt-customizer/preview?phase=bootstrap', (j) => j !== null)
|
|
96
|
+
await okGet('session 当前会话', '/api/prompt-customizer/session', (j) => j !== null)
|
|
97
|
+
await okGet('skill-manager list', '/api/skill-manager/list', (j) => Array.isArray(j.bundles) || Array.isArray(j.loose) || Array.isArray(j.skills))
|
|
98
|
+
const st = await okGet('skill-toggles status', '/api/skill-toggles/status', (j) => j.skills !== undefined)
|
|
99
|
+
await okGet('skill-toggles presets', '/api/skill-toggles/presets', (j) => Array.isArray(j.presets))
|
|
100
|
+
await okGet('skill-toggles workspaces', '/api/skill-toggles/workspaces', (j) => j.workspaces !== undefined)
|
|
101
|
+
await okGet('skill-health', '/api/skill-health', (j) => Array.isArray(j.issues) || j.ok === true)
|
|
102
|
+
const mcp = await okGet('MCP status', '/api/triad/mcp-status', (j) => Array.isArray(j.servers))
|
|
103
|
+
await okGet('MCP workspace scope status', `/api/triad/mcp-workspace-scope/status?cwd=${encodeURIComponent(WS)}`, (j) => j.ok === true)
|
|
104
|
+
|
|
105
|
+
const presets = cfg.json?.config ? cfg.json : cfg.json
|
|
106
|
+
const presetList = (await req('GET', '/api/prompt-customizer/agent-presets')).json?.presets ?? []
|
|
107
|
+
const presetId = Array.isArray(presetList) && presetList.length > 0 ? (presetList.find((p) => !p.broken)?.id ?? presetList[0].id) : 'standard'
|
|
108
|
+
const servers = Array.isArray(mcp.json?.servers) ? mcp.json.servers : []
|
|
109
|
+
record('MCP 已注册 Server 数 >= 1', servers.length >= 1, `servers=${servers.map((s) => s.serverName).join(', ')}`)
|
|
110
|
+
for (const s of servers) {
|
|
111
|
+
record(`MCP Server 存在: ${s.serverName}`, true, `tools=${(s.tools ?? []).length}`
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
const skillsObj = st.json?.skills ?? {}
|
|
115
|
+
const skillNames = Object.keys(skillsObj)
|
|
116
|
+
|
|
117
|
+
if (!DESTRUCTIVE) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── 写入场景(备份 → 测试 → 恢复)────────────────────────────
|
|
122
|
+
const backup = await req('GET', '/api/prompt-customizer/backup')
|
|
123
|
+
record('备份整库 /backup', backup.status === 200 && backup.json?.ok === true && backup.json.docs !== undefined, `docs keys=${Object.keys(backup.json?.docs ?? {}).join(',')}`)
|
|
124
|
+
const backupFile = path.join(process.cwd(), 'scripts', '.e2e-backup.json')
|
|
125
|
+
fs.writeFileSync(backupFile, JSON.stringify(backup.json, null, 2))
|
|
126
|
+
const originalOverrides = JSON.parse(JSON.stringify(backup.json?.docs?.config?.overrides ?? {}))
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// 提示词·预设级 config/apply
|
|
130
|
+
await expect('config/apply 预设级写入', '/api/prompt-customizer/config/apply', 'POST', { target: presetId, patch: { sections: ['__e2e_probe__'] } })
|
|
131
|
+
const c1 = await req('GET', '/api/prompt-customizer/config')
|
|
132
|
+
record('config 反映预设级 sections=__e2e_probe__', (c1.json?.config?.overrides?.[presetId]?.sections ?? []).includes('__e2e_probe__'), `sections=${JSON.stringify(c1.json?.config?.overrides?.[presetId]?.sections)}`)
|
|
133
|
+
await expect('config/apply 预设级还原', '/api/prompt-customizer/config/apply', 'POST', { target: presetId, patch: { sections: null } })
|
|
134
|
+
|
|
135
|
+
// 提示词·工作区级 config/apply level=workspace
|
|
136
|
+
await expect('config/apply 工作区级写入', '/api/prompt-customizer/config/apply', 'POST', { level: 'workspace', workspace: WS, patch: { sections: ['__e2e_ws__'] } })
|
|
137
|
+
const c2 = await req('GET', '/api/prompt-customizer/config')
|
|
138
|
+
record('config 反映工作区级 workspaceOverrides', (c2.json?.workspaceOverrides?.[WS]?.sections ?? []).includes('__e2e_ws__'), `ws=${JSON.stringify(c2.json?.workspaceOverrides?.[WS]?.sections)}`)
|
|
139
|
+
await expect('config/apply 工作区级还原', '/api/prompt-customizer/config/apply', 'POST', { level: 'workspace', workspace: WS, patch: { sections: null } })
|
|
140
|
+
|
|
141
|
+
// config/set + unset(forceSections)
|
|
142
|
+
await expect('config/set forceSections=false', '/api/prompt-customizer/config/set', 'POST', { field: 'forceSections', value: false })
|
|
143
|
+
const c3 = await req('GET', '/api/prompt-customizer/config')
|
|
144
|
+
record('config 反映 forceSections=false', c3.json?.config?.forceSections === false)
|
|
145
|
+
await expect('config/unset forceSections', '/api/prompt-customizer/config/unset', 'POST', { field: 'forceSections' })
|
|
146
|
+
|
|
147
|
+
// export(只读导出该预设定制)
|
|
148
|
+
await okGet(`export 当前预设定制`, `/api/prompt-customizer/export?scope=${encodeURIComponent(presetId)}`, (j) => j.customization !== undefined)
|
|
149
|
+
|
|
150
|
+
// SKILL·工作区级
|
|
151
|
+
if (skillNames.length > 0) {
|
|
152
|
+
const sk = skillNames[0]
|
|
153
|
+
await expect(`SKILL 工作区级关闭 ${sk}`, `/api/skill-toggles/workspaces/${encodeURIComponent(WS)}/skills/${encodeURIComponent(sk)}`, 'PUT', { enabled: false })
|
|
154
|
+
const w1 = await req('GET', '/api/skill-toggles/workspaces')
|
|
155
|
+
record('SKILL workspaces 反映关闭', (w1.json?.workspaces?.[WS]?.[sk]) === false)
|
|
156
|
+
await expect(`SKILL 工作区级还原 ${sk}`, `/api/skill-toggles/workspaces/${encodeURIComponent(WS)}/skills/${encodeURIComponent(sk)}`, 'PUT', { enabled: true })
|
|
157
|
+
// SKILL·预设级
|
|
158
|
+
await expect(`SKILL 预设级关闭 ${sk}@${presetId}`, `/api/skill-toggles/presets/${encodeURIComponent(presetId)}/skills/${encodeURIComponent(sk)}`, 'PUT', { enabled: false })
|
|
159
|
+
await expect(`SKILL 预设级还原 ${sk}@${presetId}`, `/api/skill-toggles/presets/${encodeURIComponent(presetId)}/skills/${encodeURIComponent(sk)}`, 'PUT', { enabled: true })
|
|
160
|
+
} else {
|
|
161
|
+
record('SKILL 开关(无可用技能,跳过)', true, '0 skills')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MCP·工作区级工具启停 + server 关闭 + 还原
|
|
165
|
+
if (servers.length > 0) {
|
|
166
|
+
const s0 = servers[0]
|
|
167
|
+
const fullName = (s0.tools ?? [])[0]?.name ?? `mcp__${s0.serverName}__probe`
|
|
168
|
+
await expect(`MCP 工作区级工具禁用 ${fullName}`, `/api/triad/mcp-workspace-scope/workspace/${encodeURIComponent(WS)}/tools`, 'PUT', { name: fullName, enabled: false })
|
|
169
|
+
const m1 = await req('GET', `/api/triad/mcp-workspace-scope/status?cwd=${encodeURIComponent(WS)}`)
|
|
170
|
+
record('MCP status 反映工具禁用', (m1.json?.toolDisabled ?? []).includes(fullName), `toolDisabled=${JSON.stringify(m1.json?.toolDisabled)}`)
|
|
171
|
+
await expect(`MCP 工作区级工具还原 ${fullName}`, `/api/triad/mcp-workspace-scope/workspace/${encodeURIComponent(WS)}/tools`, 'PUT', { name: fullName, enabled: true })
|
|
172
|
+
|
|
173
|
+
await expect(`MCP 工作区级关闭 server ${s0.serverName}`, `/api/triad/mcp-workspace-scope/workspace/${encodeURIComponent(WS)}/servers/${encodeURIComponent(s0.serverName)}`, 'PUT', { enabled: false })
|
|
174
|
+
const m2 = await req('GET', `/api/triad/mcp-workspace-scope/status?cwd=${encodeURIComponent(WS)}`)
|
|
175
|
+
record('MCP status 反映 server 关闭', (m2.json?.closed ?? []).includes(s0.serverName))
|
|
176
|
+
await expect(`MCP 工作区级启用 server ${s0.serverName}`, `/api/triad/mcp-workspace-scope/workspace/${encodeURIComponent(WS)}/servers/${encodeURIComponent(s0.serverName)}`, 'PUT', { enabled: true })
|
|
177
|
+
|
|
178
|
+
// 预设级遮蔽 mask
|
|
179
|
+
await expect(`MCP 预设级遮蔽 ${s0.serverName}@${presetId}`, `/api/triad/mcp-masks/${encodeURIComponent(presetId)}/${encodeURIComponent(s0.serverName)}`, 'PUT', { enabled: false }, (st2, j) => st2 === 200 && j?.ok === true)
|
|
180
|
+
await expect(`MCP 预设级遮蔽还原 ${s0.serverName}@${presetId}`, `/api/triad/mcp-masks/${encodeURIComponent(presetId)}/${encodeURIComponent(s0.serverName)}`, 'PUT', { enabled: true }, (st2, j) => st2 === 200 && j?.ok === true)
|
|
181
|
+
} else {
|
|
182
|
+
record('MCP 写入(无 Server,跳过)', true, '0 servers')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// catalog rescan(可能 200 / 501 / 409)
|
|
186
|
+
const rs = await req('POST', '/api/prompt-customizer/catalog/rescan')
|
|
187
|
+
record('catalog/rescan 可调用', [200, 409, 501].includes(rs.status), `status=${rs.status} ${rs.json?.error ?? ''}`)
|
|
188
|
+
} finally {
|
|
189
|
+
// 恢复整库
|
|
190
|
+
const restore = await req('POST', '/api/prompt-customizer/restore', { docs: backup.json?.docs ?? {}, catalog: backup.json?.catalog ?? {} })
|
|
191
|
+
record('恢复整库 /restore', restore.status === 200 && restore.json?.ok === true, `status=${restore.status}`)
|
|
192
|
+
const after = await req('GET', '/api/prompt-customizer/config')
|
|
193
|
+
const a = JSON.stringify(after.json?.config?.overrides ?? {})
|
|
194
|
+
const b = JSON.stringify(originalOverrides)
|
|
195
|
+
record('恢复后 overrides 与备份一致', a === b, a === b ? '' : '不一致')
|
|
196
|
+
try { fs.unlinkSync(backupFile) } catch { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await main()
|
|
202
|
+
} catch (e) {
|
|
203
|
+
record('主流程异常', false, e instanceof Error ? e.stack ?? e.message : String(e))
|
|
204
|
+
} finally {
|
|
205
|
+
const passed = results.filter((r) => r.ok).length
|
|
206
|
+
const failed = results.length - passed
|
|
207
|
+
console.log(`\n==== 汇总: ${passed}/${results.length} 通过, ${failed} 失败 ====`)
|
|
208
|
+
try { fs.writeFileSync(path.join(process.cwd(), 'scripts', 'e2e-report.json'), JSON.stringify({ when: new Date().toISOString(), url: ORIGIN, destructive: DESTRUCTIVE, passed, failed, results }, null, 2)) } catch { /* ignore */ }
|
|
209
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
210
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端到端模拟:用真实投递的 `lib/index.js`,在隔离的临时 DSH_HOME 里跑
|
|
3
|
+
* `/api/prompt-customizer/catalog/rescan`,验证「积压 → 重扫 → 对齐真实注册表」。
|
|
4
|
+
*
|
|
5
|
+
* 不碰用户 profile:DSH_HOME 是临时目录,假 roster 与假装配都在本进程内,
|
|
6
|
+
* 只有被测代码是真实构建产物。真实的 4 个预设各自贡献的段/工具用 stub 装配
|
|
7
|
+
* 按真实形状(含 dsh_browser_* 这类已卸载的历史工具)构造。
|
|
8
|
+
*
|
|
9
|
+
* Usage: node scripts/simulate-rescan.mjs
|
|
10
|
+
*
|
|
11
|
+
* 状态(2.1.0):本脚本基于 ADR-0001 已删除的「目录累积池 + /catalog/rescan」,
|
|
12
|
+
* 现在跑不通,保留作历史材料。全域接管的当前验证见
|
|
13
|
+
* test/universal-prompt-takeover.test.mjs 与 `npm run check`。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import os from 'node:os'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
import { pathToFileURL } from 'node:url'
|
|
20
|
+
import { parse } from 'yaml'
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(import.meta.dirname, '..')
|
|
23
|
+
|
|
24
|
+
const fail = (msg) => { console.error(`FAIL ${msg}`); process.exitCode = 1 }
|
|
25
|
+
const pass = (msg) => console.log(`ok ${msg}`)
|
|
26
|
+
|
|
27
|
+
// ── 一个隔离的 DSH_HOME,预置「积压」登记表 ────────────────────────────────
|
|
28
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'pc-rescan-sim-'))
|
|
29
|
+
const dataDir = path.join(home, 'prompt-customizer')
|
|
30
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
31
|
+
const catalogFile = path.join(dataDir, 'catalog.yaml')
|
|
32
|
+
|
|
33
|
+
// 积压:407 个工具里 307 个早已不存在(这里取真实样本形状),45 个段里 44 个是幽灵。
|
|
34
|
+
const staleTools = Array.from({ length: 307 }, (_, i) => ({
|
|
35
|
+
name: i < 20 ? `dsh_browser_${['open', 'click', 'type', 'scroll', 'screenshot'][i % 5]}_${i}` : `ghost_tool_${i}`,
|
|
36
|
+
description: 'stale — the plugin that registered this is long gone',
|
|
37
|
+
}))
|
|
38
|
+
const liveTools = [
|
|
39
|
+
{ name: 'bash', description: 'run a shell' },
|
|
40
|
+
{ name: 'edit', description: 'edit a file' },
|
|
41
|
+
{ name: 'grep', description: 'search' },
|
|
42
|
+
]
|
|
43
|
+
const staleSections = Array.from({ length: 44 }, (_, i) => ({ name: `ghost:section:${i}`, text: 'old' }))
|
|
44
|
+
const liveSections = [{ name: 'persona', text: 'You are a coding agent.' }]
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(catalogFile, [
|
|
47
|
+
'tools:',
|
|
48
|
+
...[...staleTools, ...liveTools].map((t) => ` - name: ${t.name}\n description: ${JSON.stringify(t.description)}`),
|
|
49
|
+
'sections:',
|
|
50
|
+
...[...staleSections, ...liveSections].map((s) => ` - name: ${s.name}\n text: ${JSON.stringify(s.text)}`),
|
|
51
|
+
'',
|
|
52
|
+
].join('\n'))
|
|
53
|
+
|
|
54
|
+
// ── 真实的宿主替身:4 个预设,各自贡献不同的段/工具 ────────────────────────
|
|
55
|
+
const PRESET_VIEWS = {
|
|
56
|
+
standard: {
|
|
57
|
+
sections: [{ name: 'persona', text: 'You are a coding agent.' }, { name: 'guidelines', text: 'Be careful.' }],
|
|
58
|
+
tools: [{ name: 'bash', description: 'run a shell' }, { name: 'edit', description: 'edit a file' }, { name: 'grep', description: 'search' }],
|
|
59
|
+
},
|
|
60
|
+
minimal: {
|
|
61
|
+
sections: [{ name: 'persona', text: 'You are a coding agent.' }],
|
|
62
|
+
tools: [{ name: 'bash', description: 'run a shell' }],
|
|
63
|
+
},
|
|
64
|
+
ptc: {
|
|
65
|
+
sections: [{ name: 'persona', text: 'You are a coding agent.' }],
|
|
66
|
+
tools: [{ name: 'run_code', description: 'PTC transport' }],
|
|
67
|
+
},
|
|
68
|
+
cordis: {
|
|
69
|
+
sections: [{ name: 'persona', text: 'You are a coding agent.' }, { name: 'cordis:composition', text: 'compose plugins' }],
|
|
70
|
+
tools: [{ name: 'bash', description: 'run a shell' }, { name: 'cordis_api_catalog', description: 'catalog' }],
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
// 真实预期并集(4 个预设各自 base 视图去重后):
|
|
74
|
+
const EXPECT_SECTIONS = ['persona', 'guidelines', 'cordis:composition']
|
|
75
|
+
const EXPECT_TOOLS = ['bash', 'edit', 'grep', 'run_code', 'cordis_api_catalog']
|
|
76
|
+
|
|
77
|
+
const routes = {}
|
|
78
|
+
const disposed = []
|
|
79
|
+
const roster = {
|
|
80
|
+
defaultId: 'standard',
|
|
81
|
+
list: async () => Object.keys(PRESET_VIEWS).map((id) => ({ id })),
|
|
82
|
+
acquireScope: async (id) => ({
|
|
83
|
+
key: id,
|
|
84
|
+
async [Symbol.asyncDispose]() { disposed.push(id) },
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const webServer = { register: (r) => { routes[r.path] = r.handler; return () => {} } }
|
|
89
|
+
const ctx = {
|
|
90
|
+
on: () => {},
|
|
91
|
+
// 真实 Cordis 的 effect 立即执行回调并登记 disposer;inject 立即用子作用域调用
|
|
92
|
+
// 回调。桩必须同形,否则路由根本不会注册上(实测踩过)。
|
|
93
|
+
effect: (fn) => { fn?.(); return () => {} },
|
|
94
|
+
inject: (names, callback) => { callback?.(ctx); return () => {} },
|
|
95
|
+
logger: { info: () => {}, warn: (m) => console.log("CTX-WARN:", String(m).slice(0,300)), debug: () => {} },
|
|
96
|
+
// host.ts 有两层:自有模块用 `ctx.webServer` 属性,vendor 半身用 `ctx.get('webServer')`。
|
|
97
|
+
// 两个面都要给,否则提示词半身整块不挂(实测踩过)。
|
|
98
|
+
webServer,
|
|
99
|
+
tools: { schemas: () => [], register: () => () => {} },
|
|
100
|
+
sessionProjections: { register: () => () => {} },
|
|
101
|
+
get: (name) => {
|
|
102
|
+
if (name === 'webServer') return webServer
|
|
103
|
+
if (name === 'agentPresets') return roster
|
|
104
|
+
if (name === 'connection') return { requestRejection: () => undefined }
|
|
105
|
+
if (name === 'settings') return { documentPath: path.join(home, 'settings.yaml') }
|
|
106
|
+
if (name === 'profileContext') return undefined
|
|
107
|
+
return undefined
|
|
108
|
+
},
|
|
109
|
+
systemPrompt: {
|
|
110
|
+
assemble: async (hint) => ({
|
|
111
|
+
sections: [], tools: [], variables: {}, contexts: [],
|
|
112
|
+
layers: { merge: () => new Map() },
|
|
113
|
+
promptCustomizerBaseView: PRESET_VIEWS[hint.scope] ?? { sections: [], tools: [] },
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── 跑真实构建产物 ────────────────────────────────────────────────────────
|
|
119
|
+
const mod = await import(pathToFileURL(path.join(ROOT, 'lib/index.js')).href)
|
|
120
|
+
// host.ts 的 apply 是 async:不 await 会在第一个 await 处就往下走,
|
|
121
|
+
// 结果是只有同步注册的 skill-manager 挂上了(实测踩过)。
|
|
122
|
+
await mod.apply(ctx, { dataDir })
|
|
123
|
+
|
|
124
|
+
const handler = routes['/api/prompt-customizer/catalog/rescan']
|
|
125
|
+
console.log('ROUTES', Object.keys(routes).join(', '))
|
|
126
|
+
if (handler === undefined) fail('catalog/rescan 路由未注册')
|
|
127
|
+
else pass('catalog/rescan 路由已注册')
|
|
128
|
+
|
|
129
|
+
function post() {
|
|
130
|
+
const req = { url: '/api/prompt-customizer/catalog/rescan', method: 'POST', headers: { host: '127.0.0.1' }, on(ev, cb) { if (ev === 'end') cb() } }
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const res = {
|
|
133
|
+
status: 0, body: null,
|
|
134
|
+
writeHead(code) { this.status = code },
|
|
135
|
+
end(text) { this.body = JSON.parse(text); resolve(res) },
|
|
136
|
+
}
|
|
137
|
+
void handler(req, res)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── 断言 ──────────────────────────────────────────────────────────────────
|
|
142
|
+
const before = parse(fs.readFileSync(catalogFile, 'utf8'))
|
|
143
|
+
pass(`积压现状:${before.tools.length} 个工具 / ${before.sections.length} 个段`)
|
|
144
|
+
|
|
145
|
+
const res = await post()
|
|
146
|
+
|
|
147
|
+
if (res.status !== 200) fail(`重扫应 200,实得 ${res.status}: ${JSON.stringify(res.body)}`)
|
|
148
|
+
else pass('重扫返回 200')
|
|
149
|
+
|
|
150
|
+
const report = res.body.report ?? {}
|
|
151
|
+
const afterTools = res.body.catalog?.tools ?? []
|
|
152
|
+
const afterSections = res.body.catalog?.sections ?? []
|
|
153
|
+
|
|
154
|
+
const gotTools = afterTools.map((t) => t.name).sort()
|
|
155
|
+
const gotSections = afterSections.map((s) => s.name).sort()
|
|
156
|
+
|
|
157
|
+
if (JSON.stringify(gotTools) !== JSON.stringify([...EXPECT_TOOLS].sort())) {
|
|
158
|
+
fail(`工具并集应等于真实注册表\n 期望 ${JSON.stringify([...EXPECT_TOOLS].sort())}\n 实得 ${JSON.stringify(gotTools)}`)
|
|
159
|
+
} else pass(`工具并集对齐真实注册表(${gotTools.length} 个)`)
|
|
160
|
+
|
|
161
|
+
if (JSON.stringify(gotSections) !== JSON.stringify([...EXPECT_SECTIONS].sort())) {
|
|
162
|
+
fail(`段并集应等于真实注册表\n 期望 ${JSON.stringify([...EXPECT_SECTIONS].sort())}\n 实得 ${JSON.stringify(gotSections)}`)
|
|
163
|
+
} else pass(`段并集对齐真实注册表(${gotSections.length} 个)`)
|
|
164
|
+
|
|
165
|
+
if (report.tools?.removed === 307) pass(`清掉 307 个已不存在的工具(${report.tools.before} → ${report.tools.after})`)
|
|
166
|
+
else fail(`应清掉 307 个工具,实得 ${JSON.stringify(report.tools)}`)
|
|
167
|
+
|
|
168
|
+
if (report.sections?.removed === 44) pass(`清掉 44 个已不存在的段(${report.sections.before} → ${report.sections.after})`)
|
|
169
|
+
else fail(`应清掉 44 个段,实得 ${JSON.stringify(report.sections)}`)
|
|
170
|
+
|
|
171
|
+
if (JSON.stringify(disposed.sort()) === JSON.stringify(Object.keys(PRESET_VIEWS).sort())) pass('每个预设的作用域租约都已释放')
|
|
172
|
+
else fail(`租约释放不全:${JSON.stringify(disposed)}`)
|
|
173
|
+
|
|
174
|
+
// 落盘:重启后不会被历史条目捞回来
|
|
175
|
+
const onDisk = parse(fs.readFileSync(catalogFile, 'utf8'))
|
|
176
|
+
if (onDisk.tools.length === EXPECT_TOOLS.length && onDisk.sections.length === EXPECT_SECTIONS.length) {
|
|
177
|
+
pass('登记表已落盘(重启不会捞回历史条目)')
|
|
178
|
+
} else fail(`落盘内容不对:${onDisk.tools.length} 工具 / ${onDisk.sections.length} 段`)
|
|
179
|
+
|
|
180
|
+
if ((res.body.skipped ?? []).length === 0) pass('无预设被跳过')
|
|
181
|
+
else fail(`不应有跳过的预设:${JSON.stringify(res.body.skipped)}`)
|
|
182
|
+
|
|
183
|
+
fs.rmSync(home, { recursive: true, force: true })
|
|
184
|
+
console.log(process.exitCode === 1 ? '\nSIMULATION FAILED' : '\nSIMULATION PASSED — 积压清理按真实注册表对齐')
|