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
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-prompt-customizer — MCP 工具级禁用(全局层 / 预设层 / 自带行分账)回归测试。
|
|
3
|
-
*
|
|
4
|
-
* 直接跑构建产物 lib/index.js:DSH_HOME / DSH_AGENTS_HOME 指到临时目录,
|
|
5
|
-
* 不碰用户真实预设、技能与账本。0.1.7 起预设是声明式插件行(config.plugins),
|
|
6
|
-
* 因此「自带行」直接写在假预设行的 plugins 里;写路径经假 configEditor.edit()
|
|
7
|
-
* 回写同一份数据。覆盖:
|
|
8
|
-
* - 脏账本(v2)归一化与 own + inherit 两侧升级;
|
|
9
|
-
* - serverName 最长匹配、装配过滤(system-prompt/assemble waterfall);
|
|
10
|
-
* - agent 作用域 deny:全局行 ∪ 本预设 inherit,排除预设自带(作用域内名字);
|
|
11
|
-
* - 同名彻底独立:自带行存在时全局层/ inherit 条目的分账语义;
|
|
12
|
-
* - 名单缓存(无活动 agent 时仍能列全并整台开关)。
|
|
13
|
-
*
|
|
14
|
-
* Usage: node scripts/test-mcp-tool-disable.mjs
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"
|
|
18
|
-
import { tmpdir } from "node:os"
|
|
19
|
-
import { join } from "node:path"
|
|
20
|
-
import { pathToFileURL } from "node:url"
|
|
21
|
-
import { EventEmitter } from "node:events"
|
|
22
|
-
|
|
23
|
-
const sandbox = mkdtempSync(join(tmpdir(), "dsh-prompt-customizer-tool-"))
|
|
24
|
-
process.env.DSH_AGENTS_HOME = join(sandbox, "agents")
|
|
25
|
-
process.env.DSH_HOME = join(sandbox, "dsh")
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 预设声明行:code 自带两台 MCP —— web(与全局同名)与 ownonly(全局没有)。
|
|
29
|
-
* 行的 options.config.plugins 就是该预设的子行清单。
|
|
30
|
-
*/
|
|
31
|
-
const presetRows = new Map()
|
|
32
|
-
const presetRow = (id, order, plugins) => ({
|
|
33
|
-
options: { id: `preset-${id}`, name: "@deepseek-ai/dsh-agent-preset", config: { id, order, plugins } },
|
|
34
|
-
})
|
|
35
|
-
presetRows.set("code", presetRow("code", 2, [
|
|
36
|
-
{ id: "mcp-web", name: "@deepseek-ai/dsh-mcp-client", config: { serverName: "web", transport: "stdio", command: "npx" } },
|
|
37
|
-
{ id: "mcp-ownonly", name: "@deepseek-ai/dsh-mcp-client", config: { serverName: "ownonly", transport: "stdio", command: "npx" } },
|
|
38
|
-
]))
|
|
39
|
-
presetRows.set("minimal", presetRow("minimal", 1, []))
|
|
40
|
-
|
|
41
|
-
/* 脏账本(v2 形状):全局层 github、预设层 code/my_server(v2 数组 → 应升级成
|
|
42
|
-
* own + inherit 两侧);其余名字非法,该被丢弃。 */
|
|
43
|
-
const ledgerFile = join(process.env.DSH_HOME, "mcp", "dsh-prompt-customizer", "tool-disable.json")
|
|
44
|
-
mkdirSync(join(process.env.DSH_HOME, "mcp", "dsh-prompt-customizer"), { recursive: true })
|
|
45
|
-
writeFileSync(ledgerFile, JSON.stringify({
|
|
46
|
-
version: 2,
|
|
47
|
-
disabled: { github: ["mcp__github__search", "evil", "mcp__other__x", "mcp__github__"] },
|
|
48
|
-
presets: { code: { my_server: ["mcp__my_server__deep"] } },
|
|
49
|
-
}))
|
|
50
|
-
|
|
51
|
-
const maskLedgerFile = join(process.env.DSH_HOME, "mcp", "dsh-prompt-customizer", "preset-masks.json")
|
|
52
|
-
|
|
53
|
-
/* ── 断言小工具 ─────────────────────────────────────────────────────── */
|
|
54
|
-
let failed = 0
|
|
55
|
-
function check(label, condition) {
|
|
56
|
-
if (condition) {
|
|
57
|
-
console.log(`ok ${label}`)
|
|
58
|
-
} else {
|
|
59
|
-
failed += 1
|
|
60
|
-
console.error(`FAIL ${label}`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const settle = () => new Promise(resolve => setTimeout(resolve, 20))
|
|
65
|
-
|
|
66
|
-
/* ── 假的宿主面 ─────────────────────────────────────────────────────── */
|
|
67
|
-
const routes = new Map()
|
|
68
|
-
const restrictCalls = []
|
|
69
|
-
const assembleHandlers = []
|
|
70
|
-
const eventHandlers = new Map()
|
|
71
|
-
|
|
72
|
-
const globalSchemas = [
|
|
73
|
-
{ name: "mcp__github__search", description: "search repos" },
|
|
74
|
-
{ name: "mcp__github__create_issue", description: "create an issue" },
|
|
75
|
-
{ name: "mcp__web__fetch", description: "fetch (全局那台)" },
|
|
76
|
-
{ name: "mcp__ownonly__sync", description: "sync (预设自带)" },
|
|
77
|
-
{ name: "mcp__maskme__alpha", description: "alpha" },
|
|
78
|
-
{ name: "mcp__maskme__beta", description: "beta" },
|
|
79
|
-
{ name: "mcp__my__ping", description: "ping" },
|
|
80
|
-
{ name: "mcp__my_server__deep", description: "deep" },
|
|
81
|
-
{ name: "read", description: "read a file" },
|
|
82
|
-
]
|
|
83
|
-
|
|
84
|
-
const agentServices = {
|
|
85
|
-
tools: {
|
|
86
|
-
schemas: () => globalSchemas,
|
|
87
|
-
restrict: (filter) => {
|
|
88
|
-
restrictCalls.push(filter)
|
|
89
|
-
return () => {}
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
const codeAgent = { id: "agent-1", presetId: "code", ctx: { presetId: "code", get: (name) => agentServices[name], inject: () => () => {} } }
|
|
94
|
-
let liveAgents = [codeAgent]
|
|
95
|
-
|
|
96
|
-
const overrides = new Map()
|
|
97
|
-
let editCalls = 0
|
|
98
|
-
|
|
99
|
-
const ctx = {
|
|
100
|
-
logger: { info: () => {}, warn: () => {}, debug: () => {} },
|
|
101
|
-
webServer: { register: (route) => { routes.set(route.path, route.handler); return () => {} } },
|
|
102
|
-
tools: { register: () => () => {}, schemas: () => globalSchemas },
|
|
103
|
-
on: (name, handler) => {
|
|
104
|
-
if (name === "system-prompt/assemble") assembleHandlers.push(handler)
|
|
105
|
-
const list = eventHandlers.get(name) ?? []
|
|
106
|
-
list.push(handler)
|
|
107
|
-
eventHandlers.set(name, list)
|
|
108
|
-
return () => {}
|
|
109
|
-
},
|
|
110
|
-
emit: (name, ...args) => { for (const handler of eventHandlers.get(name) ?? []) handler(...args) },
|
|
111
|
-
effect: (fn) => { fn?.(); return () => {} },
|
|
112
|
-
settings: { get: () => ({ providers: {} }), register: () => () => {} },
|
|
113
|
-
credentials: {}, sessions: {}, sessionPersistence: {}, llm: {},
|
|
114
|
-
loader: { entries: () => [...presetRows.values()] },
|
|
115
|
-
get: (name) => {
|
|
116
|
-
if (name === "agentPresets") {
|
|
117
|
-
return {
|
|
118
|
-
list: async () => [
|
|
119
|
-
{ id: "code", name: "Code" },
|
|
120
|
-
{ id: "minimal", name: "Minimal" },
|
|
121
|
-
],
|
|
122
|
-
composedPreset: (agentCtx) => agentCtx?.presetId,
|
|
123
|
-
readDocument: async (presetId) => ({
|
|
124
|
-
agentPreset: presetId,
|
|
125
|
-
content: (presetRows.get(presetId)?.options.config.plugins ?? [])
|
|
126
|
-
.map(row => `- id: ${row.id}\n name: ${row.name}\n`).join(""),
|
|
127
|
-
}),
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (name === "agents") return { list: () => liveAgents }
|
|
131
|
-
if (name === "configEditor") {
|
|
132
|
-
return {
|
|
133
|
-
entries: () => [...presetRows.values()],
|
|
134
|
-
configuration: () => [...presetRows.values()].map(entry => ({
|
|
135
|
-
entry,
|
|
136
|
-
inherited: {},
|
|
137
|
-
override: overrides.get(entry.options.config.id) ?? {},
|
|
138
|
-
})),
|
|
139
|
-
edit: async (entry, change) => {
|
|
140
|
-
editCalls += 1
|
|
141
|
-
const next = change(structuredClone(entry.options.config), {})
|
|
142
|
-
entry.options.config = next
|
|
143
|
-
overrides.set(next.id, next)
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return undefined
|
|
148
|
-
},
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const mod = await import(pathToFileURL(join(process.cwd(), "lib/index.js")).href)
|
|
152
|
-
await mod.apply(ctx, {})
|
|
153
|
-
await settle()
|
|
154
|
-
|
|
155
|
-
/* ── HTTP 小工具 ────────────────────────────────────────────────────── */
|
|
156
|
-
function fakeReq(method, url, body) {
|
|
157
|
-
const req = new EventEmitter()
|
|
158
|
-
req.method = method
|
|
159
|
-
req.url = url
|
|
160
|
-
req.socket = { remoteAddress: "127.0.0.1" }
|
|
161
|
-
req.headers = { host: "localhost" }
|
|
162
|
-
req.destroy = () => {}
|
|
163
|
-
const payload = body === undefined ? undefined : Buffer.from(JSON.stringify(body))
|
|
164
|
-
if (payload !== undefined) {
|
|
165
|
-
setTimeout(() => { req.emit("data", payload); req.emit("end") }, 0)
|
|
166
|
-
} else {
|
|
167
|
-
setTimeout(() => req.emit("end"), 0)
|
|
168
|
-
}
|
|
169
|
-
req[Symbol.asyncIterator] = async function* () { if (payload !== undefined) yield payload }
|
|
170
|
-
return req
|
|
171
|
-
}
|
|
172
|
-
function fakeRes() {
|
|
173
|
-
const res = { status: 0, body: "" }
|
|
174
|
-
res.settled = new Promise(resolve => { res.settle = resolve })
|
|
175
|
-
res.writeHead = status => { res.status = status }
|
|
176
|
-
res.end = body => { res.body = body; res.settle() }
|
|
177
|
-
return res
|
|
178
|
-
}
|
|
179
|
-
async function callOn(routePath, method, url, body) {
|
|
180
|
-
const res = fakeRes()
|
|
181
|
-
routes.get(routePath)(fakeReq(method, url, body), res)
|
|
182
|
-
await res.settled
|
|
183
|
-
return { status: res.status, json: res.body === "" ? undefined : JSON.parse(res.body) }
|
|
184
|
-
}
|
|
185
|
-
const callTools = (method, url, body) => callOn("/api/triad/mcp-tools", method, url, body)
|
|
186
|
-
const callStatus = () => callOn("/api/triad/mcp-status", "GET", "/api/triad/mcp-status")
|
|
187
|
-
|
|
188
|
-
const readLedger = () => JSON.parse(readFileSync(ledgerFile, "utf8"))
|
|
189
|
-
/** 预设层某条行的禁用名。 */
|
|
190
|
-
const entryOf = (presetId, serverName, owner) => readLedger().presets?.[presetId]?.[serverName]?.[owner] ?? []
|
|
191
|
-
const ownServersOf = presetId => (presetRows.get(presetId)?.options.config.plugins ?? [])
|
|
192
|
-
.filter(row => row.name === "@deepseek-ai/dsh-mcp-client")
|
|
193
|
-
.map(row => row.config.serverName)
|
|
194
|
-
|
|
195
|
-
/** 跑一遍装配 waterfall(presetId 缺省 = 无预设,只应用全局行)。 */
|
|
196
|
-
async function assembleTools(names, presetId) {
|
|
197
|
-
const assembly = { tools: names.map(name => ({ name })) }
|
|
198
|
-
const handler = assembleHandlers[0]
|
|
199
|
-
if (handler === undefined) throw new Error("no system-prompt/assemble listener registered")
|
|
200
|
-
const context = presetId === undefined ? {} : { agent: { ctx: { presetId } } }
|
|
201
|
-
let nextCalled = false
|
|
202
|
-
await handler(assembly, context, async () => { nextCalled = true; return assembly })
|
|
203
|
-
return { names: assembly.tools.map(tool => tool.name), nextCalled }
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const visible = (list, name) => list.includes(name)
|
|
207
|
-
const ALL = [
|
|
208
|
-
"mcp__github__search", "mcp__github__create_issue", "mcp__web__fetch", "mcp__ownonly__sync",
|
|
209
|
-
"mcp__maskme__alpha", "mcp__maskme__beta", "mcp__my__ping", "mcp__my_server__deep", "read",
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
/* ── 1. 脏账本归一化 + v2 升级 + 装配过滤 ───────────────────────────── */
|
|
213
|
-
const before = await assembleTools(ALL, "minimal")
|
|
214
|
-
check("装配过滤可用(waterfall 放行 next)", before.nextCalled === true)
|
|
215
|
-
check("脏账本只保留合法条目:全局行禁用名被剔除", visible(before.names, "mcp__github__search") === false)
|
|
216
|
-
check("同名 server 的其它工具不动", visible(before.names, "mcp__github__create_issue") === true)
|
|
217
|
-
check("非 mcp 工具不动", visible(before.names, "read") === true)
|
|
218
|
-
// v2 的预设层数组在内存里升级成 own + inherit 两侧(磁盘要等首次写入才改写,
|
|
219
|
-
// 所以从 /api/triad/mcp-status 的镜像看)。
|
|
220
|
-
const upgradedNow = await callStatus()
|
|
221
|
-
const upgradedEntry = upgradedNow.json?.toolDisabledByPreset?.code?.my_server
|
|
222
|
-
check("v2 预设层数组升级为 own + inherit 两侧",
|
|
223
|
-
Array.isArray(upgradedEntry?.own) && upgradedEntry.own.includes("mcp__my_server__deep")
|
|
224
|
-
&& Array.isArray(upgradedEntry?.inherit) && upgradedEntry.inherit.includes("mcp__my_server__deep"))
|
|
225
|
-
check("预设自带行从声明行读到(web / ownonly)",
|
|
226
|
-
ownServersOf("code").join(",") === "web,ownonly"
|
|
227
|
-
&& (upgradedNow.json?.presetServers?.code ?? []).length === 2)
|
|
228
|
-
|
|
229
|
-
/* ── 2. 预设层只对所属预设生效 ─────────────────────────────────────── */
|
|
230
|
-
check("v2 升级条目对 code 生效(my_server)",
|
|
231
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__my_server__deep") === false)
|
|
232
|
-
check("同一个工具对别的预设不受影响(minimal)",
|
|
233
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__my_server__deep") === true)
|
|
234
|
-
check("无 agent 上下文的装配只应用全局行",
|
|
235
|
-
visible((await assembleTools(ALL)).names, "mcp__my_server__deep") === true)
|
|
236
|
-
|
|
237
|
-
/* ── 3. serverName 最长匹配:my 与 my_server 不互相牵连 ─────────────── */
|
|
238
|
-
const pingOnly = await assembleTools(["mcp__my__ping", "mcp__my_server__deep"], "code")
|
|
239
|
-
check("下划线 server 只剔除自己的工具", visible(pingOnly.names, "mcp__my__ping") === true)
|
|
240
|
-
const status = await callStatus()
|
|
241
|
-
const statusServes = (status.json?.servers ?? []).map(server => server.serverName)
|
|
242
|
-
check("状态路由按最长匹配归组:my 与 my_server 各自成组",
|
|
243
|
-
statusServes.includes("my") === true && statusServes.includes("my_server") === true)
|
|
244
|
-
const myGroup = (status.json?.servers ?? []).find(server => server.serverName === "my")
|
|
245
|
-
check("my 组里不含 my_server 的工具",
|
|
246
|
-
Array.isArray(myGroup?.tools) && myGroup.tools.some(tool => tool.name === "mcp__my_server__deep") === false)
|
|
247
|
-
check("状态路由暴露按行分账的镜像(v3)",
|
|
248
|
-
Array.isArray(entryOf("code", "my_server", "own"))
|
|
249
|
-
&& status.json?.toolDisabledByPreset?.code?.my_server?.inherit !== undefined)
|
|
250
|
-
|
|
251
|
-
/* ── 4. agent 作用域 deny:全局行 ∪ 本预设 inherit,排除自带 ────────── */
|
|
252
|
-
await settle()
|
|
253
|
-
const denyOf = () => restrictCalls.flatMap(call => call.deny ?? [])
|
|
254
|
-
check("全局行禁用名进 deny", denyOf().includes("mcp__github__search") === true)
|
|
255
|
-
check("预设 inherit 条目进 deny(code)", denyOf().includes("mcp__my_server__deep") === true)
|
|
256
|
-
check("预设自带工具不进 deny(作用域内名字会抛错)", denyOf().includes("mcp__web__fetch") === false)
|
|
257
|
-
check("预设自带工具也不因 inherit 条目进 deny", denyOf().includes("mcp__ownonly__sync") === false)
|
|
258
|
-
|
|
259
|
-
/* ── 5. 同名彻底独立(核心)──────────────────────────────────────────
|
|
260
|
-
* code 自带 web(覆盖全局那条):
|
|
261
|
-
* - own 条目 → 生效;inherit 条目 → 不生效(那条此刻不提供工具);
|
|
262
|
-
* - 全局层禁用 → 对 code 不生效(提供者是自带行),对 minimal 生效。 */
|
|
263
|
-
const ownOff = await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: false, tools: ["mcp__web__fetch"], preset: "code", source: "own" })
|
|
264
|
-
check("自带行禁用:200,且响应标注 source=own", ownOff.status === 200 && ownOff.json?.source === "own")
|
|
265
|
-
check("写入 own 条目", entryOf("code", "web", "own").includes("mcp__web__fetch"))
|
|
266
|
-
check("inherit 条目未被动过", entryOf("code", "web", "inherit").length === 0)
|
|
267
|
-
check("自带行生效:code 的该工具被剔除", visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === false)
|
|
268
|
-
|
|
269
|
-
const inheritOff = await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: false, tools: ["mcp__web__fetch"], preset: "code", source: "inherit" })
|
|
270
|
-
check("继承行禁用:200,写入 inherit 条目", inheritOff.status === 200 && entryOf("code", "web", "inherit").includes("mcp__web__fetch"))
|
|
271
|
-
check("inherit 与 own 分账,互不覆盖", entryOf("code", "web", "own").includes("mcp__web__fetch") === true)
|
|
272
|
-
check("继承行此刻不生效(提供者是自带行):工具仍被 own 条目剔除,而非被 inherit 影响",
|
|
273
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === false)
|
|
274
|
-
|
|
275
|
-
/* 只清 own,保留 inherit:自带行在时 inherit 不生效(各记各的账) */
|
|
276
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: true, tools: ["mcp__web__fetch"], preset: "code", source: "own" })
|
|
277
|
-
check("清 own 后 inherit 条目仍在", entryOf("code", "web", "inherit").includes("mcp__web__fetch"))
|
|
278
|
-
check("自带行仍是提供者 → inherit 条目暂不生效:工具可见(两个开关各记各的账)",
|
|
279
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === true)
|
|
280
|
-
check("对别的预设(minimal,继承全局行)也不可见(那条只是 code 的 inherit,全局行没禁)",
|
|
281
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__web__fetch") === true)
|
|
282
|
-
|
|
283
|
-
/* 全局层禁用 web:对 minimal 生效,对自带同名的 code 不生效(彻底独立) */
|
|
284
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: false, tools: ["mcp__web__fetch"] })
|
|
285
|
-
check("全局层禁用:minimal(继承全局行)不可见",
|
|
286
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__web__fetch") === false)
|
|
287
|
-
check("全局层禁用:code(自带同名行)不受影响 —— 同名彻底独立",
|
|
288
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === true)
|
|
289
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: true, tools: ["mcp__web__fetch"] })
|
|
290
|
-
|
|
291
|
-
/* ── 6. 全局一票否决 + 个性化偏好记忆(全局关→开往返)───────────────── */
|
|
292
|
-
/* 个性化偏好:预设 code 单独遮蔽 create_issue */
|
|
293
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: false, tools: ["mcp__github__create_issue"], preset: "code", source: "inherit" })
|
|
294
|
-
check("预设层写入个性化遮蔽偏好", entryOf("code", "github", "inherit").includes("mcp__github__create_issue"))
|
|
295
|
-
/* 全局关闭:一票否决,所有预设都不可见 */
|
|
296
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: false, tools: ["mcp__github__create_issue"] })
|
|
297
|
-
check("全局关闭:code 不可见", visible((await assembleTools(ALL, "code")).names, "mcp__github__create_issue") === false)
|
|
298
|
-
check("全局关闭:minimal 同样不可见(一票否决覆盖所有预设)",
|
|
299
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__github__create_issue") === false)
|
|
300
|
-
/* 全局关闭期间:预设层不可拨动(显式拒绝,且不改写偏好)*/
|
|
301
|
-
const veto = await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: true, tools: ["mcp__github__create_issue"], preset: "code", source: "inherit" })
|
|
302
|
-
check("全局关闭期间预设层不能打开(409)", veto.status === 409 && veto.json?.ok === false)
|
|
303
|
-
check("被拒的请求不改写偏好(遮蔽条目仍在)", entryOf("code", "github", "inherit").includes("mcp__github__create_issue"))
|
|
304
|
-
const statusVeto = await callStatus()
|
|
305
|
-
check("面板数据仍标出全局层禁用(据此置灰 + 『全局已停用』标签)",
|
|
306
|
-
(statusVeto.json?.toolDisabled?.github ?? []).includes("mcp__github__create_issue"))
|
|
307
|
-
/* 全局重新开启:偏好自动还原 */
|
|
308
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: true, tools: ["mcp__github__create_issue"] })
|
|
309
|
-
check("全局恢复:个性化遮蔽自动还原(code 仍不可见)",
|
|
310
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__github__create_issue") === false)
|
|
311
|
-
check("全局恢复:未单独设置的预设回到「继承使用」(minimal 可见)",
|
|
312
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__github__create_issue") === true)
|
|
313
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: true, tools: ["mcp__github__create_issue"], preset: "code", source: "inherit" })
|
|
314
|
-
|
|
315
|
-
/* ── 7. 预设自带 Server 的独立账目(ownonly 全局没有)──────────────── */
|
|
316
|
-
const ownOnlyOff = await callTools("PUT", "/api/triad/mcp-tools/ownonly", { enabled: false, tools: ["mcp__ownonly__sync"], preset: "code", source: "own" })
|
|
317
|
-
check("ownonly:写入 own 条目", ownOnlyOff.status === 200 && entryOf("code", "ownonly", "own").includes("mcp__ownonly__sync"))
|
|
318
|
-
check("ownonly:全局层没有任何条目", readLedger().disabled?.ownonly === undefined)
|
|
319
|
-
check("ownonly:code 不可见", visible((await assembleTools(ALL, "code")).names, "mcp__ownonly__sync") === false)
|
|
320
|
-
check("ownonly:minimal 本来就没有它,也不受影响", visible((await assembleTools(ALL, "minimal")).names, "mcp__ownonly__sync") === true)
|
|
321
|
-
|
|
322
|
-
/* ── 8. 名单缓存:没有活动 agent 也能列全并整台开关 ────────────────── */
|
|
323
|
-
liveAgents = []
|
|
324
|
-
const statusNoAgent = await callStatus()
|
|
325
|
-
const webRow = (statusNoAgent.json?.presetServers?.code ?? []).find(row => row.serverName === "web")
|
|
326
|
-
check("无活动 agent 时预设自带 Server 仍列出工具(缓存)",
|
|
327
|
-
Array.isArray(webRow?.tools) && webRow.tools.some(tool => tool.name === "mcp__web__fetch"))
|
|
328
|
-
check("声明行读取不依赖活动 agent(行数 = 2)",
|
|
329
|
-
(statusNoAgent.json?.presetServers?.code ?? []).length === 2)
|
|
330
|
-
const wholeOff = await callTools("PUT", "/api/triad/mcp-tools/ownonly", { enabled: false, preset: "code", source: "own" })
|
|
331
|
-
check("无活动 agent 时也能整台关闭(tools 省略走缓存)",
|
|
332
|
-
wholeOff.status === 200 && Array.isArray(wholeOff.json?.tools) && wholeOff.json.tools.includes("mcp__ownonly__sync"))
|
|
333
|
-
liveAgents = [codeAgent]
|
|
334
|
-
|
|
335
|
-
/* ── 9. 同名覆盖:遮蔽开关仍可用(面板现场反馈那个 case)────────────── */
|
|
336
|
-
const coveredMask = await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/web", { enabled: false })
|
|
337
|
-
await settle()
|
|
338
|
-
check("预设自带同名 Server 时,全局那一条仍可遮蔽(200)", coveredMask.status === 200 && coveredMask.json?.ok === true)
|
|
339
|
-
check("遮蔽账本写入 presets.code.web=false",
|
|
340
|
-
JSON.parse(readFileSync(maskLedgerFile, "utf8")).presets?.code?.web === false)
|
|
341
|
-
check("遮蔽同名覆盖不误伤自带工具(deny 仍不含它)", denyOf().includes("mcp__web__fetch") === false)
|
|
342
|
-
|
|
343
|
-
/* ── 9b. 撤掉自带行 → inherit 账目接管(分账的实际意义)────────────── */
|
|
344
|
-
await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/web", { enabled: true })
|
|
345
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: false, tools: ["mcp__web__fetch"], preset: "code", source: "inherit" })
|
|
346
|
-
check("自带行在时 inherit 条目仍不生效(工具可见)",
|
|
347
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === true)
|
|
348
|
-
const editBefore = editCalls
|
|
349
|
-
const delOwn = await callOn("/api/triad/mcp-presets", "DELETE", "/api/triad/mcp-presets/code/servers/web")
|
|
350
|
-
check("移除预设自带行:200", delOwn.status === 200 && delOwn.json?.ok === true)
|
|
351
|
-
check("移除走 configEditor.edit 且只动声明行",
|
|
352
|
-
editCalls === editBefore + 1 && ownServersOf("code").join(",") === "ownonly")
|
|
353
|
-
await callStatus()
|
|
354
|
-
check("撤掉自带行后 inherit 接管:工具不可见(全局行这条'不要'的账开始生效)",
|
|
355
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === false)
|
|
356
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: true, tools: ["mcp__web__fetch"], preset: "code", source: "inherit" })
|
|
357
|
-
check("清 inherit 后可见恢复", visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === true)
|
|
358
|
-
|
|
359
|
-
/* ── 9c. 遮蔽经由装配过滤生效(面板反馈:遮蔽了但工具仍被注入)─────────
|
|
360
|
-
* restrict 若安装失败或与工具注册错位,native 目录这一层靠装配过滤兜底。 */
|
|
361
|
-
const beforeMask = await assembleTools(ALL, "code")
|
|
362
|
-
check("遮蔽前:maskme 的工具在 code 可见(两条都在)",
|
|
363
|
-
visible(beforeMask.names, "mcp__maskme__alpha") && visible(beforeMask.names, "mcp__maskme__beta"))
|
|
364
|
-
const maskPut = await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/maskme", { enabled: false })
|
|
365
|
-
await settle()
|
|
366
|
-
const afterMask = await assembleTools(ALL, "code")
|
|
367
|
-
check("遮蔽后:装配过滤把该 server 的工具全部剔除(不依赖 restrict)",
|
|
368
|
-
visible(afterMask.names, "mcp__maskme__alpha") === false
|
|
369
|
-
&& visible(afterMask.names, "mcp__maskme__beta") === false)
|
|
370
|
-
check("遮蔽只对本预设生效:minimal 仍可见",
|
|
371
|
-
visible((await assembleTools(ALL, "minimal")).names, "mcp__maskme__alpha") === true)
|
|
372
|
-
check("遮蔽 PUT 返回安装诊断",
|
|
373
|
-
typeof maskPut.json?.install === "object" && maskPut.json.install !== null
|
|
374
|
-
&& typeof maskPut.json.install.agents === "number")
|
|
375
|
-
const statusMaskInstall = await callStatus()
|
|
376
|
-
check("状态路由暴露遮蔽安装诊断", typeof statusMaskInstall.json?.maskInstall?.code === "object")
|
|
377
|
-
/* 同名自带行在提供工具时,遮蔽全局那条不该误伤它(9b 删过自带 web 行,先加回来) */
|
|
378
|
-
const readdOwn = await callOn("/api/triad/mcp-presets", "POST", "/api/triad/mcp-presets/code/servers", {
|
|
379
|
-
format: "json",
|
|
380
|
-
text: JSON.stringify({ mcpServers: { web: { command: "npx", args: ["-y", "web-mcp"] } } }),
|
|
381
|
-
})
|
|
382
|
-
check("重新加回预设自带 web 行:200", readdOwn.status === 200 && readdOwn.json?.ok === true)
|
|
383
|
-
check("加回的行落在声明行 plugins 里", ownServersOf("code").includes("web"))
|
|
384
|
-
await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/web", { enabled: false })
|
|
385
|
-
await settle()
|
|
386
|
-
check("遮蔽被同名自带行覆盖的 server:工具仍可见(自带那台在提供)",
|
|
387
|
-
visible((await assembleTools(ALL, "code")).names, "mcp__web__fetch") === true)
|
|
388
|
-
await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/web", { enabled: true })
|
|
389
|
-
await callOn("/api/triad/mcp-masks", "PUT", "/api/triad/mcp-masks/code/maskme", { enabled: true })
|
|
390
|
-
await settle()
|
|
391
|
-
|
|
392
|
-
/* ── 10. 非法输入 ──────────────────────────────────────────────────── */
|
|
393
|
-
check("非法 preset 被拒(400)", (await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: false, preset: "Bad Id" })).status === 400)
|
|
394
|
-
check("非法 source 被拒(400)", (await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: false, preset: "code", source: "both" })).status === 400)
|
|
395
|
-
check("不属于该 server 的名字被拒(400)", (await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: false, tools: ["mcp__other__x"] })).status === 400)
|
|
396
|
-
check("缺 enabled 被拒(400)", (await callTools("PUT", "/api/triad/mcp-tools/github", { tools: ["mcp__github__search"] })).status === 400)
|
|
397
|
-
check("GET 无路由(404)", (await callTools("GET", "/api/triad/mcp-tools/github")).status === 404)
|
|
398
|
-
|
|
399
|
-
/* ── 11. 清空所有条目 ──────────────────────────────────────────────── */
|
|
400
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: true, tools: ["mcp__github__search", "mcp__github__create_issue"] })
|
|
401
|
-
await callTools("PUT", "/api/triad/mcp-tools/github", { enabled: true, tools: ["mcp__github__create_issue"], preset: "code", source: "inherit" })
|
|
402
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: true, tools: ["mcp__web__fetch"], preset: "code", source: "inherit" })
|
|
403
|
-
await callTools("PUT", "/api/triad/mcp-tools/web", { enabled: true, tools: ["mcp__web__fetch"], preset: "code", source: "own" })
|
|
404
|
-
await callTools("PUT", "/api/triad/mcp-tools/ownonly", { enabled: true, tools: ["mcp__ownonly__sync"], preset: "code", source: "own" })
|
|
405
|
-
await callTools("PUT", "/api/triad/mcp-tools/my_server", { enabled: true, tools: ["mcp__my_server__deep"], preset: "code", source: "own" })
|
|
406
|
-
await callTools("PUT", "/api/triad/mcp-tools/my_server", { enabled: true, tools: ["mcp__my_server__deep"], preset: "code", source: "inherit" })
|
|
407
|
-
const emptyLedger = readLedger()
|
|
408
|
-
check("两层账本清空后无残留",
|
|
409
|
-
Object.keys(emptyLedger.disabled ?? {}).length === 0
|
|
410
|
-
&& Object.values(emptyLedger.presets ?? {}).every(table => Object.keys(table).length === 0))
|
|
411
|
-
check("清空后装配目录完整", (await assembleTools(ALL, "code")).names.length === ALL.length)
|
|
412
|
-
check("名单缓存保留(下次打开面板仍能列出工具名)",
|
|
413
|
-
(emptyLedger.known?.web ?? []).includes("mcp__web__fetch"))
|
|
414
|
-
|
|
415
|
-
rmSync(sandbox, { recursive: true, force: true })
|
|
416
|
-
if (failed > 0) {
|
|
417
|
-
console.error(`\nMCP-TOOL-DISABLE TEST FAILED — ${failed} 项未通过`)
|
|
418
|
-
process.exit(1)
|
|
419
|
-
}
|
|
420
|
-
console.log("\nMCP-TOOL-DISABLE TEST PASSED")
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/** Run the exact installed dsh through a temporary headless profile and overlay. */
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { join, resolve } from 'node:path'
|
|
6
|
-
import { pathToFileURL } from 'node:url'
|
|
7
|
-
import { spawnSync } from 'node:child_process'
|
|
8
|
-
import yaml from 'js-yaml'
|
|
9
|
-
const manifest = process.env.PC_DSH_MANIFEST
|
|
10
|
-
if (!manifest) {
|
|
11
|
-
console.log('SKIP rc.2 runtime: set PC_DSH_MANIFEST to the exact installed dsh package.json')
|
|
12
|
-
process.exit(0)
|
|
13
|
-
}
|
|
14
|
-
const require = createRequire(manifest)
|
|
15
|
-
if (require(manifest).version !== '0.2.0-rc.2') throw new Error('This smoke targets exactly 0.2.0-rc.2')
|
|
16
|
-
const root = mkdtempSync(join(tmpdir(), 'pc-real-rc2-'))
|
|
17
|
-
try {
|
|
18
|
-
const env = { ...process.env, DSH_HOME: root, PC_DSH_MANIFEST: manifest, PC_DATA_DIR: join(root, 'prompt-data'), PC_COMPOSITION_URL: pathToFileURL(resolve('scripts/fixtures/rc2.cordis.yml')).href, DSH_TELEMETRY_MODE: 'OFF' }
|
|
19
|
-
const composition = yaml.load(require('node:fs').readFileSync(resolve('scripts/fixtures/rc2.cordis.yml'), 'utf8'))
|
|
20
|
-
composition.unshift({ id: 'config-editor', name: '@deepseek-ai/dsh-config-editor' }, { id: 'settings', name: '@deepseek-ai/dsh-settings' })
|
|
21
|
-
composition.push({ id: 'prompt-customizer-artifact', name: pathToFileURL(resolve('lib/index.js')).href })
|
|
22
|
-
for (const entry of composition) entry.name = entry.name.startsWith('file:') ? entry.name : entry.name.startsWith('.')
|
|
23
|
-
? pathToFileURL(resolve('scripts/fixtures', entry.name)).href
|
|
24
|
-
: pathToFileURL(require.resolve(entry.name)).href
|
|
25
|
-
const compositionFile = join(root, 'cordis.yml')
|
|
26
|
-
writeFileSync(compositionFile, yaml.dump(composition))
|
|
27
|
-
env.PC_COMPOSITION_URL = pathToFileURL(compositionFile).href
|
|
28
|
-
const bin = join(resolve(manifest, '..'), 'lib/bin.js')
|
|
29
|
-
const dump = spawnSync(process.execPath, [bin, 'headless', '--dump-default-config'], { env, encoding: 'utf8' })
|
|
30
|
-
if (dump.status !== 0) throw new Error(dump.stderr)
|
|
31
|
-
const overlay = join(root, 'probe.overlay.yml')
|
|
32
|
-
const dumpSchema = yaml.DEFAULT_SCHEMA.extend([new yaml.Type('tag:yaml.org,2002:js', { kind: 'scalar', construct: value => value })])
|
|
33
|
-
const patches = yaml.load(dump.stdout, { schema: dumpSchema }).map(entry => ({ id: entry.id, disabled: true }))
|
|
34
|
-
patches.push({ insert: [{ id: 'pc-rc2-test-composition', name: 'cordis:include', config: { path: env.PC_COMPOSITION_URL } }] })
|
|
35
|
-
writeFileSync(overlay, yaml.dump(patches))
|
|
36
|
-
const run = spawnSync(process.execPath, [bin, 'headless', '--patch', overlay], { env, encoding: 'utf8', timeout: 45000 })
|
|
37
|
-
process.stdout.write(run.stdout ?? '')
|
|
38
|
-
process.stderr.write(run.stderr ?? '')
|
|
39
|
-
if (run.status !== 0 || !run.stdout.includes('PC_RC2_SMOKE')) throw new Error(`rc.2 runtime smoke failed: ${run.status} ${run.error ?? ''}`)
|
|
40
|
-
} finally {
|
|
41
|
-
rmSync(root, { recursive: true, force: true })
|
|
42
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-prompt-customizer — 技能开关回归测试(目录名 ≠ frontmatter name 时开关是否还打得动)。
|
|
3
|
-
*
|
|
4
|
-
* 直接跑构建产物 lib/index.js:DSH_AGENTS_HOME / DSH_HOME 指到临时目录,
|
|
5
|
-
* 不碰用户真实技能。Usage: node scripts/test-skill-toggles.mjs
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"
|
|
9
|
-
import { tmpdir } from "node:os"
|
|
10
|
-
import { join } from "node:path"
|
|
11
|
-
import { pathToFileURL } from "node:url"
|
|
12
|
-
import { EventEmitter } from "node:events"
|
|
13
|
-
|
|
14
|
-
const sandbox = mkdtempSync(join(tmpdir(), "dsh-prompt-customizer-toggles-"))
|
|
15
|
-
process.env.DSH_AGENTS_HOME = join(sandbox, "agents")
|
|
16
|
-
process.env.DSH_HOME = join(sandbox, "dsh")
|
|
17
|
-
const agentsSkills = join(process.env.DSH_AGENTS_HOME, "skills")
|
|
18
|
-
mkdirSync(join(process.env.DSH_HOME, "skills"), { recursive: true })
|
|
19
|
-
mkdirSync(agentsSkills, { recursive: true })
|
|
20
|
-
|
|
21
|
-
// 目录名 odd-dir,frontmatter 规范名 canon-name —— 面板/账本给的都是 canon-name
|
|
22
|
-
mkdirSync(join(agentsSkills, "odd-dir"), { recursive: true })
|
|
23
|
-
writeFileSync(join(agentsSkills, "odd-dir", "SKILL.md"), "---\nname: canon-name\ndescription: 目录名不一致的技能\n---\n\nbody\n")
|
|
24
|
-
writeFileSync(join(agentsSkills, ".bundles.json"), JSON.stringify({ version: 1, bundles: [{ id: "odd", name: "Odd", skills: ["odd-dir"] }] }, null, 2))
|
|
25
|
-
|
|
26
|
-
const mod = await import(pathToFileURL(join(process.cwd(), "lib/index.js")).href)
|
|
27
|
-
const routes = new Map()
|
|
28
|
-
const noop = () => () => {}
|
|
29
|
-
const ctx = {
|
|
30
|
-
logger: { info: noop(), warn: noop(), debug: noop() },
|
|
31
|
-
webServer: { register: (route) => { routes.set(route.path, route.handler); return () => {} } },
|
|
32
|
-
tools: { register: noop() },
|
|
33
|
-
on: () => () => {},
|
|
34
|
-
get: () => undefined,
|
|
35
|
-
effect: (fn) => { fn?.(); return () => {} },
|
|
36
|
-
settings: { get: () => ({ providers: {} }), register: () => () => {} },
|
|
37
|
-
credentials: {}, sessions: {}, sessionPersistence: {}, llm: {},
|
|
38
|
-
}
|
|
39
|
-
await mod.apply(ctx, {})
|
|
40
|
-
|
|
41
|
-
function call(prefix, method, url, body) {
|
|
42
|
-
const handler = routes.get(prefix)
|
|
43
|
-
if (handler === undefined) throw new Error(`route ${prefix} not mounted`)
|
|
44
|
-
return new Promise((resolve, reject) => {
|
|
45
|
-
const req = new EventEmitter()
|
|
46
|
-
req.method = method
|
|
47
|
-
req.url = url
|
|
48
|
-
req.headers = { host: "localhost" }
|
|
49
|
-
req.socket = { remoteAddress: "127.0.0.1" }
|
|
50
|
-
const res = {
|
|
51
|
-
statusCode: 0,
|
|
52
|
-
writeHead(status) { this.statusCode = status },
|
|
53
|
-
end(payload) {
|
|
54
|
-
let parsed = payload
|
|
55
|
-
try { parsed = JSON.parse(payload) } catch { /* 原样 */ }
|
|
56
|
-
resolve({ status: this.statusCode, body: parsed })
|
|
57
|
-
},
|
|
58
|
-
}
|
|
59
|
-
handler(req, res)
|
|
60
|
-
queueMicrotask(() => {
|
|
61
|
-
if (body === undefined) { req.emit("end"); return }
|
|
62
|
-
req.emit("data", Buffer.from(JSON.stringify(body)))
|
|
63
|
-
req.emit("end")
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let failed = 0
|
|
69
|
-
const check = (cond, msg) => { if (cond) console.log(`ok ${msg}`); else { failed += 1; console.error(`FAIL ${msg}`) } }
|
|
70
|
-
const TOGGLES = "/api/skill-toggles"
|
|
71
|
-
const SKILL_MD = join(agentsSkills, "odd-dir", "SKILL.md")
|
|
72
|
-
|
|
73
|
-
// 1. 按规范名禁用:旧实现只按目录名定位 → not found,面板开关点了没反应
|
|
74
|
-
const off = await call(TOGGLES, "PUT", "/api/skill-toggles/skills/canon-name", { enabled: false })
|
|
75
|
-
check(off.status === 200 && off.body.ok === true, "按规范名禁用目录名不一致的技能")
|
|
76
|
-
check(readFileSync(SKILL_MD, "utf8").includes("user-invocable: false"), "SKILL.md 写入禁用字段")
|
|
77
|
-
|
|
78
|
-
// 2. 状态表按规范名回报
|
|
79
|
-
const status = await call(TOGGLES, "GET", "/api/skill-toggles/status")
|
|
80
|
-
check(status.body.skills["canon-name"] === false, "status 按规范名回报禁用状态")
|
|
81
|
-
|
|
82
|
-
// 3. 整包开关:账本条目是目录名也要能处理
|
|
83
|
-
const bundleOn = await call(TOGGLES, "PUT", "/api/skill-toggles/bundles/odd", { enabled: true })
|
|
84
|
-
check(bundleOn.status === 200 && bundleOn.body.handled === 1, "整包开关按目录名条目也能命中成员")
|
|
85
|
-
check(!readFileSync(SKILL_MD, "utf8").includes("user-invocable: false"), "整包启用后禁用字段已移除")
|
|
86
|
-
|
|
87
|
-
// 4. 整包状态:目录名条目要能查到规范名的状态
|
|
88
|
-
const off2 = await call(TOGGLES, "PUT", "/api/skill-toggles/skills/canon-name", { enabled: false })
|
|
89
|
-
const status2 = await call(TOGGLES, "GET", "/api/skill-toggles/status")
|
|
90
|
-
check(off2.status === 200 && status2.body.bundles.odd === false, "整包状态跟随成员禁用(不再因别名漏判)")
|
|
91
|
-
|
|
92
|
-
// 5. 预设层:整包关闭要按规范名写账本
|
|
93
|
-
const preset = await call(TOGGLES, "PUT", "/api/skill-toggles/presets/standard/bundles/odd", { enabled: false })
|
|
94
|
-
check(preset.status === 200 && preset.body.changed === 1, "预设层整包关闭写入规范名")
|
|
95
|
-
const ledger = JSON.parse(readFileSync(join(agentsSkills, ".preset-skills.json"), "utf8"))
|
|
96
|
-
check(ledger.presets.standard["canon-name"] === false, "预设账本键为规范名(闸门才遮得住)")
|
|
97
|
-
|
|
98
|
-
rmSync(sandbox, { recursive: true, force: true })
|
|
99
|
-
console.log(failed === 0 ? "\nSKILL-TOGGLES TEST PASSED" : `\nSKILL-TOGGLES TEST FAILED (${String(failed)})`)
|
|
100
|
-
process.exit(failed === 0 ? 0 : 1)
|