@mzzsfy/dsh-usage-dash 0.3.0 → 0.4.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.
@@ -81,7 +81,9 @@ function fakeDomain({ failUpdate = () => null } = {}) {
81
81
  }
82
82
  }
83
83
 
84
- const facilityOf = (domain) => ({ open: async () => domain })
84
+ const facilityOf = (domain, backupDomain) => ({
85
+ open: async (definition) => (definition?.name === 'usage_stats_backup' ? (backupDomain ?? domain) : domain),
86
+ })
85
87
 
86
88
  const tokenSample = (time, extra = {}) => ({
87
89
  time,
@@ -437,6 +439,61 @@ test('启动时游标非空则既有行保留', async () => {
437
439
  assert.equal(domain.rows.size, 1)
438
440
  })
439
441
 
442
+ test('reset 前自动快照,restore 整体回退到快照态', async () => {
443
+ const domain = fakeDomain()
444
+ const backup = fakeDomain()
445
+ const store = new UsageStore(facilityOf(domain, backup))
446
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
447
+ await store.markSeenSessions(['s1', 's2'])
448
+ assert.equal(store.backupInfo().available, false)
449
+ await store.reset()
450
+ assert.equal(domain.rows.size, 0)
451
+ assert.deepEqual([...(await store.seenSessions())], [])
452
+ // 快照在清空前生成:行与游标都可回退
453
+ const info = store.backupInfo()
454
+ assert.equal(info.available, true)
455
+ assert.equal(info.rows, 3)
456
+ assert.equal(info.sessions, 2)
457
+ const restored = await store.restoreFromBackup()
458
+ assert.equal(restored.rows, 3)
459
+ assert.equal(domain.rows.size, 3)
460
+ assert.deepEqual([...(await store.seenSessions())].sort(), ['s1', 's2'])
461
+ })
462
+
463
+ test('restore 前先快照当前态:回退动作自身可再回退', async () => {
464
+ const domain = fakeDomain()
465
+ const backup = fakeDomain()
466
+ const store = new UsageStore(facilityOf(domain, backup))
467
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
468
+ await store.reset()
469
+ await store.record(tokenSample(local(2026, 8, 3, 14, 0), { inputTokens: 99 }))
470
+ await store.flushNow()
471
+ // 第一轮回退到重建前(有 8/2 数据)
472
+ await store.restoreFromBackup()
473
+ const dayRows = await store.rangeRows('D', '2026-08-02', '2026-08-02')
474
+ assert.equal(dayRows.length, 1)
475
+ // 第二次回退回到"重建后"(8/3 数据),回退链不断
476
+ await store.restoreFromBackup()
477
+ const after = await store.rangeRows('D', '2026-08-03', '2026-08-03')
478
+ assert.equal(after.length, 1)
479
+ })
480
+
481
+ test('备份域不可用:重建照常,backupInfo 标记不可用,restore 报错', async () => {
482
+ const domain = fakeDomain()
483
+ const facility = {
484
+ open: async (definition) => {
485
+ if (definition?.name === 'usage_stats_backup') throw new Error('backup domain unavailable')
486
+ return domain
487
+ },
488
+ }
489
+ const store = new UsageStore(facility)
490
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
491
+ await store.reset()
492
+ assert.equal(domain.rows.size, 0)
493
+ assert.equal(store.backupInfo().available, false)
494
+ await assert.rejects(store.restoreFromBackup(), /backup domain unavailable/)
495
+ })
496
+
440
497
  test('域打开失败进入降级:ready 可等待,操作按调用失败', async () => {
441
498
  const store = new UsageStore({ open: () => Promise.reject(new Error('domain already open')) })
442
499
  await store.readyPromise()
@@ -1,4 +1,4 @@
1
- // 注入点B(S15)纯函数层测试:select 矩阵/模型键回退/单轮行文本/title 口径/文案键/显隐 CSS 镜像
1
+ // 注入点B 纯函数层测试:messageId 反查矩阵/模型键回退/芯片文本/title 口径/文案键/注册与 CSS 镜像
2
2
  // client.js 为经典 script bundle(禁 import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
3
3
  import { test } from 'node:test'
4
4
  import assert from 'node:assert/strict'
@@ -10,14 +10,13 @@ const {
10
10
  MESSAGES_EN,
11
11
  MESSAGES_ZH,
12
12
  MODEL_UNROUTED,
13
- TURN_TAIL_DATA_KEY,
14
- TURN_TAIL_PRIORITY,
15
- buildTurnCostLine,
13
+ buildTurnCostChipText,
16
14
  createTranslator,
17
15
  matchPrice,
18
- selectTurnTokenUsage,
16
+ turnCostAmountOf,
19
17
  turnCostTitleText,
20
18
  turnModelOf,
19
+ turnTokenUsageOfMessage,
21
20
  } = core
22
21
 
23
22
  const zhT = createTranslator(MESSAGES_ZH)
@@ -26,32 +25,70 @@ const enT = createTranslator(MESSAGES_EN)
26
25
  // 本地时区固定时钟,与 client.test.mjs 同一刻
27
26
  const NOW = new Date(2026, 2, 15, 14, 30, 45)
28
27
 
29
- test('chain 常量锁定:数据键与尝试顺序后试于 deliverables', () => {
30
- assert.equal(TURN_TAIL_DATA_KEY, 'turn-tail')
31
- assert.equal(TURN_TAIL_PRIORITY, 1)
28
+ const tailNode = (messageId, tokenUsage) => ({
29
+ kind: 'turn-tail',
30
+ data: { turn: 1, closing: { finalNode: { seq: 9, messageId } }, tokenUsage },
32
31
  })
33
32
 
34
- test('select Turn 位置数据返回 tokenUsage 本体', () => {
33
+ test('反查:matchId 命中返回 tokenUsage 本体', () => {
35
34
  const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
36
- const owner = { turn: { data: new Map([[TURN_TAIL_DATA_KEY, { tokenUsage }]]) } }
37
- assert.equal(selectTurnTokenUsage(owner), tokenUsage)
35
+ assert.equal(turnTokenUsageOfMessage([tailNode('m-1', tokenUsage)], 'm-1'), tokenUsage)
38
36
  })
39
37
 
40
- test('select 数据键缺席或 tokenUsage 缺失返回 null', () => {
41
- assert.equal(selectTurnTokenUsage({ turn: { data: new Map([[TURN_TAIL_DATA_KEY, {}]]) } }), null)
42
- assert.equal(selectTurnTokenUsage({ turn: { data: new Map() } }), null)
38
+ test('反查:多节点取命中者', () => {
39
+ const tokenUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
40
+ const nodes = [tailNode('m-0', { uncachedInputTokens: 9, outputTokens: 9, totalTokens: 18 }), tailNode('m-1', tokenUsage)]
41
+ assert.equal(turnTokenUsageOfMessage(nodes, 'm-1'), tokenUsage)
43
42
  })
44
43
 
45
- test('select owner 形状残缺全部 null 不抛', () => {
46
- assert.equal(selectTurnTokenUsage({ turn: { data: {} } }), null)
47
- assert.equal(selectTurnTokenUsage({ turn: {} }), null)
48
- assert.equal(selectTurnTokenUsage({}), null)
49
- assert.equal(selectTurnTokenUsage(null), null)
50
- assert.equal(selectTurnTokenUsage(undefined), null)
44
+ test('反查:非命中 kind messageId 不误收', () => {
45
+ const assistant = { kind: 'assistant', messageId: 'm-1', data: { closing: { finalNode: { messageId: 'm-1' } }, tokenUsage: {} } }
46
+ assert.equal(turnTokenUsageOfMessage([assistant], 'm-1'), null)
47
+ assert.equal(turnTokenUsageOfMessage([tailNode('m-0', {})], 'm-1'), null)
51
48
  })
52
49
 
53
- test('计价模型键取 routes 首个,缺失回退全通配键', () => {
54
- assert.equal(turnModelOf({ routes: [{ provider: 'p', model: 'p/m' }] }), 'p/m')
50
+ test('反查:tokenUsage 缺失返回 null', () => {
51
+ assert.equal(turnTokenUsageOfMessage([{ kind: 'turn-tail', data: { closing: { finalNode: { messageId: 'm-1' } } } }], 'm-1'), null)
52
+ assert.equal(turnTokenUsageOfMessage([tailNode('m-1', null)], 'm-1'), null)
53
+ })
54
+
55
+ test('反查:closing.usage 采样回退为聚合形态,可选桶带上', () => {
56
+ const sampled = { inputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 }
57
+ const node = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 1, messageId: 'm-1' }, usage: sampled } } }
58
+ assert.deepEqual(turnTokenUsageOfMessage([node], 'm-1'), { uncachedInputTokens: 100, outputTokens: 20, totalTokens: 130, cacheReadTokens: 10 })
59
+ const bare = { kind: 'turn-tail', data: { closing: { finalNode: { seq: 2, messageId: 'm-2' }, usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 } } } }
60
+ assert.deepEqual(turnTokenUsageOfMessage([bare], 'm-2'), { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 })
61
+ })
62
+
63
+ test('反查:非数组与空列表返回 null', () => {
64
+ assert.equal(turnTokenUsageOfMessage(null, 'm-1'), null)
65
+ assert.equal(turnTokenUsageOfMessage(undefined, 'm-1'), null)
66
+ assert.equal(turnTokenUsageOfMessage([], 'm-1'), null)
67
+ })
68
+
69
+ test('反查:chat 节点表 Map 形态与数组等价', () => {
70
+ const tokenUsage = { uncachedInputTokens: 10, outputTokens: 5, totalTokens: 15 }
71
+ const nodes = new Map([
72
+ ['k1', tailNode('m-0', { uncachedInputTokens: 9, outputTokens: 9, totalTokens: 18 })],
73
+ ['k2', tailNode('m-1', tokenUsage)],
74
+ ])
75
+ assert.equal(turnTokenUsageOfMessage(nodes, 'm-1'), tokenUsage)
76
+ assert.equal(turnTokenUsageOfMessage(new Map(), 'm-1'), null)
77
+ assert.equal(turnTokenUsageOfMessage({ size: 0 }, 'm-1'), null)
78
+ })
79
+
80
+ test('反查:单节点形状残缺跳过不抛,后续命中不受阻', () => {
81
+ const hostile = { get kind() { throw new Error('boom') } }
82
+ const tokenUsage = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
83
+ assert.equal(turnTokenUsageOfMessage([hostile, tailNode('m-1', tokenUsage)], 'm-1'), tokenUsage)
84
+ assert.equal(turnTokenUsageOfMessage([{ kind: 'turn-tail', get data() { throw new Error('boom') } }], 'm-1'), null)
85
+ assert.equal(turnTokenUsageOfMessage([null, 'x', 42], 'm-1'), null)
86
+ })
87
+
88
+ test('计价模型键:双全拼两段,仅 model 用裸名,双缺回退全通配键', () => {
89
+ assert.equal(turnModelOf({ routes: [{ provider: 'p', model: 'm' }] }), 'p/m')
90
+ assert.equal(turnModelOf({ routes: [{ model: 'm' }] }), 'm')
91
+ assert.equal(turnModelOf({ routes: [{ provider: 'p' }] }), MODEL_UNROUTED)
55
92
  assert.equal(turnModelOf({ routes: [] }), MODEL_UNROUTED)
56
93
  assert.equal(turnModelOf({}), MODEL_UNROUTED)
57
94
  assert.equal(turnModelOf(null), MODEL_UNROUTED)
@@ -67,46 +104,63 @@ test('routes 缺失模型键命中全通配规则', () => {
67
104
  assert.deepEqual(matchPrice(rules, turnModelOf(tokenUsage), NOW), { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 })
68
105
  })
69
106
 
70
- test('单轮行四桶全带:摘要含 prompt 三桶费用按价计', () => {
107
+ test('芯片文本四桶全带:prompt 三桶入费用分母按价计', () => {
71
108
  const tokenUsage = {
72
109
  uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 10000,
73
- cacheReadTokens: 1000, cacheWriteTokens: 0, routes: [{ provider: 'p', model: 'p/m' }],
110
+ cacheReadTokens: 1000, cacheWriteTokens: 0, routes: [{ provider: 'p', model: 'm' }],
74
111
  }
75
112
  const price = { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 }
76
- assert.equal(buildTurnCostLine(zhT, tokenUsage, price, '¥'), '输入 9K tok · 输出 1K tok · 费用 ≈ ¥0.01')
113
+ assert.equal(buildTurnCostChipText(zhT, tokenUsage, price, '¥'), '¥0.01')
77
114
  })
78
115
 
79
- test('单轮行可选桶缺失按 0 计入摘要与费用', () => {
116
+ test('芯片文本可选桶缺失按 0 计入费用', () => {
80
117
  const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
81
118
  const price = { input: 1, output: 2, cacheRead: 5, cacheWrite: 5 }
82
- assert.equal(buildTurnCostLine(zhT, tokenUsage, price, '¥'), '输入 8K tok · 输出 1K tok · 费用 ≈ ¥0.01')
119
+ assert.equal(buildTurnCostChipText(zhT, tokenUsage, price, '¥'), '¥0.01')
120
+ })
121
+
122
+ test('计费额 0 元为 null,正额保留', () => {
123
+ const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
124
+ assert.equal(turnCostAmountOf(tokenUsage, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }), null)
125
+ assert.equal(turnCostAmountOf(tokenUsage, null), null)
126
+ assert.equal(turnCostAmountOf(tokenUsage, { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }), 0.008)
83
127
  })
84
128
 
85
- test('单轮行取不到价费用为占位符', () => {
129
+ test('芯片文本取不到价为占位符', () => {
86
130
  const tokenUsage = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000 }
87
- assert.equal(buildTurnCostLine(zhT, tokenUsage, null, ''), '输入 8K tok · 输出 1K tok · 费用 ≈ —')
88
- assert.equal(buildTurnCostLine(enT, tokenUsage, null, ''), 'Input 8K tok · Output 1K tok · Cost ≈ —')
131
+ assert.equal(buildTurnCostChipText(zhT, tokenUsage, null, ''), '—')
132
+ assert.equal(buildTurnCostChipText(enT, tokenUsage, null, ''), '—')
89
133
  })
90
134
 
91
- test('title 估算口径,可选桶未上报追加标注', () => {
92
- const full = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2, cacheReadTokens: 0, cacheWriteTokens: 0 }
93
- assert.equal(turnCostTitleText(zhT, full), '单轮用量按当前费率估算')
135
+ test('title 含 token 摘要与估算口径,可选桶未上报追加标注', () => {
136
+ const full = { uncachedInputTokens: 8000, outputTokens: 1000, totalTokens: 9000, cacheReadTokens: 0, cacheWriteTokens: 0 }
137
+ assert.equal(turnCostTitleText(zhT, full), '输入 8K tok · 输出 1K tok,单轮用量按当前费率估算')
94
138
  const missing = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
95
- assert.equal(turnCostTitleText(zhT, missing), '单轮用量按当前费率估算,该提供商未上报此桶')
96
- assert.equal(turnCostTitleText(enT, missing), 'Per-turn usage estimated at current rates,Not reported by this provider')
139
+ assert.equal(turnCostTitleText(zhT, missing), '输入 1 tok · 输出 1 tok,单轮用量按当前费率估算,该提供商未上报此桶')
140
+ assert.equal(turnCostTitleText(enT, missing), 'Input 1 tok · Output 1 tok,Per-turn usage estimated at current rates,Not reported by this provider')
97
141
  })
98
142
 
99
- test('注入点B 文案键 zh/en 值锁定', () => {
100
- assert.equal(MESSAGES_ZH['stats.turnCost'], '{summary} · 费用 ≈ {cost}')
101
- assert.equal(MESSAGES_EN['stats.turnCost'], '{summary} · Cost ≈ {cost}')
143
+ test('注入点B 文案键 zh/en 值锁定,独立行旧键已删', () => {
144
+ assert.equal(MESSAGES_ZH['stats.cost'], '费用 ≈ {cost}')
145
+ assert.equal(MESSAGES_EN['stats.cost'], 'Cost ≈ {cost}')
146
+ assert.equal(MESSAGES_ZH['turnCostChip'], '{cost}')
147
+ assert.equal(MESSAGES_EN['turnCostChip'], '{cost}')
148
+ assert.equal(MESSAGES_ZH['stats.turnCost'], undefined)
149
+ assert.equal(MESSAGES_EN['stats.turnCost'], undefined)
102
150
  assert.equal(MESSAGES_ZH.turnCostTitle, '单轮用量按当前费率估算')
103
151
  assert.equal(MESSAGES_EN.turnCostTitle, 'Per-turn usage estimated at current rates')
104
152
  assert.equal(MESSAGES_ZH.turnTokensUnreported, '该提供商未上报此桶')
105
153
  assert.equal(MESSAGES_EN.turnTokensUnreported, 'Not reported by this provider')
154
+ assert.equal(MESSAGES_ZH.costDisplayDesc, '在信息栏、趋势悬浮与回合费用芯片中显示按当前费率估算的费用。')
155
+ assert.equal(MESSAGES_EN.costDisplayDesc, 'Show costs estimated at current rates in the stats line, trend tooltips and the turn cost chip.')
156
+ })
157
+
158
+ test('注册指向官方动作行槽,turnTail 旧路径移除', () => {
159
+ assert.ok(CLIENT_SOURCE.includes("ctx.slots.inject('conversation.chat.assistant-actions'"))
160
+ assert.ok(!CLIENT_SOURCE.includes('conversation.chat.turnTail'))
106
161
  })
107
162
 
108
- test('显隐 CSS 镜像官方动作行:历史悬停显现与焦点恢复', () => {
163
+ test('芯片样式:显隐随官方动作行父级,不自绘重复规则', () => {
109
164
  assert.ok(CLIENT_SOURCE.includes('.ud-turn-cost{'))
110
- assert.ok(CLIENT_SOURCE.includes('@media (hover:hover){[data-actions-reveal=hover] .ud-turn-cost{opacity:0'))
111
- assert.ok(CLIENT_SOURCE.includes('[data-actions-reveal=hover]:hover .ud-turn-cost,[data-actions-reveal=hover]:focus-within .ud-turn-cost{opacity:1}'))
165
+ assert.ok(!CLIENT_SOURCE.includes('[data-actions-reveal=hover] .ud-turn-cost'))
112
166
  })