dsh-loadout 0.12.0 → 0.12.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/CHANGELOG.md +9 -0
- package/index.js +21 -4
- package/package.json +1 -1
- package/scripts/client-smoke.mjs +8 -6
- package/scripts/selftest.mjs +143 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
「装备栏」是一个 DeepSeek Harness 插件:给每个对话选一套工具(套装),只把用得上的工具投给模型。
|
|
4
4
|
|
|
5
|
+
## 0.12.2
|
|
6
|
+
|
|
7
|
+
- **修「绑定到已不存在的套装」**:如果某个对话绑的套装在应用关着的时候被删掉(旧版本、手改 loadout.json、或别的机器同步过来的文件),启动后它会被当成「空套装」套用——等于把这对话的工具连同内置工具一起裁光,只剩 kit_*。现在这种绑定在第一次被读到时回退到「全部」,并把 ssignments 里那个失效名字改成
|
|
8
|
+
ull,日志里留一行说明。回归断言: binding to a deleted kit falls back to 全部(用第二个模块实例模拟重启)。
|
|
9
|
+
## 0.12.1
|
|
10
|
+
|
|
11
|
+
- **修卡死**:编辑(覆盖保存)、重命名或删除一个正在被对话使用的套装时,宿主会遍历「正在用它的对话」并逐个重新套用工具;这个遍历边改边往 `Map` 里重写条目,迭代器被塞回一个已经走过的键,于是永远走不完——表现是界面点下去没有任何反应、进程占满一个核。现在遍历快照(`[...state]`),编辑与删除都会立刻返回。
|
|
12
|
+
- 测试补齐验收表:`selftest` 新增「同名不同大小写/空格被拒绝」「预置名不能被用户套装占用」「连续改默认不影响已有对话」「删除套装报告并释放全部绑定」「删除默认套装同时清空默认」「改名不动绑定」「写盘不留临时文件」「预置也能当默认」八项断言;`client-smoke` 的预置夹具改为随包发布的五套(仅聊天 / 编程用 / 查找资料 / 项目梳理 / 写作)。
|
|
13
|
+
|
|
5
14
|
## 未发布
|
|
6
15
|
|
|
7
16
|
- 修 `kit_list`:返回值里带 `note`,但输出 schema 没有声明它,宿主按 schema 校验,于是这个工具**每次调用都直接失败**(`kit_list returned invalid output: "note" is not a declared property`)。现在 `note` 是合法字段,渲染时也会在套装名后显示备注;`selftest` 新增断言 `kit_list declares every field it returns`,防止返回值与 schema 再次漂移。
|
package/index.js
CHANGED
|
@@ -1132,8 +1132,19 @@ export function apply(ctx, config = {}) {
|
|
|
1132
1132
|
*/
|
|
1133
1133
|
function ensureBindingApplied(agent) {
|
|
1134
1134
|
if (agent === undefined || agent === null) return
|
|
1135
|
-
|
|
1135
|
+
let value = ensureBinding(agent)
|
|
1136
1136
|
if (value === undefined) return
|
|
1137
|
+
// A stored binding can name a kit that no longer exists — an older build, a
|
|
1138
|
+
// hand-edited store, or a kit deleted while the app was closed. Applying it
|
|
1139
|
+
// would compute an empty allow list and strip the conversation down to the
|
|
1140
|
+
// kit_* tools, so such a conversation falls back to 全部 and the dangling
|
|
1141
|
+
// name is rewritten to stop it resurfacing on every read.
|
|
1142
|
+
if (value !== null && value !== DEFAULT_DSH && resolveKit(lastAgent, value) === undefined) {
|
|
1143
|
+
book.assignments[sessionIdOf(agent)] = null
|
|
1144
|
+
save()
|
|
1145
|
+
ctx.logger?.warn(`dsh-loadout: 套装「${value}」已不存在,这个对话回到「全部」`)
|
|
1146
|
+
value = null
|
|
1147
|
+
}
|
|
1137
1148
|
const selection = selectionForBinding(value)
|
|
1138
1149
|
const current = state.get(agent)?.selection
|
|
1139
1150
|
if (current !== undefined && sameSelection(current, selection)) return
|
|
@@ -1516,7 +1527,7 @@ export function apply(ctx, config = {}) {
|
|
|
1516
1527
|
throw new Error('至少要选一个内置工具或一个插件,否则这个包等于"全部"')
|
|
1517
1528
|
}
|
|
1518
1529
|
if (oldName !== kit.name) {
|
|
1519
|
-
for (const [agent, entry] of state) {
|
|
1530
|
+
for (const [agent, entry] of [...state]) {
|
|
1520
1531
|
if (entry.selection.base === BASE_KIT && entry.selection.kit === oldName) {
|
|
1521
1532
|
entry.selection = { ...entry.selection, kit: kit.name }
|
|
1522
1533
|
entry.label = labelOf(agent, entry.selection)
|
|
@@ -1526,7 +1537,11 @@ export function apply(ctx, config = {}) {
|
|
|
1526
1537
|
// Editing the tools of the kit a conversation is running has to change that
|
|
1527
1538
|
// conversation now: a saved edit that only takes effect after the next
|
|
1528
1539
|
// switch reads as "the edit did nothing".
|
|
1529
|
-
|
|
1540
|
+
//
|
|
1541
|
+
// Iterate a snapshot: `useSelection` deletes and re-adds the agent's entry,
|
|
1542
|
+
// and a Map iterator that is handed back an entry it already passed never
|
|
1543
|
+
// finishes (that is a hang, not a slow loop).
|
|
1544
|
+
for (const [agent, entry] of [...state]) {
|
|
1530
1545
|
if (entry.selection.base === BASE_KIT && entry.selection.kit === kit.name) {
|
|
1531
1546
|
useSelection(agent, { base: BASE_KIT, kit: kit.name, extras: [...entry.selection.extras] })
|
|
1532
1547
|
}
|
|
@@ -1553,7 +1568,9 @@ export function apply(ctx, config = {}) {
|
|
|
1553
1568
|
for (const [sessionId, value] of Object.entries(book.assignments)) {
|
|
1554
1569
|
if (value === kit.name) book.assignments[sessionId] = null
|
|
1555
1570
|
}
|
|
1556
|
-
for
|
|
1571
|
+
// Snapshot for the same reason as `update`: each `useSelection` rewrites the
|
|
1572
|
+
// map entry it belongs to.
|
|
1573
|
+
for (const [agent, entry] of [...state]) {
|
|
1557
1574
|
if (entry.selection.base === BASE_KIT && entry.selection.kit === kit.name) {
|
|
1558
1575
|
useSelection(agent, emptySelection())
|
|
1559
1576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loadout",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Loadout for DeepSeek Harness: group tools into kits, keep only the current kit's tools visible in each conversation, and manage everything from a settings panel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/scripts/client-smoke.mjs
CHANGED
|
@@ -179,11 +179,13 @@ const fixture = {
|
|
|
179
179
|
ok: true,
|
|
180
180
|
source: 'C:/fake/.dsh/loadout.json',
|
|
181
181
|
presets: [
|
|
182
|
+
{ name: 'chat', title: '仅聊天', allow: ['ask_user_question', 'web_search', 'read'] },
|
|
183
|
+
{ name: 'coding', title: '编程用', allow: ['pwsh', 'read'] },
|
|
184
|
+
{ name: 'research', title: '查找资料', allow: ['web_search', 'web_fetch'] },
|
|
185
|
+
{ name: 'plan', title: '项目梳理', allow: ['read', 'write', 'todo_write'] },
|
|
182
186
|
{ name: 'writing', title: '写作', allow: ['read', 'write', 'edit'] },
|
|
183
|
-
{ name: 'research', title: '调研', allow: ['web_search', 'web_fetch'] },
|
|
184
|
-
{ name: 'coding', title: '编程', allow: ['pwsh', 'read'] },
|
|
185
187
|
],
|
|
186
|
-
presetNames: ['
|
|
188
|
+
presetNames: ['chat', 'coding', 'research', 'plan', 'writing'],
|
|
187
189
|
kits: [
|
|
188
190
|
{ name: 'mine', title: '我的套', note: '用于视频制作', builtins: ['read'], plugins: [], allow: ['read'], description: '内置 1 个', tokens: 860, isDefault: true },
|
|
189
191
|
{ name: 'other', title: '另一套', builtins: ['write'], plugins: [], allow: ['write'], description: '内置 1 个', tokens: 420, isDefault: false },
|
|
@@ -246,7 +248,7 @@ if (menu !== undefined) {
|
|
|
246
248
|
check('我的套装 counts user kits only', allText.includes('我的套装(2)'), allText.slice(0, 400))
|
|
247
249
|
// Presets are back from 0.12: they are the quickest way to start a known kind
|
|
248
250
|
// of work, they stay read-only, and each one now says what it leaves out.
|
|
249
|
-
check('menu lists the presets again', allText.includes('预设套装(
|
|
251
|
+
check('menu lists the presets again', allText.includes('预设套装(5)') && allText.includes('写作') && allText.includes('仅聊天'), allText.slice(0, 400))
|
|
250
252
|
check('加挂 counts extras', allText.includes('加挂(1)'), allText.slice(0, 400))
|
|
251
253
|
check('footer text is the approved sentence', allText.includes(FOOTER))
|
|
252
254
|
check('the menu shows the note with the kit name', allText.includes('我的套(用于视频制作)'), allText.slice(0, 400))
|
|
@@ -320,7 +322,7 @@ try {
|
|
|
320
322
|
try {
|
|
321
323
|
const panel = expand(registration.component({ preview: { data: fixture, openSections: { presets: true, kits: true } } }))
|
|
322
324
|
const panelText = textOf(panel)
|
|
323
|
-
check('settings lists the presets again', panelText.includes('预设套装 · 只读(
|
|
325
|
+
check('settings lists the presets again', panelText.includes('预设套装 · 只读(5)') && panelText.includes('写作') && panelText.includes('项目梳理'), panelText.slice(0, 400))
|
|
324
326
|
check('settings lists my kits', panelText.includes('我的套装'), panelText.slice(0, 400))
|
|
325
327
|
check('settings picks a default kit', panelText.includes('新对话默认套装') && panelText.includes('仅内置'), panelText.slice(0, 400))
|
|
326
328
|
check('a note rides next to the kit name', panelText.includes('我的套(用于视频制作)'), panelText.slice(0, 400))
|
|
@@ -335,7 +337,7 @@ try {
|
|
|
335
337
|
const copyButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '复制切换命令')
|
|
336
338
|
const defaultButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '设为默认')
|
|
337
339
|
// 2 defaults + 3 presets + 2 user kits — every card carries both actions.
|
|
338
|
-
check('全部 and 仅内置 are cards with the two actions', copyButtons.length ===
|
|
340
|
+
check('全部 and 仅内置 are cards with the two actions', copyButtons.length === 9 && defaultButtons.length === 9, `${copyButtons.length}/${defaultButtons.length}`)
|
|
339
341
|
check('preset cards no longer offer save-as', !panelText.includes('另存为我的套装'), panelText.slice(0, 400))
|
|
340
342
|
const editButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '编辑')
|
|
341
343
|
check('settings offers an edit action for saved kits', editButtons.length === 2, `${editButtons.length} · 编辑`)
|
package/scripts/selftest.mjs
CHANGED
|
@@ -151,13 +151,50 @@ function childFor(dependencies, built, commands, routes) {
|
|
|
151
151
|
return child
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
const COLLAB = ['send_message', 'list_agents', 'interrupt_agent', 'subagent', 'subagent_fork']
|
|
155
|
+
|
|
156
|
+
// The coding preset restricted to what the fake runtime actually exposes: the
|
|
157
|
+
// collaboration five are listed by the real preset, but this fake agent only owns
|
|
158
|
+
// subagent from that group, and a tool it cannot see can never survive a mask.
|
|
159
|
+
const CODING_KEPT = ['pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'subagent', ...OWN_TOOLS]
|
|
160
|
+
|
|
161
|
+
// Mirrors the presets in `cordis.patch.yml` (the host hands this same shape to
|
|
162
|
+
// `apply`). The last check in this block keeps the two lists from drifting.
|
|
154
163
|
const config = {
|
|
155
164
|
defaultKit: 'writing',
|
|
156
165
|
kits: [
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
{
|
|
167
|
+
name: 'chat',
|
|
168
|
+
title: '仅聊天',
|
|
169
|
+
builtins: ['ask_user_question', 'weather', 'web_search', 'web_fetch', 'read'],
|
|
170
|
+
},
|
|
171
|
+
{ name: 'coding', title: '编程用', builtins: ['pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write'] },
|
|
172
|
+
{
|
|
173
|
+
name: 'research',
|
|
174
|
+
title: '查找资料',
|
|
175
|
+
builtins: ['web_search', 'web_fetch', 'advanced_search', 'research', 'platform_search', 'read', 'write', 'glob', 'grep'],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'plan',
|
|
179
|
+
title: '项目梳理',
|
|
180
|
+
builtins: ['ask_user_question', 'web_search', 'web_fetch', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write'],
|
|
181
|
+
},
|
|
182
|
+
{ name: 'writing', title: '写作', builtins: ['read', 'write', 'edit', 'glob', 'grep'] },
|
|
183
|
+
].map((preset) => ({
|
|
184
|
+
...preset,
|
|
185
|
+
// Every preset carries the collaboration five, or a subagent running it
|
|
186
|
+
// cannot answer its parent at all.
|
|
187
|
+
builtins: [...preset.builtins, 'send_message', 'list_agents', 'interrupt_agent', 'subagent', 'subagent_fork'],
|
|
188
|
+
})),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The five preset ids must exist in the shipped patch too — the test's copy above
|
|
192
|
+
// is hand-written, and a rename in one place without the other is silent drift.
|
|
193
|
+
{
|
|
194
|
+
const patch = readFileSync(new URL('../cordis.patch.yml', import.meta.url), 'utf8')
|
|
195
|
+
const missing = config.kits.filter((preset) => !new RegExp(`name:\\s*${preset.name}\\b`).test(patch))
|
|
196
|
+
check('the shipped patch carries every preset the tests use',
|
|
197
|
+
missing.length === 0, missing.map((preset) => preset.name).join(','))
|
|
161
198
|
}
|
|
162
199
|
|
|
163
200
|
const ctx = makeCtx()
|
|
@@ -345,7 +382,7 @@ let listed = await run('kit_list', {}, agent)
|
|
|
345
382
|
check('kit_list active title', listed.active === '写作', listed.active)
|
|
346
383
|
check('kit_list base is kit', listed.base === 'kit', listed.base)
|
|
347
384
|
check('kit_list prefers the shipped config', listed.source === 'bundle 配置', listed.source)
|
|
348
|
-
check('kit_list
|
|
385
|
+
check('kit_list lists the five shipped presets', listed.presets.length === 5, String(listed.presets.length))
|
|
349
386
|
check('kit_list has no user kit yet', listed.kits.length === 0, String(listed.kits.length))
|
|
350
387
|
// The host validates execute() output against the declared schema, so a field
|
|
351
388
|
// that is returned but not declared fails the whole call at runtime
|
|
@@ -360,12 +397,12 @@ check('kit_list has no user kit yet', listed.kits.length === 0, String(listed.ki
|
|
|
360
397
|
}
|
|
361
398
|
|
|
362
399
|
// ---- kit_apply by title ------------------------------------------------------
|
|
363
|
-
const appliedKit = await run('kit_apply', { kit: '
|
|
400
|
+
const appliedKit = await run('kit_apply', { kit: '编程用' }, agent)
|
|
364
401
|
check('kit_apply by title', appliedKit.kit === 'coding', appliedKit.kit)
|
|
365
402
|
check('previous restriction released', agent.filters[0].active === false)
|
|
366
403
|
check(
|
|
367
404
|
'preset keeps exactly its built-ins plus own tools',
|
|
368
|
-
sameSet(keptOf(agent),
|
|
405
|
+
sameSet(keptOf(agent), CODING_KEPT),
|
|
369
406
|
keptOf(agent).join(','),
|
|
370
407
|
)
|
|
371
408
|
|
|
@@ -380,7 +417,7 @@ check('unknown kit throws', unknownError.includes('未知套装'), unknownError)
|
|
|
380
417
|
// ---- GET /state exposes the whole model -------------------------------------
|
|
381
418
|
let state = await getState()
|
|
382
419
|
check('state route 200', state.status === 200, String(state.status))
|
|
383
|
-
check('state lists presets with names', JSON.stringify(state.body.presetNames) === JSON.stringify(['
|
|
420
|
+
check('state lists presets with names', JSON.stringify(state.body.presetNames) === JSON.stringify(['chat', 'coding', 'research', 'plan', 'writing']), JSON.stringify(state.body.presetNames))
|
|
384
421
|
check('state lists preset allow', Array.isArray(state.body.presets) && state.body.presets[0].allow.includes('read'), JSON.stringify(state.body.presets?.[0]))
|
|
385
422
|
check('state splits the catalog', Array.isArray(state.body.catalog.builtins) && Array.isArray(state.body.catalog.plugins), JSON.stringify(state.body.catalog))
|
|
386
423
|
check(
|
|
@@ -433,7 +470,7 @@ check('presets carry a token estimate', Number.isFinite(answer.body.presets[0].t
|
|
|
433
470
|
answer = await post({ action: 'applyBase', sessionId: SESSION_ID, base: 'kit', kit: 'coding' })
|
|
434
471
|
const kitKept = keptOf(agent)
|
|
435
472
|
check('applyBase kit 200', answer.status === 200, JSON.stringify(answer.body))
|
|
436
|
-
check('kit keeps only its built-ins', sameSet(kitKept,
|
|
473
|
+
check('kit keeps only its built-ins', sameSet(kitKept, CODING_KEPT), kitKept.join(','))
|
|
437
474
|
check('kit masks plugin tools', !kitKept.includes('demo_search'), kitKept.join(','))
|
|
438
475
|
|
|
439
476
|
let badBase = await post({ action: 'applyBase', sessionId: SESSION_ID, base: 'kit', kit: 'ghost' })
|
|
@@ -678,7 +715,7 @@ check('GET answers for the Session it names', firstView.body.selection.kit === '
|
|
|
678
715
|
check('GET answers for a different Session', secondView.body.selection.kit === 'research', JSON.stringify(secondView.body.selection))
|
|
679
716
|
check(
|
|
680
717
|
'two Sessions do not leak into each other',
|
|
681
|
-
firstView.body.label === '
|
|
718
|
+
firstView.body.label === '编程用' && secondView.body.label === '查找资料',
|
|
682
719
|
JSON.stringify([firstView.body.label, secondView.body.label]),
|
|
683
720
|
)
|
|
684
721
|
|
|
@@ -785,6 +822,102 @@ try {
|
|
|
785
822
|
}
|
|
786
823
|
check('a reserved name is refused', /全部/u.test(reservedError ?? ''), reservedError)
|
|
787
824
|
|
|
825
|
+
// T18: the same name with different case or surrounding space is the same kit.
|
|
826
|
+
let caseDupError
|
|
827
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
828
|
+
{ name: 'qa', title: '问答', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
829
|
+
try {
|
|
830
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
831
|
+
{ name: ' QA ', title: '重复', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
832
|
+
} catch (error) {
|
|
833
|
+
caseDupError = String(error?.message ?? error)
|
|
834
|
+
}
|
|
835
|
+
check('a name that differs only by case or space is refused',
|
|
836
|
+
/已存在/u.test(caseDupError ?? ''), caseDupError)
|
|
837
|
+
|
|
838
|
+
// T24: a user kit may not shadow a shipped preset.
|
|
839
|
+
let presetClashError
|
|
840
|
+
try {
|
|
841
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
842
|
+
{ name: 'chat', title: '冒充预置', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
843
|
+
} catch (error) {
|
|
844
|
+
presetClashError = String(error?.message ?? error)
|
|
845
|
+
}
|
|
846
|
+
check('a preset name cannot be taken by a user kit',
|
|
847
|
+
/预置/u.test(presetClashError ?? ''), presetClashError)
|
|
848
|
+
|
|
849
|
+
// T22: a binding that names a kit removed outside the app falls back to 全部.
|
|
850
|
+
// Simulated the way it really happens: the kit is gone from the store file when
|
|
851
|
+
// the app starts again, so a fresh module instance has to cope with it.
|
|
852
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
853
|
+
{ name: 'ghost-kit', title: '已删除的', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
854
|
+
await ctxV2.registered.get('kit_assign').execute({ sessionId: 'session-ghost', kit: 'ghost-kit' }, exec(agentB))
|
|
855
|
+
const storeWithGhost = storeOf()
|
|
856
|
+
storeWithGhost.kits = storeWithGhost.kits.filter((kit) => kit.name !== 'ghost-kit')
|
|
857
|
+
writeFileSync(join(v2Home, '.dsh', 'loadout.json'), JSON.stringify(storeWithGhost), 'utf8')
|
|
858
|
+
const v2b = await import('../index.js?v2=2')
|
|
859
|
+
const ctxV2b = makeCtx()
|
|
860
|
+
v2b.apply(ctxV2b, { defaultKit: '', kits: config.kits })
|
|
861
|
+
const agentGhost = makeAgent(undefined, { id: 'session-ghost' })
|
|
862
|
+
for (const handler of ctxV2b.handlers.get('agent/created') ?? []) handler(agentGhost)
|
|
863
|
+
check('a binding to a deleted kit falls back to 全部',
|
|
864
|
+
storeOf().assignments['session-ghost'] === null && keptOf(agentGhost).includes('web_search'),
|
|
865
|
+
JSON.stringify({ assignments: storeOf().assignments, kept: keptOf(agentGhost) }))
|
|
866
|
+
|
|
867
|
+
// T03/T05: the default changes again; conversations that already answered keep theirs.
|
|
868
|
+
const agentC = makeAgent(undefined, { id: 'session-c' })
|
|
869
|
+
await ctxV2.registered.get('kit_set_default').execute({ name: 'chat' }, exec(agentB))
|
|
870
|
+
fireCreated(agentC)
|
|
871
|
+
check('changing the default twice keeps earlier conversations',
|
|
872
|
+
storeOf().assignments['session-a'] === null && storeOf().assignments['session-c'] === 'chat',
|
|
873
|
+
JSON.stringify(storeOf().assignments))
|
|
874
|
+
|
|
875
|
+
// T08: deleting a kit reports and releases every conversation that held it.
|
|
876
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
877
|
+
{ name: 'shared', title: '共享', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
878
|
+
for (const id of ['session-a', 'session-b', 'session-c']) {
|
|
879
|
+
await ctxV2.registered.get('kit_assign').execute({ sessionId: id, kit: 'shared' }, exec(agentB))
|
|
880
|
+
}
|
|
881
|
+
const sharedDelete = await ctxV2.registered.get('kit_delete').execute({ name: 'shared' }, exec(agentB))
|
|
882
|
+
check('deleting a kit reports every conversation it held',
|
|
883
|
+
sharedDelete.affectedSessions === 3
|
|
884
|
+
&& ['session-a', 'session-b', 'session-c'].every((id) => storeOf().assignments[id] === null),
|
|
885
|
+
JSON.stringify({ sharedDelete, assignments: storeOf().assignments }))
|
|
886
|
+
|
|
887
|
+
// T09: deleting the default kit also clears the default.
|
|
888
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
889
|
+
{ name: 'shared-two', title: '共享二', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
890
|
+
await ctxV2.registered.get('kit_set_default').execute({ name: 'shared-two' }, exec(agentB))
|
|
891
|
+
await ctxV2.registered.get('kit_assign').execute({ sessionId: 'session-a', kit: 'shared-two' }, exec(agentB))
|
|
892
|
+
const defaultDelete = await ctxV2.registered.get('kit_delete').execute({ name: 'shared-two' }, exec(agentB))
|
|
893
|
+
check('deleting the default kit clears the default',
|
|
894
|
+
defaultDelete.affectedSessions === 1
|
|
895
|
+
&& (storeOf().defaultKit === null || storeOf().defaultKit === ''),
|
|
896
|
+
JSON.stringify({ defaultDelete, defaultKit: storeOf().defaultKit }))
|
|
897
|
+
|
|
898
|
+
// T10: renaming changes the display name only; bindings point at the stable id.
|
|
899
|
+
await ctxV2.registered.get('kit_create').execute(
|
|
900
|
+
{ name: 'stable', title: '稳定', builtins: ['read'], plugins: [] }, exec(agentB))
|
|
901
|
+
await ctxV2.registered.get('kit_assign').execute({ sessionId: 'session-b', kit: 'stable' }, exec(agentB))
|
|
902
|
+
await ctxV2.registered.get('kit_rename').execute({ name: 'stable', title: '稳定改名' }, exec(agentB))
|
|
903
|
+
check('renaming a bound kit keeps the binding',
|
|
904
|
+
storeOf().assignments['session-b'] === 'stable'
|
|
905
|
+
&& storeOf().kits.some((kit) => kit.name === 'stable' && kit.title === '稳定改名'),
|
|
906
|
+
JSON.stringify(storeOf().kits.map((kit) => [kit.name, kit.title])))
|
|
907
|
+
|
|
908
|
+
// T15: every write is a complete file — the temporary one never survives.
|
|
909
|
+
check('the store write leaves no temporary file',
|
|
910
|
+
!existsSync(join(v2Home, '.dsh', 'loadout.json.tmp')), 'loadout.json.tmp is still there')
|
|
911
|
+
|
|
912
|
+
// T25: a shipped preset can be the new-conversation default too.
|
|
913
|
+
await ctxV2.registered.get('kit_set_default').execute({ name: 'plan' }, exec(agentB))
|
|
914
|
+
const agentD = makeAgent(undefined, { id: 'session-d' })
|
|
915
|
+
fireCreated(agentD)
|
|
916
|
+
check('a preset can be the default',
|
|
917
|
+
storeOf().assignments['session-d'] === 'plan'
|
|
918
|
+
&& keptOf(agentD).includes('read') && !keptOf(agentD).includes('pwsh'),
|
|
919
|
+
JSON.stringify({ binding: storeOf().assignments['session-d'], kept: keptOf(agentD) }))
|
|
920
|
+
|
|
788
921
|
process.env.USERPROFILE = savedHomeV2
|
|
789
922
|
process.env.HOME = savedHomeV2b
|
|
790
923
|
rmSync(v2Home, { recursive: true, force: true })
|