dsh-agent-board 1.7.9 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/index.mjs +1 -0
- package/lib/client/dashboard.js +11 -8
- package/lib/client.js +11 -8
- package/lib/core.mjs +15 -2
- package/lib/dispatch.mjs +122 -5
- package/lib/rpc.mjs +71 -9
- package/lib/usage.mjs +13 -4
- package/package.json +1 -1
- package/test/core.test.mjs +258 -1
- package/test/integration.test.mjs +153 -0
- package/test/scoreboard-client.test.mjs +15 -9
- package/test/scoreboard.test.mjs +44 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// dsh-agent-board — 集成套件(无头基建卡2 前半 + 卡3 后半):
|
|
2
2
|
// 前半(a-d):全链路 / 驳回重派 / dependsOn 接续 / touches 排他
|
|
3
3
|
// 后半(e-i,task-muxpgq5b):仲裁冻结时钟推进 / 回执聚合过滤 / 歧义去抖 / progress / 孤儿回收
|
|
4
|
+
// 竞态根修(k-m,task-mv6c7cdm):terminate×续跑在途落闸弃权 / 正常续跑不回归 / fresh spawn 在途弃权
|
|
4
5
|
// 九场景全部跑在「真实 index.mjs apply(mockCtx)」上(卡1 harness),不改生产代码;
|
|
5
6
|
// 每用例独立 mockCtx + 独立临时 HOME,互不污染(HOME env 是进程级,顶层用例默认串行,严禁并行)。
|
|
6
7
|
//
|
|
@@ -541,3 +542,155 @@ test('集成j·归档数据源全量:老归档(>7天)与仪表盘同口径,g
|
|
|
541
542
|
assert.equal(d2.tasks.filter(function (t) { return t.status === 'archived' }).length, 2, '无 includeArchived 时同样全量(与仪表盘计数同源)')
|
|
542
543
|
} finally { await env.cleanup() }
|
|
543
544
|
})
|
|
545
|
+
|
|
546
|
+
// ===== 场景 k:terminate × 续跑在途竞态根修①(task-mv6c7cdm,实证 n-mv65bhk0mg5w)=====
|
|
547
|
+
// 竞态时序复刻:首轮 Worker 失败回 pending(留档 incomplete+continuable 有续跑资格)→ 重派轮
|
|
548
|
+
// sendMessage 冷复活挂起(在途窗口)→ terminate 恰好落进窗口(活跃表无 rec,interrupt 打不到、
|
|
549
|
+
// 关账轮不到,只把卡翻回 pending)→ 放行 sendMessage。修复前:续跑照旧落闸(注册 rec+落留档),
|
|
550
|
+
// 下一轮 cycle 又把 pending 卡 fresh spawn → 同卡双 run。修复后:落闸前锁内重验发现卡面已翻走 →
|
|
551
|
+
// 弃权三件套(interrupt 刚复活的子会话 + 旧留档立 noResume + 不注册/不落档),下一轮 fresh spawn
|
|
552
|
+
// 新 Worker——同卡同秒最多一个 spawn 落闸。
|
|
553
|
+
test('集成k·terminate竞态①:续跑 spawn 在途被 terminate → 落闸锁内重验弃权(interrupt+noResume),同卡不再双 run', async () => {
|
|
554
|
+
var env = createMockCtx()
|
|
555
|
+
try {
|
|
556
|
+
plugin.apply(env.ctx)
|
|
557
|
+
await createTask(env, 'task-k1', { pipeline: 'work' })
|
|
558
|
+
await env.clock.advance(50)
|
|
559
|
+
await env.waitFor(function () { return env.continuables.length >= 1 }, '首轮 worker spawn 发生')
|
|
560
|
+
var child1 = env.continuables[0].childId
|
|
561
|
+
await env.waitFor(function () { var x = env.task('task-k1'); return x && x.status === 'in-progress' && x.claimedBy === child1 ? x : null }, '落卡 in-progress')
|
|
562
|
+
// 首轮失败(不写子会话日志 → 空文本按失败重排)→ pending + 留档 incomplete(有续跑资格)
|
|
563
|
+
env.emitStatus(child1, 'running')
|
|
564
|
+
env.emitStatus(child1, 'idle')
|
|
565
|
+
await env.waitFor(function () { var x = env.task('task-k1'); return x && x.status === 'pending' && x.runs && x.runs[0] && x.runs[0].outcome === 'incomplete' ? x : null }, '失败回 pending 且留档 incomplete')
|
|
566
|
+
// ===== 构造竞态时序:续跑 sendMessage 挂起,在途窗口由测试掌控 =====
|
|
567
|
+
var origSend = env.ctx.subagents.sendMessage
|
|
568
|
+
var releaseResume = null
|
|
569
|
+
env.ctx.subagents.sendMessage = function (parent, childId, content) {
|
|
570
|
+
env.sendMessages.push({ parent: parent, childId: childId, content: content })
|
|
571
|
+
return new Promise(function (res) { releaseResume = function () { res({ ok: true }) } })
|
|
572
|
+
}
|
|
573
|
+
// 触发重派轮:占位 claim → tryResumeWorker → sendMessage 在途挂起
|
|
574
|
+
await env.clock.advance(50)
|
|
575
|
+
await env.waitFor(function () { return env.sendMessages.length >= 1 }, '续跑 sendMessage 已发出(在途挂起)')
|
|
576
|
+
var t = env.task('task-k1')
|
|
577
|
+
assert.equal(t.status, 'in-progress')
|
|
578
|
+
assert.equal(t.claimedBy, 'spawn-pending', '续跑在途时占位仍在')
|
|
579
|
+
// ===== terminate 落进在途窗口(竞态前提:活跃表无 rec,拦不住在途 spawn)=====
|
|
580
|
+
var term = await env.rpc('terminate-agent', { sessionId: env.sid, taskId: 'task-k1' })
|
|
581
|
+
assert.equal(term.ok, true)
|
|
582
|
+
assert.equal(term.terminated, 'no-active-run', '竞态前提成立:terminate 看不到在途续跑(无 rec 可 interrupt)')
|
|
583
|
+
t = env.task('task-k1')
|
|
584
|
+
assert.equal(t.status, 'pending', 'terminate 把卡翻回 pending')
|
|
585
|
+
assert.equal(t.claimedBy, null, 'terminate 清掉占位')
|
|
586
|
+
// ===== 放行 sendMessage → 落闸锁内重验:卡面已翻走 → 弃权 =====
|
|
587
|
+
releaseResume()
|
|
588
|
+
await env.waitFor(function () { return env.interrupts.some(function (x) { return x.childId === child1 }) }, '弃权即 interrupt 刚复活的子会话(防空跑烧上下文)')
|
|
589
|
+
t = await env.waitFor(function () { var x = env.task('task-k1'); return x && x.runs && x.runs[0] && x.runs[0].noResume === true ? x : null }, '旧留档立 noResume(与 terminate 命中同语义:不再续跑)')
|
|
590
|
+
// 弃权零落闸:不注册 rec、不落续跑留档、不新建子会话
|
|
591
|
+
assert.equal(env.continuables.length, 1, '弃权不新建子会话')
|
|
592
|
+
assert.equal(t.runs.length, 1, '弃权不落续跑留档(仍只有首轮一条)')
|
|
593
|
+
assert.equal(t.status, 'pending')
|
|
594
|
+
assert.equal(t.claimedBy, null)
|
|
595
|
+
// ===== 下一轮 cycle:noResume 关掉续跑资格 → fresh spawn 新 Worker(单 run 落闸)=====
|
|
596
|
+
await env.clock.advance(50)
|
|
597
|
+
await env.waitFor(function () { return env.continuables.length >= 2 }, '重派 fresh spawn 发生')
|
|
598
|
+
var child2 = env.continuables[1].childId
|
|
599
|
+
assert.notEqual(child2, child1, '新 Worker 是新会话(旧子按 terminate 语义废弃)')
|
|
600
|
+
assert.equal(env.sendMessages.length, 1, '续跑只有被弃权的那一次(noResume 挡住二次复活)')
|
|
601
|
+
t = await env.waitFor(function () { var x = env.task('task-k1'); return x && x.status === 'in-progress' && x.claimedBy === child2 ? x : null }, '新 Worker 落卡 in-progress')
|
|
602
|
+
assert.equal(t.runs.length, 2, '首轮 + 新 Worker 各一条留档(双 run 不再现:任一时刻最多一条在跑)')
|
|
603
|
+
assert.ok(!t.runs[1].resume, '第二条是新 spawn 留档(非续跑记录)')
|
|
604
|
+
// 新 Worker 正常完成 → 全链单 run 收口
|
|
605
|
+
var rep = await env.tools.get('board_report').execute({ taskId: 'task-k1', kind: 'complete', summary: '重跑交付', changes: '改了 k', selfTest: 'npm test 全绿', diffStat: '' })
|
|
606
|
+
assert.equal(rep.ok, true)
|
|
607
|
+
t = await env.waitFor(function () { var x = env.task('task-k1'); return x && x.status === 'resolved' && x.runs[1].outcome === 'completed' ? x : null }, 'work 档直落 resolved 且 run 关账')
|
|
608
|
+
} finally { await env.cleanup() }
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// ===== 场景 l:terminate 竞态根修②(正常 resume 不回归)=====
|
|
612
|
+
// 同样的「失败回 pending → 重派续跑」主线,但无 terminate 落窗:闸门必须以锁内新鲜快照放行——
|
|
613
|
+
// sendMessage 冷复活照常落闸(rec 注册 + 续跑留档 resume:true),续跑轮可正常交付收口。
|
|
614
|
+
test('集成l·terminate竞态②:正常续跑不回归——无 terminate 时闸门放行,冷复活照常落闸并交付', async () => {
|
|
615
|
+
var env = createMockCtx()
|
|
616
|
+
try {
|
|
617
|
+
plugin.apply(env.ctx)
|
|
618
|
+
await createTask(env, 'task-l1', { pipeline: 'work' })
|
|
619
|
+
await env.clock.advance(50)
|
|
620
|
+
await env.waitFor(function () { return env.continuables.length >= 1 }, '首轮 worker spawn 发生')
|
|
621
|
+
var child1 = env.continuables[0].childId
|
|
622
|
+
await env.waitFor(function () { var x = env.task('task-l1'); return x && x.status === 'in-progress' && x.claimedBy === child1 ? x : null }, '落卡 in-progress')
|
|
623
|
+
// 首轮失败 → pending + incomplete(有续跑资格)。
|
|
624
|
+
// 注意:失败结算本身就会 interrupt 一次(卡2 设计:打断当前 turn、留存会话供续跑)——
|
|
625
|
+
// 闸门不误伤的判据是「续跑全程不再新增 interrupt」,先记下基数。
|
|
626
|
+
env.emitStatus(child1, 'running')
|
|
627
|
+
env.emitStatus(child1, 'idle')
|
|
628
|
+
await env.waitFor(function () { var x = env.task('task-l1'); return x && x.status === 'pending' && x.runs && x.runs[0] && x.runs[0].outcome === 'incomplete' ? x : null }, '失败回 pending')
|
|
629
|
+
var interruptBase = env.interrupts.length
|
|
630
|
+
assert.equal(interruptBase, 1, '失败结算的 interrupt 留存(卡2 既有语义)')
|
|
631
|
+
// 正常重派:无 terminate → 续跑闸门放行(锁内快照:in-progress + 占位 + resumeTarget 指向原子会话)
|
|
632
|
+
await env.clock.advance(50)
|
|
633
|
+
await env.waitFor(function () { return env.sendMessages.length >= 1 }, '续跑 sendMessage 发出')
|
|
634
|
+
assert.equal(env.sendMessages[0].childId, child1, '冷复活目标是首轮 Worker 子会话')
|
|
635
|
+
assert.equal(env.continuables.length, 1, '续跑不新建子会话')
|
|
636
|
+
var t = await env.waitFor(function () { var x = env.task('task-l1'); return x && x.status === 'in-progress' && x.claimedBy === child1 && x.runs && x.runs.length === 2 ? x : null }, '续跑落闸:认领位回原子会话 + 续跑留档')
|
|
637
|
+
assert.equal(t.runs[1].resume, true, '第二条是续跑记录')
|
|
638
|
+
assert.equal(t.runs[1].outcome, 'running')
|
|
639
|
+
assert.ok(!t.runs[0].noResume, '正常路径不留 noResume 旗(续跑资格不受损)')
|
|
640
|
+
// 续跑轮正常交付(上报通道)→ resolved + run 关账
|
|
641
|
+
var rep = await env.tools.get('board_report').execute({ taskId: 'task-l1', kind: 'complete', summary: '续跑交付', changes: '改了 l', selfTest: 'npm test 全绿', diffStat: '' })
|
|
642
|
+
assert.equal(rep.ok, true)
|
|
643
|
+
t = await env.waitFor(function () { var x = env.task('task-l1'); return x && x.status === 'resolved' && x.runs[1].outcome === 'completed' ? x : null }, '续跑轮交付落 resolved 且关账')
|
|
644
|
+
assert.equal(env.interrupts.length, interruptBase, '正常续跑路径零新增 interrupt(闸门不误伤)')
|
|
645
|
+
} finally { await env.cleanup() }
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
// ===== 场景 m:terminate 竞态根修③(fresh spawn 臂)=====
|
|
649
|
+
// 首派 spawn 在途(startContinuable 挂起)被 terminate:修复前 spawn 返回后照旧落闸(rec+留档),
|
|
650
|
+
// 与 terminate 后的重派轮形成双 run;修复后落闸弃权——新建子会话立刻 interrupt、不落档不注册,
|
|
651
|
+
// 下一轮 cycle 重新 fresh spawn,全程单 run。
|
|
652
|
+
test('集成m·terminate竞态③:fresh spawn 在途被 terminate → 落闸弃权(interrupt 新建子会话),下一轮单 run 收口', async () => {
|
|
653
|
+
var env = createMockCtx()
|
|
654
|
+
try {
|
|
655
|
+
plugin.apply(env.ctx)
|
|
656
|
+
// 首单 startContinuable 挂起:spawn 在途窗口由测试掌控;放行后后续 spawn 直通
|
|
657
|
+
var origStartC = env.ctx.subagents.startContinuable
|
|
658
|
+
var spawnHold = null
|
|
659
|
+
var releaseSpawn = null
|
|
660
|
+
env.ctx.subagents.startContinuable = function (o) {
|
|
661
|
+
if (!spawnHold) return origStartC(o)
|
|
662
|
+
return spawnHold.then(function () { return origStartC(o) })
|
|
663
|
+
}
|
|
664
|
+
spawnHold = new Promise(function (res) { releaseSpawn = res })
|
|
665
|
+
await createTask(env, 'task-m1', { pipeline: 'work' })
|
|
666
|
+
await env.clock.advance(50)
|
|
667
|
+
// 占位已 claim、spawn 在途挂起(startContinuable 未返回)
|
|
668
|
+
await env.waitFor(function () { var x = env.task('task-m1'); return x && x.status === 'in-progress' && x.claimedBy === 'spawn-pending' ? x : null }, '占位 claim 落卡,spawn 在途')
|
|
669
|
+
// terminate 落进在途窗口(竞态前提:活跃表无 rec)
|
|
670
|
+
var term = await env.rpc('terminate-agent', { sessionId: env.sid, taskId: 'task-m1' })
|
|
671
|
+
assert.equal(term.ok, true)
|
|
672
|
+
assert.equal(term.terminated, 'no-active-run', '竞态前提成立:terminate 看不到在途 spawn')
|
|
673
|
+
var t = env.task('task-m1')
|
|
674
|
+
assert.equal(t.status, 'pending', 'terminate 把卡翻回 pending')
|
|
675
|
+
assert.equal(t.claimedBy, null)
|
|
676
|
+
// 放行 spawn → 落闸重验:卡面已翻走 → 弃权(新建子会话立刻 interrupt,不注册不落档)
|
|
677
|
+
spawnHold = null
|
|
678
|
+
releaseSpawn()
|
|
679
|
+
await env.waitFor(function () { return env.continuables.length >= 1 && env.interrupts.some(function (x) { return x.childId === env.continuables[0].childId }) }, '弃权即 interrupt 刚建子会话')
|
|
680
|
+
t = env.task('task-m1')
|
|
681
|
+
assert.equal(t.status, 'pending')
|
|
682
|
+
assert.equal(t.claimedBy, null)
|
|
683
|
+
assert.ok(!t.runs || t.runs.length === 0, '弃权不落 run 留档(本次派发在卡面从未存在过)')
|
|
684
|
+
// 下一轮 cycle:正常 fresh spawn(闸门放行)→ 单 run 落卡
|
|
685
|
+
await env.clock.advance(50)
|
|
686
|
+
await env.waitFor(function () { return env.continuables.length >= 2 }, '重派 fresh spawn 发生')
|
|
687
|
+
var child2 = env.continuables[1].childId
|
|
688
|
+
assert.notEqual(child2, env.continuables[0].childId)
|
|
689
|
+
t = await env.waitFor(function () { var x = env.task('task-m1'); return x && x.status === 'in-progress' && x.claimedBy === child2 ? x : null }, '新 Worker 落卡 in-progress')
|
|
690
|
+
assert.equal(t.runs.length, 1, '只有一条真实落闸的留档(被弃权的首派零痕迹)')
|
|
691
|
+
// 正常完成 → resolved(单 run 收口)
|
|
692
|
+
var rep = await env.tools.get('board_report').execute({ taskId: 'task-m1', kind: 'complete', summary: '重跑交付', changes: '改了 m', selfTest: 'npm test 全绿', diffStat: '' })
|
|
693
|
+
assert.equal(rep.ok, true)
|
|
694
|
+
t = await env.waitFor(function () { var x = env.task('task-m1'); return x && x.status === 'resolved' && x.runs[0].outcome === 'completed' ? x : null }, 'resolved 收口且 run 关账')
|
|
695
|
+
} finally { await env.cleanup() }
|
|
696
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// dsh-agent-board — 记分卡卡3 client 模型表现区单测(test/scoreboard-client.test.mjs,task-muxhtgi2)
|
|
2
2
|
// client 无渲染 harness(react 不在包依赖,渲染由 dsh web 客户端运行时注入)——与历卡同一纪律:
|
|
3
3
|
// 全部走源码级断言(dashboard.js 模块源 + client.js 重组装产物 + README 双份同步)。
|
|
4
|
-
// ① 表格渲染——ModelPerf 组件 + 区根锚 id +
|
|
4
|
+
// ① 表格渲染——ModelPerf 组件 + 区根锚 id + 八列列头逐字(各带 title 口径说明)+ 行键=模型×角色×规模段
|
|
5
5
|
// ② 筛选器本地过滤——角色/规模段两组 chip + buckets 本地 filter;组件体内无 rpc(/fetchTasks((不重拉)
|
|
6
6
|
// ③ 样本不足灰显——insufficient 行 opacity 灰显 + title 前缀「样本 <5,仅供参考」
|
|
7
7
|
// ④ 空态——无数据「暂无足够 run 数据」(锚 id 照挂);筛选空「当前筛选无匹配桶」
|
|
@@ -24,14 +24,20 @@ function sliceFn(s, name) {
|
|
|
24
24
|
return s.slice(i, j || undefined)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// ===== ①
|
|
28
|
-
test('卡3①:表格——组件/锚 id
|
|
27
|
+
// ===== ① 八指标表格渲染 =====
|
|
28
|
+
test('卡3①:表格——组件/锚 id/八列列头带 title 口径/失败率注脚/行键=模型×角色×规模段', () => {
|
|
29
29
|
assert.match(src, /function ModelPerf\(props\)/)
|
|
30
30
|
assert.match(src, /id: 'tskb-model-perf'/)
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
assert.
|
|
31
|
+
// 八列列名逐字:有效均值/耗时中位/驳回率/一次通过率/超时率/失败率/续跑率/缓存命中率
|
|
32
|
+
// (失败率 = task-mv73ym6i 补的第八列:跑崩落定 incomplete/error/failed 计入,terminated 人工终止不计入)
|
|
33
|
+
const labels = ['有效均值', '耗时中位', '驳回率', '一次通过率', '超时率', '失败率', '续跑率', '缓存命中率']
|
|
34
|
+
for (const lb of labels) assert.ok(src.indexOf("label: '" + lb + "'") >= 0, '缺八列列头:' + lb)
|
|
35
|
+
assert.equal(MP_COLS_count(src), 8, 'MP_COLS 必须恰好八列')
|
|
36
|
+
// 失败率列取值口径:读桶 failRate 字段,率类格式化(null → —,与既有率列同一 mpPct)
|
|
37
|
+
assert.match(src, /\{ k: 'failRate', label: '失败率', title: '失败率 = /)
|
|
38
|
+
assert.match(src, /fmt: function \(b\) \{ return mpPct\(b\.failRate\) \}/)
|
|
39
|
+
// 表下口径注脚含失败率定义(含 terminated 不计入口径)
|
|
40
|
+
assert.ok(src.indexOf('失败率 = 跑崩落定(outcome 为 incomplete/error/failed)的 run / 桶内 runs,terminated 人工终止不计入') >= 0, '表下口径注脚缺失败率定义')
|
|
35
41
|
// 表头列名带 title 口径说明(th 挂 c.title,cursor:help 提示可悬浮)
|
|
36
42
|
assert.match(src, /React\.createElement\('th', \{ key: c\.k, style: thR, title: c\.title \}, c\.label\)/)
|
|
37
43
|
// 行 = 模型×场景桶:行键 model|role|size;场景列 角色·规模段(小/中/大)
|
|
@@ -91,8 +97,8 @@ test('卡3⑥:接线——Dashboard 挂载在 TokenUsage 正下方;scoreboar
|
|
|
91
97
|
const iMp = src.indexOf("React.createElement(ModelPerf, { sb: (state.usageSummary && state.usageSummary.scoreboard) || null })")
|
|
92
98
|
assert.ok(iTok >= 0, 'TokenUsage 挂载点必须存在')
|
|
93
99
|
assert.ok(iMp > iTok, 'ModelPerf 必须挂在 TokenUsage 正下方')
|
|
94
|
-
// 产物重组装:模块源关键标记逐一出现在 lib/client.js
|
|
95
|
-
for (const m of ['function ModelPerf(props)', 'tskb-model-perf', '样本 <5,仅供参考', '暂无足够 run 数据', '当前筛选无匹配桶', '→ 模型表现', 'var MP_COLS = [']) {
|
|
100
|
+
// 产物重组装:模块源关键标记逐一出现在 lib/client.js(含失败率列——改完源必须 npm run build-client)
|
|
101
|
+
for (const m of ['function ModelPerf(props)', 'tskb-model-perf', '样本 <5,仅供参考', '暂无足够 run 数据', '当前筛选无匹配桶', '→ 模型表现', 'var MP_COLS = [', "label: '失败率'"]) {
|
|
96
102
|
assert.ok(built.indexOf(m) >= 0, 'client.js 缺标记:' + m + '(需 npm run build-client 重组装)')
|
|
97
103
|
}
|
|
98
104
|
// kernel state 默认值(筛选档位跨渲染保持,与 rfOpen 同例)
|
package/test/scoreboard.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// dsh-agent-board — 记分卡卡2 host 聚合底座单测(test/scoreboard.test.mjs,task-muxhtgh9)
|
|
2
2
|
// 覆盖(对应任务断言①~⑥ + 接线):
|
|
3
|
-
// ①
|
|
4
|
-
//
|
|
3
|
+
// ① 八指标各一条数值正确性——模型×角色×规模段桶:有效token(均值+总额)/耗时(中位+P90)/驳回率/
|
|
4
|
+
// 一次通过率/超时率/失败率/续跑率/缓存命中率(失败率 = task-mv73ym6i 补的第八维度)
|
|
5
5
|
// ② 样本不足标记——桶内 runs<5 标 insufficient:true;恰好 5 条不标
|
|
6
6
|
// ③ 趋势 rollup byDay 正确——一次通过率 byDay / 卡时长四桶 / 超时率走势 byDay / 续跑成功率
|
|
7
7
|
// ④ 增量缓存命中零重算——(taskCount, 最新落定时刻) 不变时改字段值结果逐字不变;新 run 落地即重算
|
|
@@ -61,8 +61,8 @@ function bucketOf(sb, model, role, size) {
|
|
|
61
61
|
return b.length ? b[0] : null
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// ===== ①
|
|
65
|
-
test('卡2
|
|
64
|
+
// ===== ① 八指标数值正确性 =====
|
|
65
|
+
test('卡2①:八指标——m/a×worker×small 桶逐条数值核对(有效token/耗时/驳回率/一次通过率/超时率/失败率/续跑率/缓存命中率)', () => {
|
|
66
66
|
const sb = buildScoreboard(mkTasks())
|
|
67
67
|
const b = bucketOf(sb, 'm/a', 'worker', 'small')
|
|
68
68
|
assert.ok(b, 'm/a|worker|small 桶必须存在')
|
|
@@ -88,6 +88,9 @@ test('卡2①:七指标——m/a×worker×small 桶逐条数值核对(有效
|
|
|
88
88
|
assert.equal(b.resumeRate, 1 / 3)
|
|
89
89
|
// ⑦ 缓存命中率:cr 1000+3000+0=4000 / (inp 400 + cr 4000) = 4000/4400
|
|
90
90
|
assert.ok(Math.abs(b.cacheHitRate - 4000 / 4400) < 1e-12)
|
|
91
|
+
// ⑧ 失败率(task-mv73ym6i):夹具无跑崩 run → failed=0、failRate=0(率类分母=runs,与超时率同形态不 null)
|
|
92
|
+
assert.equal(b.failed, 0)
|
|
93
|
+
assert.equal(b.failRate, 0)
|
|
91
94
|
assert.equal(b.insufficient, true) // 3 < 5 → 样本护栏
|
|
92
95
|
// verifier 桶同轮核对(角色分桶 + 一次通过率按桶归属:tA 在 m/v 桶也是 firstPass)
|
|
93
96
|
const bv = bucketOf(sb, 'm/v', 'verifier', 'small')
|
|
@@ -99,6 +102,8 @@ test('卡2①:七指标——m/a×worker×small 桶逐条数值核对(有效
|
|
|
99
102
|
assert.equal(bv.rejected, 0)
|
|
100
103
|
assert.equal(bv.firstPassRate, 1)
|
|
101
104
|
assert.equal(bv.timeoutRate, 0)
|
|
105
|
+
assert.equal(bv.failed, 0)
|
|
106
|
+
assert.equal(bv.failRate, 0)
|
|
102
107
|
assert.equal(bv.resumeRate, 0)
|
|
103
108
|
assert.ok(Math.abs(bv.cacheHitRate - 500 / 550) < 1e-12)
|
|
104
109
|
assert.equal(bv.insufficient, true)
|
|
@@ -269,3 +274,38 @@ test('卡2⑦:接线——get-tasks 挂载 scoreboard / 缓存并列在 state
|
|
|
269
274
|
// 范围裁剪复用同一 dayInRange/rangeOn(与 usageSummary 同一 range 口径的硬保证)
|
|
270
275
|
assert.match(usage, /dayInRange\(q\.day, range\)/)
|
|
271
276
|
})
|
|
277
|
+
|
|
278
|
+
// ===== ⑧ 失败率维度(task-mv73ym6i):跑崩落定计入 failed,terminated 人工终止不计入 =====
|
|
279
|
+
// 实证背景 [[n-mv5ohczmaigu]]:flash 验收员两天三崩(incomplete 落定)桶面却 0 驳回 0 超时——
|
|
280
|
+
// 跑崩 run 在桶里完全隐形。本测试钉死:incomplete/error/failed 计入 failed;
|
|
281
|
+
// terminated(人工动作)/timeout-error(走超时率维度)/stale-closed(老卡残留关账)/completed 不计入。
|
|
282
|
+
test('卡2⑧:失败率——incomplete/error/failed 计入 failed;terminated/timeout-error/stale-closed/completed 不计入;failRate=failed/runs', () => {
|
|
283
|
+
const tasks = [{
|
|
284
|
+
id: 'tF', title: 'F', status: 'resolved', createdAt: isoOf(2026, 10, 6, 9), verifiedAt: isoOf(2026, 10, 6, 16),
|
|
285
|
+
usage: { input: 700, output: 350, cacheWrite: 70, total: 1120 }, // 有效 1120 → small
|
|
286
|
+
runs: [
|
|
287
|
+
{ role: 'worker', id: 'f1', at: isoOf(2026, 10, 6, 10), endedAt: isoOf(2026, 10, 6, 10, 5), model: 'm/f', outcome: 'completed', usage: U(100, 50, 1000, 10) },
|
|
288
|
+
{ role: 'worker', id: 'f2', at: isoOf(2026, 10, 6, 11), endedAt: isoOf(2026, 10, 6, 11, 1), model: 'm/f', outcome: 'incomplete', usage: U(100, 50, 1000, 10) }, // 猝死跑崩(flash 实证形态)
|
|
289
|
+
{ role: 'worker', id: 'f3', at: isoOf(2026, 10, 6, 12), endedAt: isoOf(2026, 10, 6, 12, 1), model: 'm/f', outcome: 'error', usage: U(100, 50, 1000, 10) },
|
|
290
|
+
{ role: 'worker', id: 'f4', at: isoOf(2026, 10, 6, 13), endedAt: isoOf(2026, 10, 6, 13, 1), model: 'm/f', outcome: 'failed', usage: U(100, 50, 1000, 10) },
|
|
291
|
+
{ role: 'worker', id: 'f5', at: isoOf(2026, 10, 6, 14), endedAt: isoOf(2026, 10, 6, 14, 1), model: 'm/f', outcome: 'terminated', usage: U(100, 50, 1000, 10) }, // 人工终止:不算失败
|
|
292
|
+
{ role: 'worker', id: 'f6', at: isoOf(2026, 10, 6, 14, 30), endedAt: isoOf(2026, 10, 6, 14, 31), model: 'm/f', outcome: 'timeout/error', usage: U(100, 50, 1000, 10) }, // 超时走超时率维度,不重复计
|
|
293
|
+
{ role: 'worker', id: 'f7', at: isoOf(2026, 10, 6, 15), endedAt: isoOf(2026, 10, 6, 15, 1), model: 'm/f', outcome: 'stale-closed', usage: U(100, 50, 1000, 10) }, // 老卡残留关账:非跑崩
|
|
294
|
+
{ role: 'worker', id: 'f8', at: isoOf(2026, 10, 6, 15, 30), model: 'm/f', outcome: 'running' }, // 在飞:不进任何指标
|
|
295
|
+
],
|
|
296
|
+
}]
|
|
297
|
+
const sb = buildScoreboard(tasks)
|
|
298
|
+
const b = bucketOf(sb, 'm/f', 'worker', 'small')
|
|
299
|
+
assert.ok(b, 'm/f|worker|small 桶必须存在')
|
|
300
|
+
assert.equal(sb.meta.runsSettled, 7) // 在飞的 f8 不进归一化记录
|
|
301
|
+
assert.equal(b.runs, 7)
|
|
302
|
+
assert.equal(b.failed, 3) // f2/f3/f4 = incomplete/error/failed
|
|
303
|
+
assert.equal(b.failRate, 3 / 7) // terminated(f5)/timeout-error(f6)/stale-closed(f7)/completed(f1) 不计入分子
|
|
304
|
+
assert.equal(b.timeouts, 1) // f6 只走超时维度
|
|
305
|
+
assert.equal(b.timeoutRate, 1 / 7)
|
|
306
|
+
// 全成功桶:failed=0、failRate=0(率类分母=runs,与超时率同一形态——0 而非 null)
|
|
307
|
+
const ok = [{ id: 'tG', status: 'resolved', createdAt: isoOf(2026, 10, 6, 9), verifiedAt: isoOf(2026, 10, 6, 11), usage: { input: 10, output: 10, cacheWrite: 0, total: 20 }, runs: [{ role: 'worker', id: 'g1', at: isoOf(2026, 10, 6, 10), endedAt: isoOf(2026, 10, 6, 10, 1), model: 'm/g', outcome: 'completed', usage: U(10, 10, 0, 0) }] }]
|
|
308
|
+
const bg = bucketOf(buildScoreboard(ok), 'm/g', 'worker', 'small')
|
|
309
|
+
assert.equal(bg.failed, 0)
|
|
310
|
+
assert.equal(bg.failRate, 0)
|
|
311
|
+
})
|