@mzzsfy/dsh-usage-dash 0.1.0 → 0.3.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.
@@ -94,17 +94,17 @@ const tokenSample = (time, extra = {}) => ({
94
94
  })
95
95
 
96
96
  test('分钟保留天数默认常量与上限和计划一致', () => {
97
- assert.equal(DEFAULT_MINUTE_RETENTION_DAYS, 2)
98
- assert.equal(MINUTE_RETENTION_MAX_DAYS, 2)
97
+ assert.equal(DEFAULT_MINUTE_RETENTION_DAYS, 7)
98
+ assert.equal(MINUTE_RETENTION_MAX_DAYS, 7)
99
99
  assert.equal(HOUR_RETENTION_DAYS, 15)
100
100
  })
101
101
 
102
102
  test('分钟保留值归一:非法回落默认,超上限截断,0 保留', () => {
103
103
  assert.equal(clampMinuteRetentionDays(1), 1)
104
104
  assert.equal(clampMinuteRetentionDays(0), 0)
105
- assert.equal(clampMinuteRetentionDays(7), 2)
106
- assert.equal(clampMinuteRetentionDays(-1), 2)
107
- assert.equal(clampMinuteRetentionDays('x'), 2)
105
+ assert.equal(clampMinuteRetentionDays(30), 7)
106
+ assert.equal(clampMinuteRetentionDays(-1), 7)
107
+ assert.equal(clampMinuteRetentionDays('x'), 7)
108
108
  })
109
109
 
110
110
  test('桶键生成含补零,分钟桶对齐 10 分钟', () => {
@@ -134,6 +134,7 @@ test('token 样本同刻落三粒度三行,行内无 g 字段', async () => {
134
134
  const domain = fakeDomain()
135
135
  const store = new UsageStore(facilityOf(domain))
136
136
  await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
137
+ await store.flushNow()
137
138
  const keys = ['D|2026-08-02', 'H|2026-08-02T14', 'M|2026-08-02T14:30'].map(
138
139
  (prefix) => `${prefix}|deepseek|deepseek/deepseek-chat`,
139
140
  )
@@ -155,6 +156,7 @@ test('turn 样本落合成行:provider default 与 model (turns)', async () => {
155
156
  const domain = fakeDomain()
156
157
  const store = new UsageStore(facilityOf(domain))
157
158
  await store.record({ time: local(2026, 8, 2, 14, 37), turn: true })
159
+ await store.flushNow()
158
160
  const seen = domain.rows.get('D|2026-08-02|default|(turns)')
159
161
  assert.ok(seen)
160
162
  assert.equal(seen.turns, 1)
@@ -166,6 +168,7 @@ test('request 样本只计请求数,无归因落 (unknown)', async () => {
166
168
  const store = new UsageStore(facilityOf(domain))
167
169
  await store.record({ time: local(2026, 8, 2, 14, 37), request: true, model: 'deepseek-chat' })
168
170
  await store.record({ time: local(2026, 8, 2, 14, 38), request: true })
171
+ await store.flushNow()
169
172
  const labelled = domain.rows.get('D|2026-08-02|default|deepseek-chat')
170
173
  assert.ok(labelled)
171
174
  assert.equal(labelled.requests, 1)
@@ -175,27 +178,78 @@ test('request 样本只计请求数,无归因落 (unknown)', async () => {
175
178
  assert.equal(unknown.requests, 1)
176
179
  })
177
180
 
181
+ test('token 样本时长落三粒度累加,request 与 turn 行时长恒 0', async () => {
182
+ const domain = fakeDomain()
183
+ const store = new UsageStore(facilityOf(domain))
184
+ const at = local(2026, 8, 2, 14, 37)
185
+ await store.record(tokenSample(at, { durationMs: 6500 }))
186
+ await store.record(tokenSample(at + 1000, { durationMs: 2500 }))
187
+ await store.record({ time: at, request: true, model: 'deepseek/deepseek-chat' })
188
+ await store.record({ time: at, turn: true })
189
+ await store.flushNow()
190
+ const daily = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
191
+ assert.equal(daily.outputTokens, 40)
192
+ assert.equal(daily.durationMs, 9000)
193
+ const hourly = domain.rows.get('H|2026-08-02T14|deepseek|deepseek/deepseek-chat')
194
+ assert.equal(hourly.durationMs, 9000)
195
+ const minute = domain.rows.get('M|2026-08-02T14:30|deepseek|deepseek/deepseek-chat')
196
+ assert.equal(minute.durationMs, 9000)
197
+ const requestRow = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
198
+ assert.equal(requestRow.requests, 1)
199
+ const turnRow = domain.rows.get('D|2026-08-02|default|(turns)')
200
+ assert.equal(turnRow.durationMs, 0)
201
+ })
202
+
203
+ test('老行缺 durationMs 字段:增量求和不产 NaN', async () => {
204
+ const domain = fakeDomain()
205
+ const store = new UsageStore(facilityOf(domain))
206
+ const at = local(2026, 8, 2, 14, 37)
207
+ const key = 'D|2026-08-02|deepseek|deepseek/deepseek-chat'
208
+ await store.record(tokenSample(at))
209
+ await store.flushNow()
210
+ // 模拟存量旧格式行:回写去掉 durationMs 字段
211
+ const stored = domain.rows.get(key)
212
+ delete stored.durationMs
213
+ await store.record(tokenSample(at + 1000, { durationMs: 3000 }))
214
+ await store.flushNow()
215
+ const seen = domain.rows.get(key)
216
+ assert.equal(seen.durationMs, 3000)
217
+ assert.equal(seen.outputTokens, 40)
218
+ })
219
+
178
220
  test('missing-record 首写竞态:put 种子后重试写入真值', async () => {
179
221
  const domain = fakeDomain()
180
222
  const store = new UsageStore(facilityOf(domain))
181
223
  await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
224
+ await store.flushNow()
182
225
  assert.equal(domain.stats.puts, 3)
183
226
  const seen = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
184
227
  assert.equal(seen.inputTokens, 10)
185
228
  })
186
229
 
187
- test('allSettled 聚合:单粒度失败上抛且其余粒度不丢', async () => {
230
+ test('flush 中单粒度失败:错误经 onFlushError 上抛,失败行留 pending 重试,成功粒度落盘', async () => {
231
+ const failedKeys = new Set()
188
232
  const domain = fakeDomain({
189
- failUpdate: (key) => (key.startsWith('H|') ? new Error('simulated hour failure') : null),
233
+ failUpdate: (key) => {
234
+ if (!key.startsWith('H|') || failedKeys.has(key)) return null
235
+ failedKeys.add(key)
236
+ return new Error('simulated hour failure')
237
+ },
190
238
  })
191
239
  const store = new UsageStore(facilityOf(domain))
192
- await assert.rejects(
193
- store.record(tokenSample(local(2026, 8, 2, 14, 37))),
194
- (err) => err.name === 'AggregateError' && err.errors.length === 1,
195
- )
240
+ const errors = []
241
+ store.onFlushError = (err) => errors.push(err)
242
+ await store.readyPromise()
243
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
244
+ await store.flushNow()
196
245
  assert.ok(domain.rows.has('D|2026-08-02|deepseek|deepseek/deepseek-chat'))
197
246
  assert.ok(domain.rows.has('M|2026-08-02T14:30|deepseek|deepseek/deepseek-chat'))
198
247
  assert.equal(domain.rows.has('H|2026-08-02T14|deepseek|deepseek/deepseek-chat'), false)
248
+ assert.equal(errors.length, 1)
249
+ assert.equal(errors[0].name, 'AggregateError')
250
+ assert.equal(errors[0].errors.length, 1)
251
+ await store.flushNow()
252
+ assert.ok(domain.rows.has('H|2026-08-02T14|deepseek|deepseek/deepseek-chat'))
199
253
  })
200
254
 
201
255
  test('非 missing 错误不触发种子重试', async () => {
@@ -204,11 +258,15 @@ test('非 missing 错误不触发种子重试', async () => {
204
258
  failUpdate: (candidate) => (candidate === key ? new Error('backend write failed') : null),
205
259
  })
206
260
  const store = new UsageStore(facilityOf(domain))
207
- await assert.rejects(
208
- store.record(tokenSample(local(2026, 8, 2, 14, 37))),
209
- (err) => err.name === 'AggregateError' && /backend write failed/.test(err.errors[0].message),
210
- )
261
+ const errors = []
262
+ store.onFlushError = (err) => errors.push(err)
263
+ await store.readyPromise()
264
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
265
+ await store.flushNow()
211
266
  assert.equal(domain.rows.has(key), false)
267
+ assert.equal(errors.length, 1)
268
+ assert.match(errors[0].errors[0].message, /backend write failed/)
269
+ assert.equal(domain.stats.puts, 2)
212
270
  })
213
271
 
214
272
  test('rangeRows 按粒度前缀与桶串闭区间扫描', async () => {
@@ -256,16 +314,16 @@ test('保留窗口为 0 时分钟桶全部清理', async () => {
256
314
  assert.equal(domain.rows.has('D|2026-08-10|a|m'), true)
257
315
  })
258
316
 
259
- test('分钟保留超上限时 clamp 到 48h 窗口', async () => {
317
+ test('分钟保留超上限时 clamp 到 7 天窗口', async () => {
260
318
  const domain = fakeDomain()
261
319
  const store = new UsageStore(facilityOf(domain), { now: () => local(2026, 8, 10, 12, 0) })
262
320
  const table = domain.table()
263
- // clamp 后窗口起点 = 08-08T12:00,02 天前的桶一律清理,即使注入 30 天
264
- await table.put('M|2026-08-05T00:00|a|m', rowOf('2026-08-05T00:00', 'a', 'm'))
265
- await table.put('M|2026-08-08T12:00|a|m', rowOf('2026-08-08T12:00', 'a', 'm'))
321
+ // clamp 后窗口起点 = 08-03T12:00,7 天前的桶一律清理,即使注入 30 天
322
+ await table.put('M|2026-08-01T00:00|a|m', rowOf('2026-08-01T00:00', 'a', 'm'))
323
+ await table.put('M|2026-08-03T12:00|a|m', rowOf('2026-08-03T12:00', 'a', 'm'))
266
324
  await store.pruneMinutes(30)
267
- assert.equal(domain.rows.has('M|2026-08-05T00:00|a|m'), false)
268
- assert.equal(domain.rows.has('M|2026-08-08T12:00|a|m'), true)
325
+ assert.equal(domain.rows.has('M|2026-08-01T00:00|a|m'), false)
326
+ assert.equal(domain.rows.has('M|2026-08-03T12:00|a|m'), true)
269
327
  })
270
328
 
271
329
  test('pruneHours 固定 15 天窗口清理小时桶且不碰其他粒度', async () => {
@@ -283,19 +341,22 @@ test('pruneHours 固定 15 天窗口清理小时桶且不碰其他粒度', async
283
341
  assert.equal(domain.rows.has('M|2026-07-25T00:00|a|m'), true)
284
342
  })
285
343
 
286
- test('每日本地日首次写入触发清理,同日后续写入不重复触发', async () => {
344
+ test('每日本地日首次 flush 触发清理,同日后续 flush 不重复触发', async () => {
287
345
  let clock = local(2026, 8, 10, 12, 0)
288
346
  const domain = fakeDomain()
289
347
  const store = new UsageStore(facilityOf(domain), { now: () => clock, retentionDays: () => 0 })
290
348
  const table = domain.table()
291
349
  await table.put('M|2026-08-01T00:00|a|m', rowOf('2026-08-01T00:00', 'a', 'm'))
292
350
  await store.record(tokenSample(local(2026, 8, 10, 12, 0)))
351
+ await store.flushNow()
293
352
  assert.equal(domain.rows.has('M|2026-08-01T00:00|a|m'), false)
294
353
  await table.put('M|2026-08-01T00:00|a|m', rowOf('2026-08-01T00:00', 'a', 'm'))
295
354
  await store.record(tokenSample(local(2026, 8, 10, 12, 1)))
355
+ await store.flushNow()
296
356
  assert.equal(domain.rows.has('M|2026-08-01T00:00|a|m'), true)
297
357
  clock = local(2026, 8, 11, 0, 0)
298
358
  await store.record(tokenSample(local(2026, 8, 11, 0, 0)))
359
+ await store.flushNow()
299
360
  assert.equal(domain.rows.has('M|2026-08-01T00:00|a|m'), false)
300
361
  })
301
362
 
@@ -395,3 +456,65 @@ test('globalThis 单例复用同实例,重置缝生效', () => {
395
456
  assert.notEqual(sharedStore(facilityOf(fakeDomain())), first)
396
457
  __resetSharedStoreForTests()
397
458
  })
459
+
460
+ // —— 写合并(write-behind):record 同步入 pending,flush 周期落盘 ——
461
+
462
+ test('record 只入 pending 不产生持久化写,flushNow 后每脏行恰一次 update', async () => {
463
+ const domain = fakeDomain()
464
+ const store = new UsageStore(facilityOf(domain))
465
+ await store.readyPromise()
466
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
467
+ assert.equal(domain.rows.size, 0)
468
+ await store.flushNow()
469
+ assert.equal(domain.rows.size, 3)
470
+ const seen = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
471
+ assert.equal(seen.inputTokens, 10)
472
+ })
473
+
474
+ test('同桶多样本合并:pending 累加,一次 update 落全部计数', async () => {
475
+ const domain = fakeDomain()
476
+ const store = new UsageStore(facilityOf(domain))
477
+ await store.readyPromise()
478
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
479
+ await store.record(tokenSample(local(2026, 8, 2, 14, 38)))
480
+ await store.record(tokenSample(local(2026, 8, 2, 14, 39)))
481
+ await store.flushNow()
482
+ const seen = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
483
+ assert.equal(seen.inputTokens, 30)
484
+ assert.equal(domain.stats.puts, 3)
485
+ })
486
+
487
+ test('flush 周期自动落盘', async () => {
488
+ const domain = fakeDomain()
489
+ const store = new UsageStore(facilityOf(domain), { flushIntervalMs: 5 })
490
+ await store.readyPromise()
491
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
492
+ await new Promise((resolve) => setTimeout(resolve, 30))
493
+ assert.equal(domain.rows.size, 3)
494
+ })
495
+
496
+ test('rangeRows 前自动 flush,读己之写', async () => {
497
+ const domain = fakeDomain()
498
+ const store = new UsageStore(facilityOf(domain))
499
+ await store.readyPromise()
500
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
501
+ const rows = await store.rangeRows('D', '2026-08-02', '2026-08-02')
502
+ assert.equal(rows.length, 1)
503
+ assert.equal(rows[0].inputTokens, 10)
504
+ })
505
+
506
+ test('reset 前 pending 先落盘再清表', async () => {
507
+ const domain = fakeDomain()
508
+ const store = new UsageStore(facilityOf(domain))
509
+ await store.readyPromise()
510
+ await store.record(tokenSample(local(2026, 8, 2, 14, 37)))
511
+ await store.reset(new Map())
512
+ assert.equal(domain.rows.size, 0)
513
+ assert.equal(domain.stats.puts, 3)
514
+ })
515
+
516
+ test('降级态 record 仍按调用失败', async () => {
517
+ const store = new UsageStore({ open: () => Promise.reject(new Error('domain already open')) })
518
+ await store.readyPromise()
519
+ await assert.rejects(store.record(tokenSample(local(2026, 8, 2, 14, 37))), /usage store degraded/)
520
+ })
@@ -1,23 +1,15 @@
1
1
  // 注入点B(S15)纯函数层测试:select 矩阵/模型键回退/单轮行文本/title 口径/文案键/显隐 CSS 镜像
2
- // client.js 为非模块 script(bundle 求值形态,禁 import/export),整源求值后按顶层声明名收集
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'
5
- import { readFileSync } from 'node:fs'
6
- import { fileURLToPath } from 'node:url'
7
- import { dirname, join } from 'node:path'
8
-
9
- const CLIENT_SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'client.js'), 'utf8')
10
- const DECLARATION_NAMES = [
11
- ...new Set(
12
- [...CLIENT_SOURCE.matchAll(/^(?:const|function|let) ([A-Za-z_$][\w$]*)/gm)].map((match) => match[1])
13
- ),
14
- ]
15
- const core = new Function(`${CLIENT_SOURCE}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
5
+ import { CLIENT_SOURCE, CLIENT_BODY, DECLARATION_NAMES } from './client-eval.mjs'
6
+
7
+ const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
16
8
 
17
9
  const {
18
10
  MESSAGES_EN,
19
11
  MESSAGES_ZH,
20
- MODEL_WILDCARD,
12
+ MODEL_UNROUTED,
21
13
  TURN_TAIL_DATA_KEY,
22
14
  TURN_TAIL_PRIORITY,
23
15
  buildTurnCostLine,
@@ -58,20 +50,20 @@ test('select owner 形状残缺全部 null 不抛', () => {
58
50
  assert.equal(selectTurnTokenUsage(undefined), null)
59
51
  })
60
52
 
61
- test('计价模型键取 routes 首个,缺失回退通配', () => {
53
+ test('计价模型键取 routes 首个,缺失回退全通配键', () => {
62
54
  assert.equal(turnModelOf({ routes: [{ provider: 'p', model: 'p/m' }] }), 'p/m')
63
- assert.equal(turnModelOf({ routes: [] }), MODEL_WILDCARD)
64
- assert.equal(turnModelOf({}), MODEL_WILDCARD)
65
- assert.equal(turnModelOf(null), MODEL_WILDCARD)
55
+ assert.equal(turnModelOf({ routes: [] }), MODEL_UNROUTED)
56
+ assert.equal(turnModelOf({}), MODEL_UNROUTED)
57
+ assert.equal(turnModelOf(null), MODEL_UNROUTED)
66
58
  })
67
59
 
68
- test('routes 缺失模型键命中通配规则', () => {
60
+ test('routes 缺失模型键命中全通配规则', () => {
69
61
  const rules = [
70
62
  { model: 'p/m', currency: '¥', price: { input: 9, output: 9, cacheRead: 9, cacheWrite: 9 }, conditions: [] },
71
- { model: MODEL_WILDCARD, currency: '$', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
63
+ { model: '*/*', currency: '$', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
72
64
  ]
73
65
  const tokenUsage = { uncachedInputTokens: 1000, outputTokens: 0, totalTokens: 1000 }
74
- assert.equal(turnModelOf(tokenUsage), MODEL_WILDCARD)
66
+ assert.equal(turnModelOf(tokenUsage), MODEL_UNROUTED)
75
67
  assert.deepEqual(matchPrice(rules, turnModelOf(tokenUsage), NOW), { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 })
76
68
  })
77
69