dsh-agent-board 1.7.4 → 1.7.5
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.md +33 -13
- package/index.mjs +9 -1
- package/lib/client/dashboard.js +339 -52
- package/lib/client/kernel.js +9 -6
- package/lib/client/task-detail.js +14 -1
- package/lib/client.js +362 -59
- package/lib/core.mjs +102 -8
- package/lib/dispatch.mjs +38 -3
- package/lib/health.mjs +68 -1
- package/lib/rpc.mjs +54 -10
- package/lib/usage.mjs +532 -0
- package/package.json +2 -2
- package/test/core.test.mjs +352 -65
- package/test/headless-smoke.test.mjs +209 -0
- package/test/helpers/mock-ctx.mjs +327 -0
- package/test/integration.test.mjs +506 -0
- package/test/runtime-health.test.mjs +158 -0
- package/test/scoreboard-client.test.mjs +106 -0
- package/test/scoreboard-trends-client.test.mjs +104 -0
- package/test/scoreboard.test.mjs +271 -0
- package/test/scorecard-clusters.test.mjs +225 -0
- package/test/scorecard-data.test.mjs +214 -0
- package/test/usage-mainwindow.test.mjs +7 -5
- package/test/user-guide-client.test.mjs +79 -0
package/test/core.test.mjs
CHANGED
|
@@ -115,7 +115,7 @@ test('史诗收口③:cancelled 子任务计入终态完成集 → 父卡也
|
|
|
115
115
|
assert.equal(core.isChildSettled(c1), true)
|
|
116
116
|
assert.equal(core.isChildSettled(c2), true) // 人主动取消 = 该子任务范围已了结
|
|
117
117
|
assert.equal(core.isChildSettled(c3), false)
|
|
118
|
-
assert.equal(core.isChildSettled(mkTask({ status: 'archived' })),
|
|
118
|
+
assert.equal(core.isChildSettled(mkTask({ status: 'archived' })), true) // 归档=人的显式了结,计入终态集(反馈 n-muw706h1uymy 根修:交错归档不再卡死父卡)
|
|
119
119
|
const r = core.resolveApply(d, c3, 'w', 'verifying', 'done')
|
|
120
120
|
assert.equal(r.parentUpdated, true)
|
|
121
121
|
assert.equal(p.status, 'verifying')
|
|
@@ -157,6 +157,86 @@ test('史诗收口⑥:checkParentAuto 兼容名委托共享 helper(单一判
|
|
|
157
157
|
assert.equal(p.resolution, 'all subtasks resolved') // 口径文案统一(旧 resolveApply 分支的 'all subtasks done' 已退役)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
+
// ===== 史诗自动收口漏触发根修(task-mux3uvx3,反馈 n-muw706h1uymy,2026-10-06 实证)=====
|
|
161
|
+
// 实证根因(repro 脚本逐条复现,嫌疑点逐个排除):
|
|
162
|
+
// ① isChildSettled 旧口径不含 archived——「resolve 一张归档一张」的交错序列下,末子卡 resolve
|
|
163
|
+
// 时兄卡已归档,`s.every(isChildSettled)` 永远凑不齐(resolveApply/verifyApply 的父卡钩子
|
|
164
|
+
// 其实都跑了,是口径让它哑火);
|
|
165
|
+
// ② archive 路径(task_archive 工具 / archive-task RPC)旧实现为内联代码,从不调用父卡检查——
|
|
166
|
+
// 末子卡归档这一下补不上收口(僵尸子卡直接出清链路更是一直缺这钩子)。
|
|
167
|
+
// 嫌疑排除:settleVerifier→verifyApply 路径的父卡钩子(checkParentAuto)一直在,非挂点。
|
|
168
|
+
// 修法:isChildSettled 计入 archived(与 childStats 进度口径同源)+ archiveApply 共享 helper
|
|
169
|
+
// (归档落定唯一入口)尾部挂 maybeAutoCloseParent。
|
|
170
|
+
test('史诗收口⑦:交错 resolve+archive 序列——末子卡 resolve 时兄卡已归档也能收口(根因复现①)', () => {
|
|
171
|
+
const p = mkTask({ id: 'epic', status: 'in-progress' })
|
|
172
|
+
const c1 = mkTask({ id: 'c1', parentId: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
173
|
+
const c2 = mkTask({ id: 'c2', parentId: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
174
|
+
const c3 = mkTask({ id: 'c3', parentId: 'epic', status: 'in-progress', pipeline: 'work' })
|
|
175
|
+
const d = mkBoard([p, c1, c2, c3])
|
|
176
|
+
core.resolveApply(d, c1, 'w', 'verifying', 'done'); core.archiveApply(d, c1, 'tester')
|
|
177
|
+
assert.equal(c1.status, 'archived')
|
|
178
|
+
assert.equal(p.status, 'in-progress') // 未凑齐不收口(c2/c3 仍在跑)
|
|
179
|
+
core.resolveApply(d, c2, 'w', 'verifying', 'done'); core.archiveApply(d, c2, 'tester')
|
|
180
|
+
assert.equal(p.status, 'in-progress')
|
|
181
|
+
const r = core.resolveApply(d, c3, 'w', 'verifying', 'done') // 此刻 c1/c2 已 archived
|
|
182
|
+
assert.equal(r.parentUpdated, true) // 旧口径此处为 undefined → epic 永久卡死
|
|
183
|
+
assert.equal(p.status, 'verifying')
|
|
184
|
+
assert.equal(p.resolution, 'all subtasks resolved')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('史诗收口⑧:archiveApply 归档路径本身触发父卡检查——末子卡归档即收口(根因复现②,僵尸出清链路)', () => {
|
|
188
|
+
const p = mkTask({ id: 'epic', status: 'in-progress' })
|
|
189
|
+
const c1 = mkTask({ id: 'c1', parentId: 'epic', status: 'resolved' })
|
|
190
|
+
const c2 = mkTask({ id: 'c2', parentId: 'epic', status: 'in-progress', claimedBy: null })
|
|
191
|
+
const d = mkBoard([p, c1, c2])
|
|
192
|
+
const r = core.archiveApply(d, c2, 'tester') // c2 从僵尸 in-progress 直接归档(v1.7.1 archiveErr 放行面)
|
|
193
|
+
assert.equal(c2.status, 'archived'); assert.ok(c2.archivedAt)
|
|
194
|
+
assert.equal(r.parentUpdated, true) // 归档这一下补齐收口(旧实现无父卡钩子)
|
|
195
|
+
assert.equal(p.status, 'verifying')
|
|
196
|
+
assert.equal(p.resolution, 'all subtasks resolved')
|
|
197
|
+
// 顶层卡归档(无 parentId)不触父卡检查、返回体无 parentUpdated
|
|
198
|
+
const solo = mkTask({ id: 'solo', status: 'resolved' })
|
|
199
|
+
const r2 = core.archiveApply(mkBoard([solo]), solo, 'tester')
|
|
200
|
+
assert.equal(solo.status, 'archived'); assert.equal('parentUpdated' in r2, false)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('史诗收口⑨:archiveApply 级联归档父卡不误触收口 + history/note 口径保持', () => {
|
|
204
|
+
// 父卡归档 → 级联子卡;父卡自身无 parentId(maybeAutoCloseParent 第一闸门拦),
|
|
205
|
+
// 级联子卡也不做父卡检查(父卡已 archived 非 in-progress,检查也必返回 null)——零意外流转
|
|
206
|
+
const p = mkTask({ id: 'epic', status: 'in-progress' })
|
|
207
|
+
const c1 = mkTask({ id: 'c1', parentId: 'epic', status: 'resolved' })
|
|
208
|
+
const c2 = mkTask({ id: 'c2', parentId: 'epic', status: 'in-progress', claimedBy: null })
|
|
209
|
+
const d = mkBoard([p, c1, c2])
|
|
210
|
+
const r = core.archiveApply(d, p, 'tester', 'manual archive')
|
|
211
|
+
assert.equal(p.status, 'archived')
|
|
212
|
+
assert.equal(r.childrenArchived, 2)
|
|
213
|
+
assert.equal(c1.status, 'archived'); assert.equal(c2.status, 'archived')
|
|
214
|
+
assert.equal('parentUpdated' in r, false)
|
|
215
|
+
assert.equal(p.history[0].note, 'manual archive') // note 参数透传(RPC 'manual archive' / 工具 'archived')
|
|
216
|
+
assert.equal(c2.history[c2.history.length - 1].note, 'cascade')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('史诗收口⑩:post hook 未收口的 epic 走归档路径同样只挂闸门不转 verifying(hooks 语义不变)', () => {
|
|
220
|
+
const epi = mkTask({ id: 'epic', status: 'in-progress', hooks: { post: { enabled: true, prompt: '收口', state: 'idle' } } })
|
|
221
|
+
const c1 = mkTask({ id: 'c1', parentId: 'epic', status: 'resolved' })
|
|
222
|
+
const c2 = mkTask({ id: 'c2', parentId: 'epic', status: 'resolved' })
|
|
223
|
+
const d = mkBoard([epi, c1, c2])
|
|
224
|
+
const r = core.archiveApply(d, c2, 'tester') // 末子卡归档触发父卡检查
|
|
225
|
+
assert.equal(r.parentUpdated, true) // 检查跑了(返回父卡)
|
|
226
|
+
assert.equal(epi.status, 'in-progress') // 但不直接转 verifying——post 闸门照旧
|
|
227
|
+
assert.equal(epi.hooks.post.pending, true)
|
|
228
|
+
assert.equal(epi.hooks.post.state, 'running')
|
|
229
|
+
assert.equal(epi.resolution, undefined)
|
|
230
|
+
// post 已 done 的 epic:归档路径正常收口(与 resolve/verify 路径同口径)
|
|
231
|
+
const e2 = mkTask({ id: 'e2', status: 'in-progress', hooks: { post: { enabled: true, prompt: '收口', state: 'done' } } })
|
|
232
|
+
const k1 = mkTask({ id: 'k1', parentId: 'e2', status: 'archived' })
|
|
233
|
+
const k2 = mkTask({ id: 'k2', parentId: 'e2', status: 'resolved' })
|
|
234
|
+
const d2 = mkBoard([e2, k1, k2])
|
|
235
|
+
const r2 = core.archiveApply(d2, k2, 'tester')
|
|
236
|
+
assert.equal(r2.parentUpdated, true)
|
|
237
|
+
assert.equal(e2.status, 'verifying')
|
|
238
|
+
})
|
|
239
|
+
|
|
160
240
|
// ===== 依赖 =====
|
|
161
241
|
test('validateDeps: 自引用/不存在/成环 全拒绝', () => {
|
|
162
242
|
const a = mkTask({ id: 'a' }); const b = mkTask({ id: 'b', dependsOn: ['a'] })
|
|
@@ -223,6 +303,135 @@ test('parseSections: diff 概要分段解析(Worker 变更概要 → deliverab
|
|
|
223
303
|
assert.equal(s.summary, '做了')
|
|
224
304
|
})
|
|
225
305
|
|
|
306
|
+
// ===== Verifier 自测指南(verifyUserGuide,task-muxyyvg0):契约解析 / 开关门禁 / 落账 =====
|
|
307
|
+
test('自测指南①:parseSections 收「## 自测指南」段 → userTest 四字段 + tier 三档', () => {
|
|
308
|
+
// ui 档:四字段齐全(编号列表 steps)
|
|
309
|
+
const s1 = core.parseSections('## 验证概要\n通过\n\n## 自测指南\ngist: 看板卡片加了徽章\ntier: ui\nsteps:\n1. 打开看板面板\n2. 看卡片标题右侧\nexpect: 应看到绿色徽章')
|
|
310
|
+
assert.ok(s1.userTest, '自测指南段必须解析出 userTest')
|
|
311
|
+
assert.equal(s1.userTest.gist, '看板卡片加了徽章')
|
|
312
|
+
assert.equal(s1.userTest.tier, 'ui')
|
|
313
|
+
assert.deepEqual(s1.userTest.steps, ['打开看板面板', '看卡片标题右侧'])
|
|
314
|
+
assert.equal(s1.userTest.expect, '应看到绿色徽章')
|
|
315
|
+
// metric 档 + 横杠列表 + 全角冒号
|
|
316
|
+
const s2 = core.parseSections('## 自测指南\ngist: Token 区多了主窗口行\ntier:metric\nsteps:\n- 打开仪表盘\n- 看 Token 区\nexpect: 出现「主窗口(本会话)」行')
|
|
317
|
+
assert.equal(s2.userTest.tier, 'metric')
|
|
318
|
+
assert.deepEqual(s2.userTest.steps, ['打开仪表盘', '看 Token 区'])
|
|
319
|
+
// internal 档:steps 可空,expect 写「验证靠测试套件」
|
|
320
|
+
const s3 = core.parseSections('## 自测指南\ngist: host 聚合口径修正\ntier: internal\nexpect: 验证靠测试套件')
|
|
321
|
+
assert.equal(s3.userTest.tier, 'internal')
|
|
322
|
+
assert.deepEqual(s3.userTest.steps, [])
|
|
323
|
+
// tier 非法/脏值 → 归 internal(最保守档,不伪造用户可感知面)
|
|
324
|
+
const s4 = core.parseSections('## 自测指南\ngist: x\ntier: 界面级\nexpect: y')
|
|
325
|
+
assert.equal(s4.userTest.tier, 'internal')
|
|
326
|
+
// 「## 自测情况」(Worker 段)不串进 userTest;「## 自测指南」不串进 selfTest(标题口径隔离)
|
|
327
|
+
const s5 = core.parseSections('## 自测情况\nnpm test pass')
|
|
328
|
+
assert.equal(s5.selfTest, 'npm test pass'); assert.equal(s5.userTest, undefined)
|
|
329
|
+
const s6 = core.parseSections('## 自测指南\ngist: g\ntier: ui\nexpect: e')
|
|
330
|
+
assert.equal(s6.selfTest, undefined); assert.ok(s6.userTest)
|
|
331
|
+
// 缺段 → 字段不挂
|
|
332
|
+
assert.equal(core.parseSections('## 验证概要\n通过').userTest, undefined)
|
|
333
|
+
// 段体四字段全空 → 不挂字段(视为没交指南)
|
|
334
|
+
assert.equal(core.parseSections('## 自测指南\n(无)').userTest, undefined)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('自测指南②:normalizeUserTest 归一(非对象/空指南→null;tier 归档;长度封顶)', () => {
|
|
338
|
+
assert.equal(core.normalizeUserTest(null), null)
|
|
339
|
+
assert.equal(core.normalizeUserTest('ui'), null)
|
|
340
|
+
assert.equal(core.normalizeUserTest({}), null) // 空指南不落
|
|
341
|
+
assert.equal(core.normalizeUserTest({ gist: ' ', steps: [], expect: '' }), null)
|
|
342
|
+
const ut = core.normalizeUserTest({ gist: ' 改了 x ', steps: [' 第一步 ', '', null, '第二步'], expect: ' 看到 y ', tier: 'UI' })
|
|
343
|
+
assert.deepEqual(ut, { gist: '改了 x', steps: ['第一步', '第二步'], expect: '看到 y', tier: 'ui' })
|
|
344
|
+
assert.equal(core.normalizeUserTest({ gist: 'g', tier: 'ui(界面可操作)' }).tier, 'ui') // tier 取首词
|
|
345
|
+
assert.equal(core.normalizeUserTest({ gist: 'g', tier: 'bogus' }).tier, 'internal') // 非法归 internal
|
|
346
|
+
assert.equal(core.normalizeUserTest({ gist: 'g' }).tier, 'internal') // 缺省归 internal
|
|
347
|
+
// 封顶:gist ≤300 / expect ≤600 / steps ≤12 条每条 ≤300
|
|
348
|
+
const big = core.normalizeUserTest({ gist: 'g'.repeat(400), expect: 'e'.repeat(700), steps: Array(20).fill('s'.repeat(400)) })
|
|
349
|
+
assert.equal(big.gist.length, 300); assert.equal(big.expect.length, 600)
|
|
350
|
+
assert.equal(big.steps.length, 12); assert.equal(big.steps[0].length, 300)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('自测指南③:buildVerifierPrompt 开关门禁——默认含指南段+诚实护栏;显式 false 整段消失', () => {
|
|
354
|
+
const t = mkTask({ id: 'tx', status: 'verifying' })
|
|
355
|
+
const on = core.buildVerifierPrompt(t, '')
|
|
356
|
+
assert.match(on, /## 自测指南/) // 指南段在 prompt 里
|
|
357
|
+
assert.match(on, /gist: <一句人话/) // 四字段格式
|
|
358
|
+
assert.match(on, /tier: ui \| metric \| internal/) // 三档分级
|
|
359
|
+
assert.match(on, /诚实护栏/) // 诚实护栏写进 prompt
|
|
360
|
+
assert.match(on, /不许编造没验过的操作/)
|
|
361
|
+
assert.match(on, /验证靠测试套件/) // internal 档口径
|
|
362
|
+
assert.match(on, /userTest 参数/) // 工具通道说明
|
|
363
|
+
assert.ok(on.indexOf('结论契约') < on.indexOf('自测指南(给用户看的验收指引'), '指南段追加在 prompt 末尾(结论契约之后)')
|
|
364
|
+
const off = core.buildVerifierPrompt(t, '', false) // 开关关 → 整段不进 prompt(省 token)
|
|
365
|
+
assert.doesNotMatch(off, /自测指南/)
|
|
366
|
+
assert.doesNotMatch(off, /userTest/)
|
|
367
|
+
assert.match(off, /结论契约/) // 其余契约逐字不变
|
|
368
|
+
assert.equal(core.buildVerifierPrompt(t, ''), on) // 缺省第三参 = 开
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('自测指南④:文本结算落账——开关开 userTest 落 t.verification;开关关不挂字段(端到端真 settle)', async () => {
|
|
372
|
+
const GUIDE = '\n\n## 自测指南\ngist: 卡片加了徽章\ntier: ui\nsteps:\n1. 打开看板\n2. 看徽章\nexpect: 看到绿徽章'
|
|
373
|
+
async function waitVerification(t) { // 与 waitRej 同口径:轮询等 settle 链尾,防固定 sleep 抖动
|
|
374
|
+
var t0 = Date.now()
|
|
375
|
+
while (Date.now() - t0 < 2000) { if (t.verification && t.status === 'resolved') return true; await new Promise(function (r) { setTimeout(r, 5) }) }
|
|
376
|
+
return !!(t.verification && t.status === 'resolved')
|
|
377
|
+
}
|
|
378
|
+
// 开关开(缺省):APPROVED + 指南段 → userTest 落 t.verification
|
|
379
|
+
const t1 = mkTask({ id: 'ug1', status: 'verifying' })
|
|
380
|
+
const h1 = mkVerifierSettleDispatch(mkBoard([t1]))
|
|
381
|
+
await h1.dispatch.poolCycle(FULL_SID)
|
|
382
|
+
h1.settle('APPROVED\n\n## 验证概要\n复跑通过' + GUIDE)
|
|
383
|
+
assert.equal(await waitVerification(t1), true)
|
|
384
|
+
assert.deepEqual(t1.verification.userTest, { gist: '卡片加了徽章', steps: ['打开看板', '看徽章'], expect: '看到绿徽章', tier: 'ui' })
|
|
385
|
+
// 开关关:输出即使带指南段也不挂字段(省 token 口径的落账侧门禁)
|
|
386
|
+
const t2 = mkTask({ id: 'ug2', status: 'verifying' })
|
|
387
|
+
const board2 = mkBoard([t2]); board2.verifyUserGuide = false
|
|
388
|
+
const h2 = mkVerifierSettleDispatch(board2)
|
|
389
|
+
await h2.dispatch.poolCycle(FULL_SID)
|
|
390
|
+
h2.settle('APPROVED\n\n## 验证概要\n复跑通过' + GUIDE)
|
|
391
|
+
assert.equal(await waitVerification(t2), true)
|
|
392
|
+
assert.equal(t2.verification.userTest, undefined, '开关关 → 落账不挂 userTest')
|
|
393
|
+
// 开关开但缺段 → 不挂字段
|
|
394
|
+
const t3 = mkTask({ id: 'ug3', status: 'verifying' })
|
|
395
|
+
const h3 = mkVerifierSettleDispatch(mkBoard([t3]))
|
|
396
|
+
await h3.dispatch.poolCycle(FULL_SID)
|
|
397
|
+
h3.settle('APPROVED\n\n## 验证概要\n复跑通过')
|
|
398
|
+
assert.equal(await waitVerification(t3), true)
|
|
399
|
+
assert.equal(t3.verification.userTest, undefined, '缺段 → 不挂 userTest')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('自测指南⑤:board_verdict 工具通道——userTest 参数落账 + 开关门禁 + 非法 tier 归 internal', async () => {
|
|
403
|
+
const UT = { gist: '报告多了自测清单', steps: ['打开仪表盘', '点生成报告'], expect: '报告含「本版自测清单」段', tier: 'ui' }
|
|
404
|
+
const b1 = mkBoard([mkTask({ id: 'g1', status: 'verifying' })])
|
|
405
|
+
const r1 = await mkRpcHandlers(b1).__tools['board_verdict'].execute({ taskId: 'g1', verdict: 'approved', summary: '通过', userTest: UT }, {})
|
|
406
|
+
assert.equal(r1.ok, true)
|
|
407
|
+
assert.deepEqual(b1.tasks[0].verification.userTest, UT, '工具通道 userTest 原样落账(归一后)')
|
|
408
|
+
// 开关关:参数带了也不挂字段
|
|
409
|
+
const b2 = mkBoard([mkTask({ id: 'g2', status: 'verifying' })]); b2.verifyUserGuide = false
|
|
410
|
+
await mkRpcHandlers(b2).__tools['board_verdict'].execute({ taskId: 'g2', verdict: 'approved', summary: '通过', userTest: UT }, {})
|
|
411
|
+
assert.equal(b2.tasks[0].verification.userTest, undefined, '开关关 → 工具通道也不挂 userTest')
|
|
412
|
+
// 非法 tier 归 internal;空指南(全空字段)不落
|
|
413
|
+
const b3 = mkBoard([mkTask({ id: 'g3', status: 'verifying' })])
|
|
414
|
+
await mkRpcHandlers(b3).__tools['board_verdict'].execute({ taskId: 'g3', verdict: 'approved', summary: '通过', userTest: { gist: '内部重构', tier: ' bogus ', expect: '验证靠测试套件' } }, {})
|
|
415
|
+
assert.equal(b3.tasks[0].verification.userTest.tier, 'internal')
|
|
416
|
+
const b4 = mkBoard([mkTask({ id: 'g4', status: 'verifying' })])
|
|
417
|
+
await mkRpcHandlers(b4).__tools['board_verdict'].execute({ taskId: 'g4', verdict: 'approved', summary: '通过', userTest: { gist: ' ', steps: [] } }, {})
|
|
418
|
+
assert.equal(b4.tasks[0].verification.userTest, undefined, '空指南不落字段')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('自测指南⑥:cfg/seed/normalizeBoard 三处默认开——老看板缺字段补 true,显式 false 才关', () => {
|
|
422
|
+
assert.equal(core.cfg({}).verifyUserGuide, true) // 缺省开
|
|
423
|
+
assert.equal(core.cfg({ verifyUserGuide: false }).verifyUserGuide, false)
|
|
424
|
+
assert.equal(core.cfg({ verifyUserGuide: 0 }).verifyUserGuide, true) // 脏值非 false → 开(!== false 口径)
|
|
425
|
+
assert.equal(core.seed('s-x').verifyUserGuide, true) // 新板种子自带
|
|
426
|
+
const old = { poolStatus: { workers: [], verifiers: [] }, tasks: [] } // 老看板无字段
|
|
427
|
+
core.normalizeBoard(old)
|
|
428
|
+
assert.equal(old.verifyUserGuide, true) // 读路径补 true
|
|
429
|
+
const off = { verifyUserGuide: false, tasks: [] }
|
|
430
|
+
core.normalizeBoard(off)
|
|
431
|
+
assert.equal(off.verifyUserGuide, false) // 显式 false 不被覆盖
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
|
|
226
435
|
test('parseVerdict: 行首锚定,历史提及不误判', () => {
|
|
227
436
|
assert.equal(core.parseVerdict('APPROVED: 通过'), 'APPROVED')
|
|
228
437
|
assert.equal(core.parseVerdict('REJECTED: 不达标'), 'REJECTED')
|
|
@@ -384,7 +593,7 @@ test('注入迁移(task-muvjs392):清单进首条 prompt,注入区块通
|
|
|
384
593
|
assert.doesNotMatch(dsp, /ctx\.fs|fs\.readText|sliceLines|buildFileOutline/)
|
|
385
594
|
// ③ 瘦身清单本体直接拼进 worker/verifier 两态 prompt(三态其余不变:hook 仍走 buildHookPrompt)
|
|
386
595
|
assert.match(dsp, /buildWorkerPrompt\(t, pack, cfg\(dsnap\)\.feedbackEnabled\)/)
|
|
387
|
-
assert.match(dsp, /buildVerifierPrompt\(t, pack\)/)
|
|
596
|
+
assert.match(dsp, /buildVerifierPrompt\(t, pack, cfg\(dsnap\)\.verifyUserGuide\)/)
|
|
388
597
|
assert.match(dsp, /buildHookPrompt\(t, role === 'hook-pre' \? 'pre' : 'post', kids\)/)
|
|
389
598
|
// ④ readContextPack = 组装清单(不再 await 读盘)——派发点与 rpc 预览点都直接调用
|
|
390
599
|
assert.match(dsp, /function readContextPack\(t\) \{/)
|
|
@@ -1707,7 +1916,7 @@ test('Token 区统计范围接线:get-tasks 接 range 透传 + fetchTasks 带
|
|
|
1707
1916
|
assert.match(cli, /今日与「近 7 天」为固定口径,不随范围变化/)
|
|
1708
1917
|
})
|
|
1709
1918
|
|
|
1710
|
-
// ===== Token 区三连修(task-muwq9u04):范围过滤数据源 / 选范围即渲染 /
|
|
1919
|
+
// ===== Token 区三连修(task-muwq9u04):范围过滤数据源 / 选范围即渲染 / 主数字口径(task-muxnqunk 已翻转为总量主显)=====
|
|
1711
1920
|
test('Token 区①:范围过滤读**任务级 t.runs[]**(run 级留账真实位置),不再误读 usage.runs 计数(源码级断言)', () => {
|
|
1712
1921
|
const usageSrc = readFileSync(new URL('../lib/usage.mjs', import.meta.url), 'utf8')
|
|
1713
1922
|
// 数据源必须取自 t.runs(数组,条目带 at/model/usage);t.usage.runs 只是「结算次数」计数(number)
|
|
@@ -1735,24 +1944,25 @@ test('Token 区②:usageSummary 变化纳入 notify 触发(选范围即渲
|
|
|
1735
1944
|
assert.equal(/state\.usageSummary !== prevUsage/.test(cli), false)
|
|
1736
1945
|
})
|
|
1737
1946
|
|
|
1738
|
-
test('Token 区③:Top8
|
|
1947
|
+
test('Token 区③:Top8 与模型分布主数字取总量(有效进 title),caption 写明口径翻转(源码级断言)', () => {
|
|
1739
1948
|
const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
1740
|
-
//
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
assert.match(cli, /
|
|
1744
|
-
assert.match(cli,
|
|
1745
|
-
|
|
1949
|
+
// 口径翻转(task-muxnqunk,用户 2026-10-07 裁决「算总的吧」,推翻 task-muwq9u04 有效主显):
|
|
1950
|
+
// 主数字一律总量(含缓存读),有效消耗退悬浮 title
|
|
1951
|
+
// 模型分布:主数字取 host 的 byModel(总量),byModelEff(有效分摊)退 title(缺字段标「有效不可知」)
|
|
1952
|
+
assert.match(cli, /var tt = u\.byModel\[m\] \|\| 0/)
|
|
1953
|
+
assert.match(cli, /value: m\.total, max: maxM/)
|
|
1954
|
+
assert.match(cli, /:总量 ' \+ String\(m\.total\) \+ ' tokens(含缓存读)/)
|
|
1955
|
+
// Top8:主数字取 topTasks[].total,effective 退 title
|
|
1746
1956
|
assert.match(cli, /var ee = \(typeof x\.effective === 'number'\) \? x\.effective : null/)
|
|
1747
|
-
assert.match(cli, /
|
|
1748
|
-
assert.match(cli,
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
assert.match(cli, /'
|
|
1754
|
-
assert.match(cli,
|
|
1755
|
-
assert.match(cli,
|
|
1957
|
+
assert.match(cli, /value: x\.total, max: maxT/)
|
|
1958
|
+
assert.match(cli, /(总量 ' \+ String\(x\.total\) \+ ' tokens(含缓存读)/)
|
|
1959
|
+
// 排序按显示口径(总量)降序:条形长度与行序一致
|
|
1960
|
+
assert.match(cli, /\}\)\.sort\(function \(a, b\) \{ return b\.total - a\.total \}\)/)
|
|
1961
|
+
// 标题与 caption 写明口径翻转(主数字均为总量,有效见悬浮)
|
|
1962
|
+
assert.match(cli, /'按模型分布(总量)'/)
|
|
1963
|
+
assert.match(cli, /'任务消耗 Top 8(总量)'/)
|
|
1964
|
+
assert.match(cli, /主数字(今日 \/ 近 7 天 \/ 模型分布 \/ Top 8 \/ 主窗口行)均为总量口径(含缓存读)/)
|
|
1965
|
+
assert.match(cli, /有效消耗(input\+output\+cacheWrite,不含缓存读)见悬浮 title/)
|
|
1756
1966
|
})
|
|
1757
1967
|
|
|
1758
1968
|
test('Token 区③:byModelEff 与累计有效自洽(模型有效分摊不出「模型合计 > 累计有效」的矛盾)', () => {
|
|
@@ -1766,11 +1976,12 @@ test('Token 区③:byModelEff 与累计有效自洽(模型有效分摊不出
|
|
|
1766
1976
|
assert.equal(s.byModel['m-1'], 930)
|
|
1767
1977
|
})
|
|
1768
1978
|
|
|
1769
|
-
test('README 双份同步记录 Token
|
|
1979
|
+
test('README 双份同步记录 Token 口径(总量主显 / 近 7 天 / 有效退悬浮 / 派发口径边界)', () => {
|
|
1770
1980
|
const pkg = readFileSync(new URL('../README.md', import.meta.url), 'utf8')
|
|
1771
1981
|
const root = readFileSync(new URL('../../../README.md', import.meta.url), 'utf8')
|
|
1772
1982
|
assert.equal(pkg, root) // 两份 README 必须字节一致(npm run sync-readme 的约束)
|
|
1773
|
-
|
|
1983
|
+
// 口径翻转(task-muxnqunk):主数字均为总量口径(含缓存读),有效消耗退悬浮
|
|
1984
|
+
for (const s of ['今日总量', '近 7 天', '有效消耗 = 输入 + 输出 + 缓存写(不含缓存读)', '不含主窗口对话', 'run 级留账', '统计范围', '主数字均为总量口径', 't.runs[]']) {
|
|
1774
1985
|
assert.ok(pkg.includes(s), 'README 应记录口径:' + s)
|
|
1775
1986
|
}
|
|
1776
1987
|
})
|
|
@@ -1838,7 +2049,7 @@ test('Token 消耗接线:settleRun 结算累加 + 按模型小计 + get-tasks
|
|
|
1838
2049
|
assert.match(cli, /'⛁ ' \+ fmtTokens\(t\.usage\.total\)/) // 进行中/已完成卡片显示本任务累计
|
|
1839
2050
|
})
|
|
1840
2051
|
|
|
1841
|
-
test('Token 日账接线:dispatch 记 byDay 双指标(本地日)+
|
|
2052
|
+
test('Token 日账接线:dispatch 记 byDay 双指标(本地日)+ 仪表盘总量口径(源码级断言)', () => {
|
|
1842
2053
|
const host = hostSrc()
|
|
1843
2054
|
const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
1844
2055
|
assert.match(host, /cell\.t = \(Number\(cell\.t\) \|\| 0\) \+ u\.total/) // 结算时记一笔日账总量
|
|
@@ -1854,26 +2065,24 @@ test('Token 日账接线:dispatch 记 byDay 双指标(本地日)+ 仪表
|
|
|
1854
2065
|
assert.equal(/toISOString/.test(dkBody[0]), false) // UTC 会让晚间消耗落到次日
|
|
1855
2066
|
assert.match(host, /if \(!t\.usage\.byDay\) t\.usage\.byDay = \{\}/) // 老任务就地补日账(不改老字段形态)
|
|
1856
2067
|
assert.match(host, /lc\.t \+= u\.total; lc\.e = null; s\.byDay\[lk\] = lc/) // 存量兜底归 updatedAt 本地日(只有总量,e 不可知)
|
|
1857
|
-
//
|
|
2068
|
+
// 仪表盘:大数字=今日总量(含缓存读)+「其中有效 X」小字 + 累计三分量 + 近 7 天总量柱 + 口径 caption
|
|
2069
|
+
// (口径翻转 task-muxnqunk:主显总量,有效退悬浮/次要位;旧口径灰柱规则退役——见翻转测试负断言)
|
|
1858
2070
|
assert.match(cli, /function lastNDays\(n\)/)
|
|
1859
2071
|
assert.match(cli, /var todayCell = dayOf\(byDay\[todayKey\]\)/) // 今日缺省退化(无日账不误报)
|
|
1860
|
-
assert.match(cli, /'tokens
|
|
1861
|
-
assert.match(cli, /'
|
|
2072
|
+
assert.match(cli, /'tokens(今日总量,含缓存读)'/) // 大数字口径 = 总量(含缓存读)
|
|
2073
|
+
assert.match(cli, /'其中有效 '/) // 有效消耗退居次要小字(对照总量)
|
|
1862
2074
|
assert.match(cli, /'累计(本看板) 有效 '/) // 累计三分量:有效
|
|
1863
2075
|
assert.match(cli, /' · 缓存读 '/) // 累计三分量:缓存读
|
|
1864
2076
|
assert.match(cli, /' · 合计 '/) // 累计三分量:合计
|
|
1865
|
-
assert.match(cli, /'近 7
|
|
1866
|
-
assert.match(cli, /k\.slice\(5\) \+ '
|
|
1867
|
-
assert.match(cli,
|
|
2077
|
+
assert.match(cli, /'近 7 天(总量,含缓存读)'/)
|
|
2078
|
+
assert.match(cli, /k\.slice\(5\) \+ ':总量 ' \+ String\(v\)/) // 条形 title:MM-DD:总量 N tok
|
|
2079
|
+
assert.match(cli, /' \/ 有效 ' \+ String\(c\.e\) \+ ' tok'/) // 条形 title 补有效对照
|
|
1868
2080
|
assert.match(cli, /口径:累计与 Top 8 仅看板派发的 Worker\/Verifier run 消耗/) // 口径边界明示(累计/Top8 仅 run 口径)
|
|
1869
2081
|
assert.match(cli, /主窗口行=本会话对话消耗,与看板派发口径并列不混入/) // 主窗口行并列不混入(task-muwsol23 起单列展示)
|
|
1870
|
-
assert.match(cli,
|
|
1871
|
-
assert.match(cli, /其中缓存读 ' \+ String\(x\.cacheRead \|\| 0\)/) // Top8 title 补 缓存读
|
|
1872
|
-
assert.match(cli, /background:
|
|
2082
|
+
assert.match(cli, /主数字(今日 \/ 近 7 天 \/ 模型分布 \/ Top 8 \/ 主窗口行)均为总量口径(含缓存读)/) // 主数字口径翻转(task-muxnqunk)
|
|
2083
|
+
assert.match(cli, /其中缓存读 ' \+ String\(x\.cacheRead \|\| 0\)/) // Top8 title 补 缓存读 拆分(有效进 title)
|
|
2084
|
+
assert.match(cli, /background: isToday \? C\.brand : C\.nested/) // 今天高亮 brand、其余浅底(灰柱规则已退役)
|
|
1873
2085
|
assert.match(cli, /hasDayData \? React\.createElement/) // 7 天全空不渲染该区
|
|
1874
|
-
// e 不可知的旧口径日:不拿总量冒充有效——今日「—」/ 柱形矮灰柱 + tooltip 标明(task-muwsnyqv ③,
|
|
1875
|
-
// 旧「标 ~ 近似」分支已除,见三连修测试的反向断言)
|
|
1876
|
-
assert.match(cli, /旧口径数据(仅总量/)
|
|
1877
2086
|
})
|
|
1878
2087
|
|
|
1879
2088
|
// ===== Token 口径三连修(task-muwsnyqv):byDay 闭区间 / byDayFull 固定口径 / 旧口径不冒充 =====
|
|
@@ -1934,26 +2143,33 @@ test('Token 三连修②:byDayFull 未过滤全量与 byDay 并存(今日/
|
|
|
1934
2143
|
assert.match(cli, /var byDay = \(u\.byDayFull && typeof u\.byDayFull === 'object'\) \? u\.byDayFull : \(\(u\.byDay && typeof u\.byDay === 'object'\) \? u\.byDay : \{\}\)/)
|
|
1935
2144
|
})
|
|
1936
2145
|
|
|
1937
|
-
|
|
2146
|
+
// ===== Token 口径翻转(task-muxnqunk,用户 2026-10-07 裁决「算总的吧」):总量主显 / 有效退悬浮 / 灰柱退役 =====
|
|
2147
|
+
// 推翻 task-muwq9u04 的「有效消耗主显」:主数字一律总量 t(含缓存读),有效消耗 e 退居 title/次要位;
|
|
2148
|
+
// task-muwsnyqv ③ 的旧口径灰柱规则顺势退役(t 恒有值,e===null 的日子现在就是正常柱)。
|
|
2149
|
+
// 不动的:模型表现区与质量趋势区保持有效口径(效率指标语义),报告导出分列口径不动。
|
|
2150
|
+
test('Token 口径翻转:总量主显(含缓存读)+ 有效退 title + 旧口径灰柱规则退役(源码级断言)', () => {
|
|
1938
2151
|
const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
1939
|
-
//
|
|
1940
|
-
assert.match(cli, /
|
|
1941
|
-
assert.match(cli, /var v =
|
|
1942
|
-
assert.match(cli, /
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
assert.match(cli, /
|
|
1946
|
-
//
|
|
1947
|
-
assert.match(cli,
|
|
1948
|
-
assert.match(cli,
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
assert.equal(/
|
|
1952
|
-
assert.equal(/
|
|
2152
|
+
// 近 7 天柱形:按总量归一与画柱(t 恒有值,e===null 的旧口径日就是正常柱)
|
|
2153
|
+
assert.match(cli, /if \(dc\.t > maxDay\) maxDay = dc\.t/) // maxDay 按总量归一
|
|
2154
|
+
assert.match(cli, /var v = c\.t \|\| 0/) // 柱高取总量
|
|
2155
|
+
assert.match(cli, /background: isToday \? C\.brand : C\.nested/) // 今天高亮 brand,其余浅底(无灰柱分支)
|
|
2156
|
+
// 今日大数字:显总量 todayCell.t(byDayFull 口径不变),有效退 title / 次要小字
|
|
2157
|
+
assert.match(cli, /fmtTokens\(todayCell\.t\)/)
|
|
2158
|
+
assert.match(cli, /'tokens(今日总量,含缓存读)'/)
|
|
2159
|
+
// caption 新文案:主数字均为总量口径(含缓存读);有效消耗见悬浮
|
|
2160
|
+
assert.match(cli, /主数字(今日 \/ 近 7 天 \/ 模型分布 \/ Top 8 \/ 主窗口行)均为总量口径(含缓存读)/)
|
|
2161
|
+
assert.match(cli, /有效消耗(input\+output\+cacheWrite,不含缓存读)见悬浮 title/)
|
|
2162
|
+
// 负断言:旧口径灰柱规则(task-muwsnyqv ③)顺势退役——legacy 分支 / 灰柱图例 / 今日「—」防护全删
|
|
2163
|
+
assert.equal(/var legacy = /.test(cli), false)
|
|
2164
|
+
assert.equal(/var todayLegacy/.test(cli), false)
|
|
2165
|
+
assert.equal(/hasLegacy/.test(cli), false)
|
|
2166
|
+
assert.equal(/灰柱 = 旧口径数据/.test(cli), false)
|
|
2167
|
+
assert.equal(/旧口径数据(仅总量/.test(cli), false)
|
|
2168
|
+
assert.equal(/今日有效消耗不可知/.test(cli), false)
|
|
1953
2169
|
assert.equal(/(近似:老日账只有总量)/.test(cli), false)
|
|
1954
|
-
// README
|
|
2170
|
+
// README 双份记录口径翻转
|
|
1955
2171
|
const pkg = readFileSync(new URL('../README.md', import.meta.url), 'utf8')
|
|
1956
|
-
assert.ok(pkg.includes('
|
|
2172
|
+
assert.ok(pkg.includes('主数字均为总量口径'), 'README 应记录 Token 区口径翻转(总量主显)')
|
|
1957
2173
|
})
|
|
1958
2174
|
|
|
1959
2175
|
// ===== 调研 ROI 行 token 有效口径(task-muupnnq5)=====
|
|
@@ -2130,7 +2346,7 @@ test('学习飞轮接线:两处触发点 + push-lesson 开关拦截 + prompt/
|
|
|
2130
2346
|
test('架构健康接线:get-tasks 返回 healthHints + 仪表盘 HealthHints 区缺省兼容渲染', () => {
|
|
2131
2347
|
const host = hostSrc()
|
|
2132
2348
|
const cli = readFileSync(new URL('../lib/client.js', import.meta.url), 'utf8')
|
|
2133
|
-
assert.match(host, /d\.healthHints = computeHealthHints\(d\.tasks\)/)
|
|
2349
|
+
assert.match(host, /d\.healthHints = __rh\.hints\.concat\(computeHealthHints\(d\.tasks\)\)/) // host 每次请求现算返回(零存储);运行时 hint(task-muxhrkbg)拼在静态信号前面
|
|
2134
2350
|
assert.match(cli, /function HealthHints\(\)/) // 仪表盘提示区组件
|
|
2135
2351
|
assert.match(cli, /React\.createElement\(HealthHints\)/) // 挂进 Dashboard(统计卡行下方、Token 消耗区上方)
|
|
2136
2352
|
assert.match(cli, /Array\.isArray\(d\.healthHints\)/) // 缺省兼容:老 host 无字段/非数组 → 按空处理
|
|
@@ -2267,8 +2483,8 @@ test('aggregateChildStats: 归档口径注释写明僵尸卡近似 + 旧「归
|
|
|
2267
2483
|
// 防回归:聚合循环里不得再按 archived 跳过
|
|
2268
2484
|
assert.doesNotMatch(coreSrc, /aggregateChildStats[\s\S]{0,600}?status === 'archived'\) continue/)
|
|
2269
2485
|
assert.match(coreSrc, /if \(!t \|\| !isb\(t\)\) continue/)
|
|
2270
|
-
// 与父卡自动收口口径(isChildSettled
|
|
2271
|
-
assert.match(coreSrc, /与 isChildSettled
|
|
2486
|
+
// 与父卡自动收口口径(isChildSettled)现已同口径:archived 计入终态集,注释里写明(反馈 n-muw706h1uymy)
|
|
2487
|
+
assert.match(coreSrc, /与 isChildSettled(父卡自动收口口径)现已同口径/)
|
|
2272
2488
|
// 僵尸放行本身的门禁注释仍在 rpc.mjs(口径来源不悬空)
|
|
2273
2489
|
const rpcSrc = readFileSync(new URL('../lib/rpc.mjs', import.meta.url), 'utf8')
|
|
2274
2490
|
assert.match(rpcSrc, /in-progress 且无活跃 run\s+→ 放行(parentKick 僵尸态出清/)
|
|
@@ -2928,7 +3144,7 @@ test('可续跑 Worker 接线(源码级):事件订阅走 ctx.effect 回收
|
|
|
2928
3144
|
// ⑥ 板级开关三件套(cfg 兜底 / normalizeBoard 补缺省 / seed 初值 / set-board-config 白名单 / get-tasks 透出)
|
|
2929
3145
|
assert.match(coreSrc, /workerContinuable: d\.workerContinuable !== false/)
|
|
2930
3146
|
assert.match(coreSrc, /if \(typeof d\.workerContinuable !== 'boolean'\) d\.workerContinuable = true/)
|
|
2931
|
-
assert.match(coreSrc, /epicSplit: true, workerContinuable: true, minWorkers: 1/)
|
|
3147
|
+
assert.match(coreSrc, /epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1/)
|
|
2932
3148
|
assert.match(rpcSrc, /else if \(args\.key === 'workerContinuable'\) d\.workerContinuable = !!args\.value/)
|
|
2933
3149
|
assert.match(rpcSrc, /d\.workerContinuable = cfg\(d\)\.workerContinuable/)
|
|
2934
3150
|
// ⑦ 手动终止/活动查询对 continuable rec 不炸(id 统一取 rec.id,dispose 分路)
|
|
@@ -3272,10 +3488,11 @@ test('史诗语义层接线:poolCycle 派发分支触发父卡流转 + get-tas
|
|
|
3272
3488
|
// get-tasks 现算 childStats(零存储)
|
|
3273
3489
|
assert.match(host, /d\.childStats = aggregateChildStats\(d\.tasks\)/)
|
|
3274
3490
|
// 两模块都从 core 解构引入(接线不断);rpc.mjs 解构表尾部随调研门禁(task-mute6zpw)、调研遵循三件套(task-mutnj3a4)、
|
|
3275
|
-
// tasksHash(task-mutrtwin)、hooks 浅校验(normalizeHooks/mergeHooks,本批 hooks=agent run
|
|
3491
|
+
// tasksHash(task-mutrtwin)、hooks 浅校验(normalizeHooks/mergeHooks,本批 hooks=agent run)、
|
|
3492
|
+
// 自测指南归一(normalizeUserTest,task-muxyyvg0)扩展;
|
|
3276
3493
|
// dispatch.mjs 解构表尾部追加 hook 族(buildHookPrompt/hookOn/hookSetState/gsb)与驳回包 helper(pushRejection,task-muvg15p5)
|
|
3277
3494
|
assert.match(host, /parentKickOnDispatch, LESSON_RECALL_HINT, buildHookPrompt, applyHookSettle, hookOn, hookSetState, gsb, pushRejection \} = core/)
|
|
3278
|
-
assert.match(host, /boardHome, aggregateChildStats, createTaskWarnings, epicPrecheck, epicPrecheckNote, attachContextSuggestions, REJECT_REDISPATCH_HINT, tasksHash, normalizeHooks, mergeHooks, pushRejection \} = core/)
|
|
3495
|
+
assert.match(host, /boardHome, aggregateChildStats, createTaskWarnings, epicPrecheck, epicPrecheckNote, attachContextSuggestions, REJECT_REDISPATCH_HINT, tasksHash, normalizeHooks, mergeHooks, pushRejection, normalizeUserTest \} = core/)
|
|
3279
3496
|
// 既有「全子任务 resolved → 父 verifying」逻辑不动(checkParentAuto 仍在 verifyApply 链路;
|
|
3280
3497
|
// 但其内部已委托共享 helper maybeAutoCloseParent——单一判定口径,core.mjs 不在 hostSrc 清单,单独读)
|
|
3281
3498
|
const coreSrc = readFileSync(new URL('../lib/core.mjs', import.meta.url), 'utf8')
|
|
@@ -3633,6 +3850,74 @@ test('task_archive 工具:与 archive-task RPC 同一门禁口径(僵尸放
|
|
|
3633
3850
|
assert.equal(r.ok, false); assert.match(r.error, /cannot archive/)
|
|
3634
3851
|
})
|
|
3635
3852
|
|
|
3853
|
+
// ===== 反馈修复(task-mux3uvx3,反馈 n-muw706h1uymy):归档路径触发父卡收口 + task_resolve 守卫对齐 =====
|
|
3854
|
+
test('归档双通道触发父卡自动收口:archive-task RPC 与 task_archive 工具同一 archiveApply 口径', async () => {
|
|
3855
|
+
// RPC 通道:归档末子卡 → 父卡 in-progress → verifying
|
|
3856
|
+
const p = mkTask({ id: 'ep', status: 'in-progress' })
|
|
3857
|
+
const k1 = mkTask({ id: 'k1', parentId: 'ep', status: 'resolved' })
|
|
3858
|
+
const k2 = mkTask({ id: 'k2', parentId: 'ep', status: 'resolved' })
|
|
3859
|
+
const board = mkBoard([p, k1, k2])
|
|
3860
|
+
const h = mkRpcHandlers(board)
|
|
3861
|
+
const r = await h['archive-task']({ taskId: 'k2' })
|
|
3862
|
+
assert.equal(r.ok, true); assert.equal(r.parentUpdated, true)
|
|
3863
|
+
assert.equal(p.status, 'verifying')
|
|
3864
|
+
assert.equal(p.resolution, 'all subtasks resolved')
|
|
3865
|
+
// 工具通道同口径
|
|
3866
|
+
const p2 = mkTask({ id: 'ep2', status: 'in-progress' })
|
|
3867
|
+
const j1 = mkTask({ id: 'j1', parentId: 'ep2', status: 'resolved' })
|
|
3868
|
+
const j2 = mkTask({ id: 'j2', parentId: 'ep2', status: 'resolved' })
|
|
3869
|
+
const h2 = mkRpcHandlers(mkBoard([p2, j1, j2]))
|
|
3870
|
+
const r2 = await h2.__tools['task_archive'].execute({ taskId: 'j2' }, {})
|
|
3871
|
+
assert.equal(r2.ok, true); assert.equal(r2.parentUpdated, true); assert.equal(p2.status, 'verifying')
|
|
3872
|
+
// 未凑齐不收口(还剩在跑子卡):归档其中一张已 resolved 的,父卡不动
|
|
3873
|
+
const p3 = mkTask({ id: 'ep3', status: 'in-progress' })
|
|
3874
|
+
const m1 = mkTask({ id: 'm1', parentId: 'ep3', status: 'resolved' })
|
|
3875
|
+
const m2 = mkTask({ id: 'm2', parentId: 'ep3', status: 'in-progress' })
|
|
3876
|
+
const h3 = mkRpcHandlers(mkBoard([p3, m1, m2]), { hasActiveRun: () => true })
|
|
3877
|
+
const r3 = await h3['archive-task']({ taskId: 'm1' })
|
|
3878
|
+
assert.equal(r3.ok, true); assert.equal('parentUpdated' in r3, false); assert.equal(p3.status, 'in-progress')
|
|
3879
|
+
})
|
|
3880
|
+
|
|
3881
|
+
test('batch-op archive:整批落定后触发父卡收口(第三条归档路径同口径);未凑齐不收口', async () => {
|
|
3882
|
+
// 末两张子卡一次批量归档 → 父卡 verifying(旧路径归档后从不查父卡 → 同样卡死)
|
|
3883
|
+
const p = mkTask({ id: 'bp', status: 'in-progress' })
|
|
3884
|
+
const b1 = mkTask({ id: 'b1', parentId: 'bp', status: 'resolved' })
|
|
3885
|
+
const b2 = mkTask({ id: 'b2', parentId: 'bp', status: 'resolved' })
|
|
3886
|
+
const b3 = mkTask({ id: 'b3', parentId: 'bp', status: 'cancelled' })
|
|
3887
|
+
const h = mkRpcHandlers(mkBoard([p, b1, b2, b3]))
|
|
3888
|
+
const r = await h['batch-op']({ op: 'archive', ids: ['b1', 'b2', 'b3'] })
|
|
3889
|
+
assert.equal(r.ok, true); assert.equal(r.done, 3); assert.equal(r.parentsClosed, 1)
|
|
3890
|
+
assert.equal(p.status, 'verifying')
|
|
3891
|
+
// 未凑齐(还有在跑子卡)→ 不收口、不挂 parentsClosed 字段(返回体老契约不变)
|
|
3892
|
+
const p2 = mkTask({ id: 'bp2', status: 'in-progress' })
|
|
3893
|
+
const d1 = mkTask({ id: 'd1', parentId: 'bp2', status: 'resolved' })
|
|
3894
|
+
const d2 = mkTask({ id: 'd2', parentId: 'bp2', status: 'in-progress' })
|
|
3895
|
+
const h2 = mkRpcHandlers(mkBoard([p2, d1, d2]))
|
|
3896
|
+
const r2 = await h2['batch-op']({ op: 'archive', ids: ['d1'] })
|
|
3897
|
+
assert.equal(r2.ok, true); assert.equal('parentsClosed' in r2, false); assert.equal(p2.status, 'in-progress')
|
|
3898
|
+
// 不级联:批量只动显式选中的 id(与单卡 archiveApply 的级联刻意不同)
|
|
3899
|
+
assert.equal(d2.status, 'in-progress')
|
|
3900
|
+
})
|
|
3901
|
+
|
|
3902
|
+
test('task_resolve 工具:claimedBy=null 的 in-progress 卡放行主窗口(对齐 RPC 口径);他人认领照旧拒', async () => {
|
|
3903
|
+
// claimedBy=null(parentKick 僵尸 epic / 手动置 in-progress 的无人认领卡)→ 放行(工具已有主窗口门禁)
|
|
3904
|
+
const t1 = mkTask({ id: 'u1', status: 'in-progress', claimedBy: null })
|
|
3905
|
+
const r1 = await mkRpcHandlers(mkBoard([t1])).__tools['task_resolve'].execute({ taskId: 'u1', status: 'blocked' }, {})
|
|
3906
|
+
assert.equal(r1.ok, true); assert.equal(t1.status, 'blocked')
|
|
3907
|
+
// verifying + resolution 同样放行(父卡检查仍由 resolveApply 内部触发,行为不变)
|
|
3908
|
+
const t2 = mkTask({ id: 'u2', status: 'in-progress', claimedBy: null, pipeline: 'full' })
|
|
3909
|
+
const r2 = await mkRpcHandlers(mkBoard([t2])).__tools['task_resolve'].execute({ taskId: 'u2', status: 'verifying', resolution: '手动兜底' }, {})
|
|
3910
|
+
assert.equal(r2.ok, true); assert.equal(t2.status, 'verifying')
|
|
3911
|
+
// 他人认领 → 照旧拒(既有守卫场景不回归)
|
|
3912
|
+
const t3 = mkTask({ id: 'u3', status: 'in-progress', claimedBy: 'other' })
|
|
3913
|
+
const r3 = await mkRpcHandlers(mkBoard([t3])).__tools['task_resolve'].execute({ taskId: 'u3', status: 'verifying', resolution: 'x' }, {})
|
|
3914
|
+
assert.equal(r3.ok, false); assert.match(r3.error, /not claimed by you/); assert.equal(t3.status, 'in-progress')
|
|
3915
|
+
// 本人认领 → 放行
|
|
3916
|
+
const t4 = mkTask({ id: 'u4', status: 'in-progress', claimedBy: 'tester', pipeline: 'full' })
|
|
3917
|
+
const r4 = await mkRpcHandlers(mkBoard([t4])).__tools['task_resolve'].execute({ taskId: 'u4', status: 'verifying', resolution: 'x' }, {})
|
|
3918
|
+
assert.equal(r4.ok, true); assert.equal(t4.status, 'verifying')
|
|
3919
|
+
})
|
|
3920
|
+
|
|
3636
3921
|
test('反馈修复接线断言:index.mjs 注入 hasActiveRun + get-tasks 附 tasksHash + kernel 短路分支(源码级)', () => {
|
|
3637
3922
|
const host = hostSrc()
|
|
3638
3923
|
// index.mjs:hasActiveRun 定义(runsFor 表有记录且 !settled 即活跃)并注入 createRpc deps
|
|
@@ -4056,7 +4341,7 @@ test('epicSplit 接线(源码级):缓存同步 / Team 引导段门禁 /
|
|
|
4056
4341
|
// 配置层(core.mjs:cfg 兜底 / seed 初值 / normalizeBoard 老看板补齐)
|
|
4057
4342
|
assert.match(coreSrc, /epicSplit: d\.epicSplit !== false/)
|
|
4058
4343
|
assert.match(coreSrc, /if \(typeof d\.epicSplit !== 'boolean'\) d\.epicSplit = true/)
|
|
4059
|
-
assert.match(coreSrc, /notifyDone: true, epicSplit: true, workerContinuable: true, minWorkers: 1/) // seed 缺省开(workerContinuable 随 task-muw5gnhv 插入 epicSplit 与 minWorkers 之间)
|
|
4344
|
+
assert.match(coreSrc, /notifyDone: true, epicSplit: true, workerContinuable: true, verifyUserGuide: true, minWorkers: 1/) // seed 缺省开(workerContinuable 随 task-muw5gnhv、verifyUserGuide 随 task-muxyyvg0 插入 epicSplit 与 minWorkers 之间)
|
|
4060
4345
|
// 缓存链路:rt() 读盘同步 + set-board-config 当场回填(Team 提示词是同步组装,只能读缓存)
|
|
4061
4346
|
assert.match(src, /epicSplitCache\[sid\] = nd\.epicSplit !== false/)
|
|
4062
4347
|
assert.match(src, /function epicSplitOn\(sid\) \{ return epicSplitCache\[sid\] !== false \}/)
|
|
@@ -4507,7 +4792,7 @@ test('池配置开关乐观更新:每个开关先写 state+notify 再 rpc,
|
|
|
4507
4792
|
assert.ok(m, '开关 onChange 必须是 setCfg(键, e.target.checked) 单调用,实测:' + h)
|
|
4508
4793
|
keys.push(m[1])
|
|
4509
4794
|
}
|
|
4510
|
-
assert.deepEqual(keys.slice().sort(), ['epicSplit', 'feedbackEnabled', 'notifyDispatch', 'notifyDone'])
|
|
4795
|
+
assert.deepEqual(keys.slice().sort(), ['epicSplit', 'feedbackEnabled', 'notifyDispatch', 'notifyDone', 'verifyUserGuide'])
|
|
4511
4796
|
// ② 乐观序:先写 state、再 notify、最后才 rpc(顺序颠倒 = 点击仍等往返)
|
|
4512
4797
|
const setCfgMatch = src.match(/function setCfg\(key, next\) \{[\s\S]*?\n \}/)
|
|
4513
4798
|
assert.ok(setCfgMatch, '应定义 setCfg')
|
|
@@ -4556,17 +4841,17 @@ test('池配置开关乐观更新:每个开关先写 state+notify 再 rpc,
|
|
|
4556
4841
|
|
|
4557
4842
|
test('fetchTasks 设置开关权威纠偏:hash 不变时配置变化补 notify + hash 短路结构保持(源码级)', () => {
|
|
4558
4843
|
const src = readFileSync(new URL('../lib/client/kernel.js', import.meta.url), 'utf8')
|
|
4559
|
-
// ① 取值口径单点定义(缺字段/脏值=开,只有显式 false
|
|
4844
|
+
// ① 取值口径单点定义(缺字段/脏值=开,只有显式 false 才关),且五个开关字段确由它赋值
|
|
4560
4845
|
const knobSrc = src.match(/function cfgKnobOf\(src, key\) \{[^\n]*\n/)[0]
|
|
4561
4846
|
const changedSrc = src.match(/function cfgKnobsChanged\(a, b\) \{[^\n]*\n/)[0]
|
|
4562
4847
|
const cfgKnobOf = new Function(knobSrc + '\nreturn cfgKnobOf')()
|
|
4563
4848
|
const cfgKnobsChanged = new Function(changedSrc + '\nreturn cfgKnobsChanged')()
|
|
4564
4849
|
assert.equal(cfgKnobOf({}, 'notifyDispatch'), true) // 老 host 无字段 → 开
|
|
4565
4850
|
assert.equal(cfgKnobOf({ notifyDispatch: false }, 'notifyDispatch'), false) // 只有显式 false 才关
|
|
4566
|
-
const KNOBS = ['feedbackEnabled', 'notifyDispatch', 'notifyDone', 'epicSplit']
|
|
4851
|
+
const KNOBS = ['feedbackEnabled', 'notifyDispatch', 'notifyDone', 'epicSplit', 'verifyUserGuide']
|
|
4567
4852
|
for (const k of KNOBS) assert.match(src, new RegExp("state\\." + k + " = cfgKnobOf\\(d, '" + k + "'\\)"))
|
|
4568
|
-
// ②
|
|
4569
|
-
const allOn = { feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true }
|
|
4853
|
+
// ② 真执行比较器:五个开关任一翻转都算变化(漏一个 → 该开关又要等下一次任意 notify)
|
|
4854
|
+
const allOn = { feedbackEnabled: true, notifyDispatch: true, notifyDone: true, epicSplit: true, verifyUserGuide: true }
|
|
4570
4855
|
assert.equal(cfgKnobsChanged(allOn, Object.assign({}, allOn)), false)
|
|
4571
4856
|
for (const k of KNOBS) {
|
|
4572
4857
|
assert.equal(cfgKnobsChanged(allOn, Object.assign({}, allOn, { [k]: false })), true, k + ' 翻转必须被检测到')
|
|
@@ -4782,9 +5067,11 @@ test('池冻结根修⑤(接线,源码级):幽灵回收在活跃度计
|
|
|
4782
5067
|
const iCount = dsp.indexOf('if (rc0.role === \'worker\') activeW++')
|
|
4783
5068
|
assert.ok(iReap > 0 && iCount > iReap, '幽灵回收必须先于活跃度计数')
|
|
4784
5069
|
// ② 回收后重读快照(否则空闲快进的 wt(snap) 会把收尾结果覆盖回旧状态)
|
|
4785
|
-
|
|
5070
|
+
// (task-muxhrkbg 在同一块内追加了 reapNote 打点,块体由单行扩成多行——语义不变,断言随之放宽到块形状)
|
|
5071
|
+
assert.match(dsp, /if \(reapedN > 0\) \{\s*try \{ snap = await rt\(sid\) \} catch \(_\) \{\}/)
|
|
4786
5072
|
// ③ 本轮主体包 try(整轮异常隔离),且刻意不设再入 latch
|
|
4787
|
-
|
|
5073
|
+
// (task-muxhrkbg 在 try 块内追加 poolLastOkAt 心跳打点:主体正常返回才刷新——异常隔离语义由下面的 catch 断言继续锁死)
|
|
5074
|
+
assert.match(dsp, /try \{\s*var cycleRet = await poolCycleBody\(sid, info, runs, snap\)\s*poolHealthFor\(sid\)\.poolLastOkAt = Date\.now\(\)\s*return cycleRet\s*\}/)
|
|
4788
5075
|
assert.match(dsp, /catch \(e\) \{ console\.error\('\[task-board\] poolCycle 本轮异常(已隔离,下一轮照常)/)
|
|
4789
5076
|
assert.match(dsp, /刻意\*\*不设\*\*再入守卫/)
|
|
4790
5077
|
// ④ 持锁段按阶段隔离(五段各自 try/catch,回调照样返回 d → 该写的池状态一定写下去)
|