@mzzsfy/dsh-usage-dash 0.3.0 → 0.5.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.
@@ -0,0 +1,104 @@
1
+ // direct-log-reader 测试:多帧 zstd 解压、JSONL 宽松解析、目录定位与 id 枚举。
2
+ // 全部经由 root 参数注入临时目录,不依赖真实 DSH_HOME
3
+
4
+ import test from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+ import { zstdCompressSync } from 'node:zlib'
10
+
11
+ import { decodeZstdFile, parseJsonlEvents, findSessionDir, listSessionIdsDirect, readSessionLogDirect } from '../src/direct-log-reader.js'
12
+
13
+ function tempRoot() {
14
+ return mkdtempSync(join(tmpdir(), 'ud-direct-'))
15
+ }
16
+
17
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
18
+
19
+ test('decodeZstdFile:多帧顺序解压拼接', () => {
20
+ const a = zstdCompressSync(Buffer.from('hello\n'))
21
+ const b = zstdCompressSync(Buffer.from('world\n'))
22
+ const bytes = Buffer.concat([MAGIC.slice(0, 3), a.subarray(3), MAGIC.slice(0, 3), b.subarray(3)])
23
+ assert.equal(decodeZstdFile(bytes), 'hello\nworld\n')
24
+ })
25
+
26
+ test('decodeZstdFile:无帧抛错', () => {
27
+ assert.throws(() => decodeZstdFile(Buffer.from('plain text')), /no zstd frame/)
28
+ })
29
+
30
+ test('parseJsonlEvents:header 行剔除,坏行跳过,事件保留', () => {
31
+ const text = [
32
+ JSON.stringify({ type: 'session/header', id: 's1', version: 1 }),
33
+ JSON.stringify({ type: 'assistant/message', seq: 1, data: { turn: 0, step: 0, usage: { inputTokens: 5 } } }),
34
+ 'not json',
35
+ '',
36
+ JSON.stringify({ type: 'request/context', seq: 2, data: { provider: 'p', model: 'm' } }),
37
+ ].join('\n')
38
+ const events = parseJsonlEvents(text)
39
+ assert.equal(events.length, 2)
40
+ assert.equal(events[0].type, 'assistant/message')
41
+ assert.equal(events[1].type, 'request/context')
42
+ })
43
+
44
+ test('findSessionDir:跨 cwd 桶定位 id 目录', () => {
45
+ const root = tempRoot()
46
+ try {
47
+ const dir = join(root, '--C-users-proj--', 'sid-1')
48
+ mkdirSync(dir, { recursive: true })
49
+ writeFileSync(join(dir, 'session.jsonl.zstd'), zstdCompressSync(Buffer.from('{}\n')))
50
+ assert.equal(findSessionDir('sid-1', root), dir)
51
+ assert.equal(findSessionDir('missing', root), undefined)
52
+ assert.equal(findSessionDir('sid-1', join(root, 'nope')), undefined)
53
+ } finally {
54
+ rmSync(root, { recursive: true, force: true })
55
+ }
56
+ })
57
+
58
+ test('listSessionIdsDirect:只收含日志文件的会话目录', () => {
59
+ const root = tempRoot()
60
+ try {
61
+ const withLog = join(root, 'bucket-a', 'id-1')
62
+ mkdirSync(withLog, { recursive: true })
63
+ writeFileSync(join(withLog, 'session.jsonl.zstd'), '')
64
+ const withV3 = join(root, 'bucket-a', 'id-2')
65
+ mkdirSync(withV3, { recursive: true })
66
+ writeFileSync(join(withV3, 'session.v3.jsonl.zstd'), '')
67
+ const empty = join(root, 'bucket-b', 'id-3')
68
+ mkdirSync(empty, { recursive: true })
69
+ assert.deepEqual(listSessionIdsDirect(root).sort(), ['id-1', 'id-2'])
70
+ } finally {
71
+ rmSync(root, { recursive: true, force: true })
72
+ }
73
+ })
74
+
75
+ test('readSessionLogDirect:压缩档案解出事件流', () => {
76
+ const root = tempRoot()
77
+ try {
78
+ const dir = join(root, 'bucket', 'sid-9')
79
+ mkdirSync(dir, { recursive: true })
80
+ const text = [
81
+ JSON.stringify({ type: 'session/header', id: 'sid-9' }),
82
+ JSON.stringify({ type: 'assistant/message', seq: 0, data: { turn: 0, step: 0, usage: { inputTokens: 7 } } }),
83
+ ].join('\n')
84
+ writeFileSync(join(dir, 'session.v3.jsonl.zstd'), zstdCompressSync(Buffer.from(text)))
85
+ const events = readSessionLogDirect('sid-9', root)
86
+ assert.equal(events.length, 1)
87
+ assert.equal(events[0].data.usage.inputTokens, 7)
88
+ } finally {
89
+ rmSync(root, { recursive: true, force: true })
90
+ }
91
+ })
92
+
93
+ test('readSessionLogDirect:明文 jsonl 档案同样可读', () => {
94
+ const root = tempRoot()
95
+ try {
96
+ const dir = join(root, 'bucket', 'sid-plain')
97
+ mkdirSync(dir, { recursive: true })
98
+ writeFileSync(join(dir, 'session.jsonl'), JSON.stringify({ type: 'turn/end', seq: 0 }))
99
+ const events = readSessionLogDirect('sid-plain', root)
100
+ assert.equal(events.length, 1)
101
+ } finally {
102
+ rmSync(root, { recursive: true, force: true })
103
+ }
104
+ })
@@ -15,6 +15,7 @@ import {
15
15
  CONDITION_KINDS as hostConditionKinds,
16
16
  TOKENS_PER_MILLION as hostTokensPerMillion,
17
17
  } from '../src/pricing.js'
18
+ import { refOf as hostRefOf } from '../src/collector.js'
18
19
 
19
20
  const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
20
21
 
@@ -27,6 +28,8 @@ const {
27
28
  CURRENCIES: clientCurrencies,
28
29
  CONDITION_KINDS: clientConditionKinds,
29
30
  TOKENS_PER_MILLION: clientTokensPerMillion,
31
+ MODEL_UNROUTED: clientModelUnrouted,
32
+ turnModelOf: clientTurnModelOf,
30
33
  } = core
31
34
 
32
35
  // 本地时区固定时刻(2026-03-15 为周日):同一 Date 实例喂双侧
@@ -36,32 +39,40 @@ const SUNDAY_0900 = [2026, 2, 15, 9, 0]
36
39
  const SUNDAY_2330 = [2026, 2, 15, 23, 30]
37
40
  const SUNDAY_0300 = [2026, 2, 15, 3, 0]
38
41
  const SUNDAY_1200 = [2026, 2, 15, 12, 0]
42
+ const MON_JAN01 = [2026, 0, 1, 12, 0]
39
43
  const MON_JAN15 = [2026, 0, 15, 12, 0]
40
44
  const MON_JAN16 = [2026, 0, 16, 12, 0]
41
45
  const MON_JAN26 = [2026, 0, 26, 12, 0]
46
+ const MON_JAN31 = [2026, 0, 31, 12, 0]
42
47
  const MON_JAN03 = [2026, 0, 3, 12, 0]
43
48
  const FEB_30_ROLLS_TO_MAR02 = [2026, 1, 30, 12, 0]
44
49
 
45
50
  const CONDITION_VECTORS = [
46
51
  { name: 'dailyWindow from<to 命中', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_0930, expected: true },
47
- { name: 'dailyWindow 含头不含尾', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_1800, expected: false },
52
+ { name: 'dailyWindow 双闭含 to 端点', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_1800, expected: true },
48
53
  { name: 'dailyWindow 含头', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_0900, expected: true },
49
54
  { name: 'dailyWindow 跨午夜夜段命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_2330, expected: true },
50
55
  { name: 'dailyWindow 跨午夜凌晨命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_0300, expected: true },
51
56
  { name: 'dailyWindow 跨午夜日间不命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_1200, expected: false },
52
- { name: 'dailyWindow from===to 全天', condition: { kind: 'dailyWindow', from: '00:00', to: '00:00' }, date: SUNDAY_1200, expected: true },
57
+ { name: 'dailyWindow from===to 单点命中', condition: { kind: 'dailyWindow', from: '12:00', to: '12:00' }, date: SUNDAY_1200, expected: true },
58
+ { name: 'dailyWindow 00:00~23:59 全天含尾', condition: { kind: 'dailyWindow', from: '00:00', to: '23:59' }, date: SUNDAY_2330, expected: true },
53
59
  { name: 'dailyWindow 非法格式', condition: { kind: 'dailyWindow', from: 'abc', to: '06:00' }, date: SUNDAY_1200, expected: false },
54
60
  { name: 'weekdays 命中周日', condition: { kind: 'weekdays', days: [0] }, date: SUNDAY_0930, expected: true },
55
61
  { name: 'weekdays 空 days 不成立', condition: { kind: 'weekdays', days: [] }, date: SUNDAY_0930, expected: false },
56
62
  { name: 'weekdays 未命中', condition: { kind: 'weekdays', days: [1] }, date: SUNDAY_0930, expected: false },
57
63
  { name: 'weekdays days 非数组', condition: { kind: 'weekdays', days: 'x' }, date: SUNDAY_0930, expected: false },
58
- { name: 'monthDays 双闭含端', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN15, expected: true },
64
+ { name: 'monthDays 双闭含 to 端点', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN15, expected: true },
65
+ { name: 'monthDays 含首日', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN01, expected: true },
59
66
  { name: 'monthDays 界外', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN16, expected: false },
67
+ { name: 'monthDays 1~31 全月', condition: { kind: 'monthDays', from: 1, to: 31 }, date: MON_JAN31, expected: true },
68
+ { name: 'monthDays from===to 单日命中', condition: { kind: 'monthDays', from: 15, to: 15 }, date: MON_JAN15, expected: true },
60
69
  { name: 'monthDays 环绕起点命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN26, expected: true },
61
70
  { name: 'monthDays 环绕中段不命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN15, expected: false },
62
- { name: 'monthDays 环绕终点命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN03, expected: true },
71
+ { name: 'monthDays 环绕终点含端点', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN03, expected: true },
63
72
  { name: 'monthDays 非整数不成立', condition: { kind: 'monthDays', from: 1.5, to: 5 }, date: MON_JAN15, expected: false },
64
73
  { name: 'dateRange 命中', condition: { kind: 'dateRange', from: '2026-01-01', to: '2026-01-31' }, date: MON_JAN15, expected: true },
74
+ { name: 'dateRange to 端点命中', condition: { kind: 'dateRange', from: '2026-01-01', to: '2026-01-31' }, date: MON_JAN31, expected: true },
75
+ { name: 'dateRange from===to 单日命中', condition: { kind: 'dateRange', from: '2026-01-15', to: '2026-01-15' }, date: MON_JAN15, expected: true },
65
76
  { name: 'dateRange 倒序不成立', condition: { kind: 'dateRange', from: '2026-01-31', to: '2026-01-01' }, date: MON_JAN15, expected: false },
66
77
  { name: 'dateRange 非零填充不成立', condition: { kind: 'dateRange', from: '2026-1-1', to: '2026-1-31' }, date: MON_JAN15, expected: false },
67
78
  { name: 'dateRange 2 月 30 号滚月后不命中', condition: { kind: 'dateRange', from: '2026-02-01', to: '2026-02-28' }, date: FEB_30_ROLLS_TO_MAR02, expected: false },
@@ -292,3 +303,26 @@ test(`costOf 双侧一致(${COST_VECTORS.length} 向量)`, () => {
292
303
  assert.deepEqual(host, vector.expected, vector.name)
293
304
  }
294
305
  })
306
+
307
+ // 路由引用键双实现同源:采集器 refOf(样本 model 键)与 client 注入点B turnModelOf(回合计价键)
308
+ // 双全拼两段、仅 model 用裸名、多 route 取首条;键不成立时两侧各按自身约定降级
309
+ // (refOf→undefined,turnModelOf→全通配键),该场景单独断言各自约定值
310
+ const REF_VECTORS = [
311
+ { name: '双全拼两段', route: { provider: 'deepseek', model: 'deepseek-chat' }, expected: 'deepseek/deepseek-chat' },
312
+ { name: '仅 model 用裸名', route: { model: 'm' }, expected: 'm' },
313
+ { name: 'provider 空串按裸名', route: { provider: '', model: 'm' }, expected: 'm' },
314
+ { name: 'provider 非空 model 空串不成立', route: { provider: 'p', model: '' }, expected: undefined },
315
+ { name: '空 route 对象不成立', route: {}, expected: undefined },
316
+ { name: '多 route 取首条', route: { provider: 'a', model: 'x' }, tail: { provider: 'b', model: 'y' }, expected: 'a/x' },
317
+ ]
318
+
319
+ test(`路由引用键双侧一致(${REF_VECTORS.length} 向量)`, () => {
320
+ for (const vector of REF_VECTORS) {
321
+ const routes = vector.tail ? [vector.route, vector.tail] : [vector.route]
322
+ assert.equal(hostRefOf(vector.route), vector.expected, vector.name)
323
+ assert.equal(clientTurnModelOf({ routes }), vector.expected ?? clientModelUnrouted, vector.name)
324
+ }
325
+ assert.equal(hostRefOf(null), undefined)
326
+ assert.equal(clientTurnModelOf({ routes: [] }), clientModelUnrouted)
327
+ assert.equal(clientTurnModelOf(null), clientModelUnrouted)
328
+ })
@@ -228,37 +228,47 @@ test('rules 非数组或 timestamp 非法返回 null', () => {
228
228
  assert.equal(matchPrice([makeRule()], 'a/b', Number.NaN), null)
229
229
  })
230
230
 
231
- test('dailyWindow from<to 含头不含尾', () => {
232
- // Given 10:00~12:00 When 逐时刻判定 Then 10:00 与 11:59 命中,09:59 与 12:00 不命中
231
+ test('dailyWindow from<to 双闭含两端', () => {
232
+ // Given 10:00~12:00 When 逐时刻判定 Then 10:00 与 12:00 命中,09:59 与 12:01 不命中
233
233
  const condition = { kind: 'dailyWindow', from: '10:00', to: '12:00' }
234
234
  assert.equal(conditionMatches(condition, localTime(6, 10)), true)
235
235
  assert.equal(conditionMatches(condition, localTime(6, 11, 59)), true)
236
+ assert.equal(conditionMatches(condition, localTime(6, 12)), true)
236
237
  assert.equal(conditionMatches(condition, localTime(6, 9, 59)), false)
237
- assert.equal(conditionMatches(condition, localTime(6, 12)), false)
238
+ assert.equal(conditionMatches(condition, localTime(6, 12, 1)), false)
238
239
  })
239
240
 
240
241
  test('dailyWindow 跨午夜窗口按本地分量命中', () => {
241
- // Given 22:00~02:00 When 本地 23:30 与次日 01:00 Then 命中;本地正午不命中
242
+ // Given 22:00~02:00 When 本地 23:30 与次日 01:00/02:00 Then 命中;本地正午不命中
242
243
  const condition = { kind: 'dailyWindow', from: '22:00', to: '02:00' }
243
244
  assert.equal(conditionMatches(condition, localTime(6, 23, 30)), true)
244
245
  assert.equal(conditionMatches(condition, new Date(2026, 8, 7, 1)), true)
246
+ assert.equal(conditionMatches(condition, new Date(2026, 8, 7, 2)), true)
245
247
  assert.equal(conditionMatches(condition, localTime(6, 12)), false)
246
248
  })
247
249
 
248
- test('dailyWindow from===to 全天生效', () => {
249
- // Given 10:00~10:00 When 判定 00:00/10:00/23:59 Then 全部命中
250
+ test('dailyWindow from===to 为单点仅命中该时刻', () => {
251
+ // Given 10:00~10:00 双闭单点 When 判定 10:00 与其他时刻 Then 仅 10:00 命中
250
252
  const condition = { kind: 'dailyWindow', from: '10:00', to: '10:00' }
251
- assert.equal(conditionMatches(condition, localTime(6, 0)), true)
252
253
  assert.equal(conditionMatches(condition, localTime(6, 10)), true)
254
+ assert.equal(conditionMatches(condition, localTime(6, 10, 1)), false)
255
+ assert.equal(conditionMatches(condition, localTime(6, 23, 59)), false)
256
+ })
257
+
258
+ test('dailyWindow 全天用 00:00~23:59 表达', () => {
259
+ // Given 00:00~23:59 When 判定 00:00 与 23:59 Then 全部命中
260
+ const condition = { kind: 'dailyWindow', from: '00:00', to: '23:59' }
261
+ assert.equal(conditionMatches(condition, localTime(6, 0)), true)
253
262
  assert.equal(conditionMatches(condition, localTime(6, 23, 59)), true)
254
263
  })
255
264
 
256
265
  test('dailyWindow 分钟级评估', () => {
257
- // Given 10:05~10:10 When 判定 10:05/10:09/10:10 Then 前两者命中
266
+ // Given 10:05~10:10 When 判定 10:05/10:09/10:10/10:11 Then 前三者命中
258
267
  const condition = { kind: 'dailyWindow', from: '10:05', to: '10:10' }
259
268
  assert.equal(conditionMatches(condition, localTime(6, 10, 5)), true)
260
269
  assert.equal(conditionMatches(condition, localTime(6, 10, 9)), true)
261
- assert.equal(conditionMatches(condition, localTime(6, 10, 10)), false)
270
+ assert.equal(conditionMatches(condition, localTime(6, 10, 10)), true)
271
+ assert.equal(conditionMatches(condition, localTime(6, 10, 11)), false)
262
272
  })
263
273
 
264
274
  test('weekdays 集合按 getDay 命中', () => {
@@ -273,10 +283,11 @@ test('weekdays 空数组条件不成立', () => {
273
283
  assert.equal(conditionMatches({ kind: 'weekdays', days: [] }, SUNDAY), false)
274
284
  })
275
285
 
276
- test('monthDays 正向段双闭含单日', () => {
277
- // Given 1~15 When 判定 1/15/16 号 Then 双闭;Given 5~5 When 判定 5/6 号 Then 单日
286
+ test('monthDays 双闭含两端,from===to 为单日', () => {
287
+ // Given 1~15 When 判定 1/14/15/16 号 Then 含 1 与 15 不含 16;Given 5~5 单日 When 判定 5/6 号 Then 仅 5 命中
278
288
  const span = { kind: 'monthDays', from: 1, to: 15 }
279
289
  assert.equal(conditionMatches(span, new Date(2026, 8, 1)), true)
290
+ assert.equal(conditionMatches(span, new Date(2026, 8, 14)), true)
280
291
  assert.equal(conditionMatches(span, new Date(2026, 8, 15)), true)
281
292
  assert.equal(conditionMatches(span, new Date(2026, 8, 16)), false)
282
293
  const single = { kind: 'monthDays', from: 5, to: 5 }
@@ -284,44 +295,65 @@ test('monthDays 正向段双闭含单日', () => {
284
295
  assert.equal(conditionMatches(single, new Date(2026, 8, 6)), false)
285
296
  })
286
297
 
287
- test('monthDays 单日 31 号在 2 月自然不触发', () => {
288
- // Given 31~31 When 判定 131 号与 2 月末 Then 前者命中;2 月无 31 号,该月任意日不命中
298
+ test('monthDays 全月用 1~31 表达,31 号仅大月命中', () => {
299
+ // Given 1~31 When 判定 1/30/31 Then 全月命中;2 月无 31 号自然不触发
300
+ const month = { kind: 'monthDays', from: 1, to: 31 }
301
+ assert.equal(conditionMatches(month, new Date(2026, 8, 1)), true)
302
+ assert.equal(conditionMatches(month, new Date(2026, 8, 30)), true)
303
+ assert.equal(conditionMatches(month, new Date(2026, 0, 31)), true)
304
+ assert.equal(conditionMatches(month, new Date(2026, 1, 28)), true)
305
+ })
306
+
307
+ test('monthDays 单日 31 号用 31~31 表达', () => {
308
+ // Given 31~31 When 判定 1 月 31 号与 2 月末 Then 仅大月 31 号命中,2 月无 31 号自然不触发
289
309
  const condition = { kind: 'monthDays', from: 31, to: 31 }
290
310
  assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
291
311
  assert.equal(conditionMatches(condition, new Date(2026, 1, 28)), false)
292
312
  })
293
313
 
294
- test('monthDays 26~25 跨月环绕覆盖全月', () => {
295
- // Given 26~25 账单周期 When 判定 26/31/1/25/15 号 Then 26..31 并 1..25 覆盖全月,含 15
314
+ test('monthDays 26~25 跨月环绕为 26 起至次月 25', () => {
315
+ // Given 26~25 账单周期双闭 When 判定 26/31/1/24/25/15 号 Then 26..31 并 1..25 命中
296
316
  const condition = { kind: 'monthDays', from: 26, to: 25 }
297
317
  assert.equal(conditionMatches(condition, new Date(2026, 0, 26)), true)
298
318
  assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
299
319
  assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), true)
320
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 24)), true)
300
321
  assert.equal(conditionMatches(condition, new Date(2026, 1, 25)), true)
301
322
  assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), true)
302
323
  })
303
324
 
304
325
  test('monthDays 27~10 环绕存在不命中间隙', () => {
305
- // Given 27~10 When 判定 27/31/1/10 与 15/26 号 Then 前四命中,间隙内不命中
326
+ // Given 27~10 双闭 When 判定 27/31/1/10 与 9/15/26 号 Then 27..31 并 1..10 命中,间隙不命中
306
327
  const condition = { kind: 'monthDays', from: 27, to: 10 }
307
328
  assert.equal(conditionMatches(condition, new Date(2026, 0, 27)), true)
308
329
  assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
309
330
  assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), true)
310
331
  assert.equal(conditionMatches(condition, new Date(2026, 1, 10)), true)
332
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 9)), true)
333
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 11)), false)
311
334
  assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), false)
312
335
  assert.equal(conditionMatches(condition, new Date(2026, 0, 26)), false)
313
336
  })
314
337
 
315
- test('dateRange 双闭区间', () => {
316
- // Given 2026-01-01~2026-01-31 When 判定首末日与界外 Then 端点命中,两侧不命中
338
+ test('dateRange 双闭含两端', () => {
339
+ // Given 2026-01-01~2026-01-31 When 判定首日/中间/末日/界外 Then 首日中间末日命中,两侧不命中
317
340
  const condition = { kind: 'dateRange', from: '2026-01-01', to: '2026-01-31' }
318
341
  assert.equal(conditionMatches(condition, new Date(2026, 0, 1)), true)
319
342
  assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), true)
343
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 30)), true)
320
344
  assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
321
345
  assert.equal(conditionMatches(condition, new Date(2025, 11, 31)), false)
322
346
  assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), false)
323
347
  })
324
348
 
349
+ test('dateRange from===to 为单日命中', () => {
350
+ // Given 同日区间双闭 When 判定该日与前后日 Then 仅该日命中
351
+ const single = { kind: 'dateRange', from: '2026-01-15', to: '2026-01-15' }
352
+ assert.equal(conditionMatches(single, new Date(2026, 0, 15)), true)
353
+ assert.equal(conditionMatches(single, new Date(2026, 0, 14)), false)
354
+ assert.equal(conditionMatches(single, new Date(2026, 0, 16)), false)
355
+ })
356
+
325
357
  test('dateRange 倒序属配置错误不成立', () => {
326
358
  const condition = { kind: 'dateRange', from: '2026-02-01', to: '2026-01-01' }
327
359
  assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), false)
@@ -316,6 +316,54 @@ test('attachCosts 保留槽级 speed 字段供曲线消费', () => {
316
316
  assert.equal(out.daily[1].speed, 5)
317
317
  })
318
318
 
319
+ test('decode 口径:速度分子取 decodeTokens,timing 样本零桶不稀释 token 总量', () => {
320
+ const rows = [
321
+ // token 样本行:只有 token 桶,无时长
322
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20 }),
323
+ // 纯 timing 增量行:零 token 桶 + decode 配对
324
+ makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 0, decodeTokens: 30, durationMs: 2500 }),
325
+ ]
326
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-02')
327
+ const entry = out.models.find((item) => item.model === 'm1')
328
+ assert.equal(entry.tokens, 20)
329
+ assert.equal(entry.speed, 30 / (2500 / 1000))
330
+ })
331
+
332
+ test('旧格式行 durationMs 无 decodeTokens:分子回落 outputTokens', () => {
333
+ const rows = [
334
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, durationMs: 4000 }),
335
+ ]
336
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-01')
337
+ assert.equal(out.models[0].speed, 5)
338
+ })
339
+
340
+ test('ttft 模型级聚合:加权平均,无 ttft 行不参与,无数据不挂字段', () => {
341
+ const rows = [
342
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, ttftMs: 1000, ttftSteps: 1 }),
343
+ makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 30, ttftMs: 3000, ttftSteps: 2 }),
344
+ makeRow({ bucket: '2020-01-03', model: 'm1', provider: 'p1', outputTokens: 100 }),
345
+ makeRow({ bucket: '2020-01-01', model: 'm2', provider: 'p2', outputTokens: 5 }),
346
+ ]
347
+ const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-03')
348
+ const m1 = out.models.find((item) => item.model === 'm1')
349
+ assert.equal(m1.ttft, (1000 + 3000) / (1 + 2))
350
+ const m2 = out.models.find((item) => item.model === 'm2')
351
+ assert.equal('ttft' in m2, false)
352
+ })
353
+
354
+ test('槽级 ttft 聚合:同槽配对,无 ttft 槽不挂字段,attachCosts 保留', () => {
355
+ const rows = [
356
+ makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, ttftMs: 2000, ttftSteps: 1 }),
357
+ makeRow({ bucket: '2020-01-01', model: 'm2', provider: 'p2', outputTokens: 30, ttftMs: 4000, ttftSteps: 1 }),
358
+ makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 100 }),
359
+ ]
360
+ const result = aggregateRange(rows, 'D', '2020-01-01', '2020-01-02')
361
+ assert.equal(result.daily[0].ttft, (2000 + 4000) / 2)
362
+ assert.equal('ttft' in result.daily[1], false)
363
+ const out = attachCosts(result, rows, 'D', [ruleOf()])
364
+ assert.equal(out.daily[0].ttft, 3000)
365
+ })
366
+
319
367
  test('attachCosts H 槽按桶起点计价并归集 totals 与 models', () => {
320
368
  const rows = [
321
369
  makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', inputTokens: 1000000, outputTokens: 500000 }),
@@ -393,6 +441,7 @@ test('attachCosts M 行 unpriced 按父 H 桶去重', () => {
393
441
  })
394
442
 
395
443
  test('attachCosts D 端点按 H 行桶起点计价折叠到日槽', () => {
444
+ // 双闭:两日期段不相邻不重叠,22/23 桶起点 01-01 落段一,00 桶起点 01-02 落段二
396
445
  const costRows = [
397
446
  makeRow({ bucket: '2020-01-01T22', model: 'm1', inputTokens: 1000000 }),
398
447
  makeRow({ bucket: '2020-01-01T23', model: 'm1', inputTokens: 1000000 }),
@@ -404,7 +453,7 @@ test('attachCosts D 端点按 H 行桶起点计价折叠到日槽', () => {
404
453
  ]
405
454
  const rules = [
406
455
  ruleOf({ price: inputPrice(1), conditions: [{ kind: 'dateRange', from: '2020-01-01', to: '2020-01-01' }] }),
407
- ruleOf({ price: inputPrice(3), conditions: [{ kind: 'dateRange', from: '2020-01-02', to: '2020-01-02' }] }),
456
+ ruleOf({ price: inputPrice(3), conditions: [{ kind: 'dateRange', from: '2020-01-02', to: '2020-01-03' }] }),
408
457
  ]
409
458
  const result = aggregateRange(aggregateRows, 'D', '2020-01-01', '2020-01-02')
410
459
  const out = attachCosts(result, costRows, 'D', rules)
@@ -69,6 +69,13 @@ function makeStore({ rows = [], fail } = {}) {
69
69
  const calls = { rangeRows: 0 }
70
70
  return {
71
71
  calls,
72
+ backupInfo() {
73
+ return { available: false }
74
+ },
75
+ async restoreFromBackup() {
76
+ calls.restoreFromBackup = (calls.restoreFromBackup ?? 0) + 1
77
+ return { takenAt: 0, rows: 0 }
78
+ },
72
79
  async rangeRows(g, from, to) {
73
80
  calls.rangeRows += 1
74
81
  if (fail) throw fail
@@ -301,7 +308,7 @@ test('range 合法请求回 UsageStatsRange 信封', async () => {
301
308
  assert.equal(store.calls.rangeRows, 1)
302
309
  })
303
310
 
304
- test('status 回采集快照', async () => {
311
+ test('status 回采集快照并附带回退点元信息', async () => {
305
312
  const collector = makeCollector({
306
313
  stateOverrides: { running: true, total: 5, done: 2, scannedSessions: 7, lastSessionId: 's1', error: 'boom', recordFailures: 1 },
307
314
  })
@@ -310,7 +317,7 @@ test('status 回采集快照', async () => {
310
317
  const parsed = JSON.parse(res.body)
311
318
  assert.equal(res.statusCode, 200)
312
319
  assert.equal(parsed.ok, true)
313
- assert.deepEqual(parsed.value, collector.status())
320
+ assert.deepEqual(parsed.value, { ...collector.status(), backup: { available: false } })
314
321
  })
315
322
 
316
323
  test('minutes 超出保留窗口标注 coveredFrom/coveredTo', async () => {
@@ -35,12 +35,12 @@ const PREFS_COST_ON = { cachePrecision: false, tokenDetail: false, costDisplay:
35
35
  const zhT = createTranslator(MESSAGES_ZH)
36
36
  const enT = createTranslator(MESSAGES_EN)
37
37
 
38
- // 费用组装配输入:精确规则在前、全通配规则兜底,通配仅全天时段生效
38
+ // 费用组装配输入:精确规则在前、全通配规则兜底,通配仅全天时段(00:00~23:59 双闭)生效
39
39
  const COST_RULES = [
40
40
  { model: 'p/m', currency: '¥', price: { input: 2, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
41
41
  {
42
42
  model: '*/*', currency: '', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 },
43
- conditions: [{ kind: 'dailyWindow', from: '00:00', to: '00:00' }],
43
+ conditions: [{ kind: 'dailyWindow', from: '00:00', to: '23:59' }],
44
44
  },
45
45
  ]
46
46
  const COST_USAGE = { uncachedInputTokens: 500000, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }
@@ -358,11 +358,11 @@ test('buildCostItem routes 首个 model 精确规则优先于通配', () => {
358
358
  assert.equal(buildCostItem(routed, wildcardFirst, PREFS_COST_ON, zhT, COST_NOW), '费用 ≈ ¥1.00')
359
359
  })
360
360
 
361
- test('buildCostItem 时间条件按传入时刻评估:窗口外通配不生效', () => {
362
- // 通配规则 00:00~00:00 from===to 全天生效;08:00~09:00 与固定时刻不交
363
- const midnight = [{ ...COST_RULES[1], conditions: [{ kind: 'dailyWindow', from: '00:00', to: '00:00' }] }]
361
+ test('buildCostItem 时间条件按传入时刻评估:窗口外均不生效', () => {
362
+ // 双闭下 00:00~00:00 单点仅命中零点整,COST_NOW 10:00 不命中;08:00~09:00 与固定时刻不交
363
+ const singlePoint = [{ ...COST_RULES[1], conditions: [{ kind: 'dailyWindow', from: '00:00', to: '00:00' }] }]
364
364
  const daytime = [{ ...COST_RULES[1], conditions: [{ kind: 'dailyWindow', from: '08:00', to: '09:00' }] }]
365
- assert.equal(buildCostItem(COST_USAGE, midnight, PREFS_COST_ON, zhT, COST_NOW), '费用 ≈ 0.50')
365
+ assert.equal(buildCostItem(COST_USAGE, singlePoint, PREFS_COST_ON, zhT, COST_NOW), '')
366
366
  assert.equal(buildCostItem(COST_USAGE, daytime, PREFS_COST_ON, zhT, COST_NOW), '—')
367
367
  })
368
368
 
@@ -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,
@@ -217,6 +219,64 @@ test('老行缺 durationMs 字段:增量求和不产 NaN', async () => {
217
219
  assert.equal(seen.outputTokens, 40)
218
220
  })
219
221
 
222
+ test('decode 配对与首字样本落三粒度累加,timing 样本不重复计 token', async () => {
223
+ const domain = fakeDomain()
224
+ const store = new UsageStore(facilityOf(domain))
225
+ const at = local(2026, 8, 2, 14, 37)
226
+ // chunk 先发 token 样本:只有 token 桶
227
+ await store.record(tokenSample(at, { durationMs: undefined }))
228
+ // message 补发纯 timing 增量:零桶 + decodeTokens + durationMs + ttftMs
229
+ await store.record(tokenSample(at + 1000, {
230
+ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
231
+ decodeTokens: 20, durationMs: 30000, ttftMs: 10000,
232
+ }))
233
+ await store.flushNow()
234
+ const key = 'D|2026-08-02|deepseek|deepseek/deepseek-chat'
235
+ const daily = domain.rows.get(key)
236
+ assert.equal(daily.outputTokens, 20)
237
+ assert.equal(daily.decodeTokens, 20)
238
+ assert.equal(daily.durationMs, 30000)
239
+ assert.equal(daily.ttftMs, 10000)
240
+ assert.equal(daily.ttftSteps, 1)
241
+ const minute = domain.rows.get('M|2026-08-02T14:30|deepseek|deepseek/deepseek-chat')
242
+ assert.equal(minute.decodeTokens, 20)
243
+ assert.equal(minute.ttftMs, 10000)
244
+ })
245
+
246
+ test('timing 缺失样本:decodeTokens 与 ttft 字段按零累计', async () => {
247
+ const domain = fakeDomain()
248
+ const store = new UsageStore(facilityOf(domain))
249
+ const at = local(2026, 8, 2, 14, 37)
250
+ await store.record(tokenSample(at))
251
+ await store.flushNow()
252
+ const seen = domain.rows.get('D|2026-08-02|deepseek|deepseek/deepseek-chat')
253
+ assert.equal(seen.decodeTokens, 0)
254
+ assert.equal(seen.durationMs, 0)
255
+ assert.equal(seen.ttftMs, 0)
256
+ assert.equal(seen.ttftSteps, 0)
257
+ })
258
+
259
+ test('老行缺 decode/ttft 字段:增量求和不产 NaN', async () => {
260
+ const domain = fakeDomain()
261
+ const store = new UsageStore(facilityOf(domain))
262
+ const at = local(2026, 8, 2, 14, 37)
263
+ const key = 'D|2026-08-02|deepseek|deepseek/deepseek-chat'
264
+ await store.record(tokenSample(at))
265
+ await store.flushNow()
266
+ // 模拟存量旧格式行:回写去掉新增字段
267
+ const stored = domain.rows.get(key)
268
+ delete stored.decodeTokens
269
+ delete stored.ttftMs
270
+ delete stored.ttftSteps
271
+ await store.record(tokenSample(at + 1000, { decodeTokens: 20, durationMs: 3000, ttftMs: 500 }))
272
+ await store.flushNow()
273
+ const seen = domain.rows.get(key)
274
+ assert.equal(seen.decodeTokens, 20)
275
+ assert.equal(seen.durationMs, 3000)
276
+ assert.equal(seen.ttftMs, 500)
277
+ assert.equal(seen.ttftSteps, 1)
278
+ })
279
+
220
280
  test('missing-record 首写竞态:put 种子后重试写入真值', async () => {
221
281
  const domain = fakeDomain()
222
282
  const store = new UsageStore(facilityOf(domain))
@@ -437,6 +497,61 @@ test('启动时游标非空则既有行保留', async () => {
437
497
  assert.equal(domain.rows.size, 1)
438
498
  })
439
499
 
500
+ test('reset 前自动快照,restore 整体回退到快照态', async () => {
501
+ const domain = fakeDomain()
502
+ const backup = fakeDomain()
503
+ const store = new UsageStore(facilityOf(domain, backup))
504
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
505
+ await store.markSeenSessions(['s1', 's2'])
506
+ assert.equal(store.backupInfo().available, false)
507
+ await store.reset()
508
+ assert.equal(domain.rows.size, 0)
509
+ assert.deepEqual([...(await store.seenSessions())], [])
510
+ // 快照在清空前生成:行与游标都可回退
511
+ const info = store.backupInfo()
512
+ assert.equal(info.available, true)
513
+ assert.equal(info.rows, 3)
514
+ assert.equal(info.sessions, 2)
515
+ const restored = await store.restoreFromBackup()
516
+ assert.equal(restored.rows, 3)
517
+ assert.equal(domain.rows.size, 3)
518
+ assert.deepEqual([...(await store.seenSessions())].sort(), ['s1', 's2'])
519
+ })
520
+
521
+ test('restore 前先快照当前态:回退动作自身可再回退', async () => {
522
+ const domain = fakeDomain()
523
+ const backup = fakeDomain()
524
+ const store = new UsageStore(facilityOf(domain, backup))
525
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
526
+ await store.reset()
527
+ await store.record(tokenSample(local(2026, 8, 3, 14, 0), { inputTokens: 99 }))
528
+ await store.flushNow()
529
+ // 第一轮回退到重建前(有 8/2 数据)
530
+ await store.restoreFromBackup()
531
+ const dayRows = await store.rangeRows('D', '2026-08-02', '2026-08-02')
532
+ assert.equal(dayRows.length, 1)
533
+ // 第二次回退回到"重建后"(8/3 数据),回退链不断
534
+ await store.restoreFromBackup()
535
+ const after = await store.rangeRows('D', '2026-08-03', '2026-08-03')
536
+ assert.equal(after.length, 1)
537
+ })
538
+
539
+ test('备份域不可用:重建照常,backupInfo 标记不可用,restore 报错', async () => {
540
+ const domain = fakeDomain()
541
+ const facility = {
542
+ open: async (definition) => {
543
+ if (definition?.name === 'usage_stats_backup') throw new Error('backup domain unavailable')
544
+ return domain
545
+ },
546
+ }
547
+ const store = new UsageStore(facility)
548
+ await store.record(tokenSample(local(2026, 8, 2, 14, 0)))
549
+ await store.reset()
550
+ assert.equal(domain.rows.size, 0)
551
+ assert.equal(store.backupInfo().available, false)
552
+ await assert.rejects(store.restoreFromBackup(), /backup domain unavailable/)
553
+ })
554
+
440
555
  test('域打开失败进入降级:ready 可等待,操作按调用失败', async () => {
441
556
  const store = new UsageStore({ open: () => Promise.reject(new Error('domain already open')) })
442
557
  await store.readyPromise()