@mzzsfy/dsh-usage-dash 0.2.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.
- package/README.md +54 -13
- package/package.json +2 -2
- package/src/archive-reader.js +138 -0
- package/src/client.js +548 -141
- package/src/collector.js +58 -12
- package/src/direct-log-reader.js +126 -0
- package/src/pricing.js +5 -6
- package/src/query.js +13 -0
- package/src/routes.js +14 -1
- package/src/store.js +70 -2
- package/test/archive-reader.test.mjs +365 -0
- package/test/client.test.mjs +261 -18
- package/test/collector.test.mjs +152 -8
- package/test/direct-log-reader.test.mjs +104 -0
- package/test/pricing-parity.test.mjs +38 -4
- package/test/pricing.test.mjs +50 -18
- package/test/query.test.mjs +22 -1
- package/test/routes.test.mjs +9 -2
- package/test/stats-line.test.mjs +6 -6
- package/test/store.test.mjs +58 -1
- package/test/turn-tail.test.mjs +95 -41
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|
package/test/pricing.test.mjs
CHANGED
|
@@ -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 与
|
|
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 判定
|
|
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)),
|
|
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
|
|
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
|
|
288
|
-
// Given
|
|
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
|
|
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
|
|
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)
|
package/test/query.test.mjs
CHANGED
|
@@ -296,6 +296,26 @@ test('纯缓存行带时长:输出 0 计入分母,speed 为 0', () => {
|
|
|
296
296
|
assert.equal(out.models[0].speed, 0)
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
+
test('槽级 speed 配对聚合:同槽带时长行合计,无时长槽无 speed 字段', () => {
|
|
300
|
+
const rows = [
|
|
301
|
+
makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, durationMs: 6500 }),
|
|
302
|
+
makeRow({ bucket: '2020-01-01', model: 'm2', provider: 'p2', outputTokens: 30, durationMs: 2500 }),
|
|
303
|
+
makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', outputTokens: 100 }),
|
|
304
|
+
]
|
|
305
|
+
const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-02')
|
|
306
|
+
assert.equal(out.daily[0].speed, (20 + 30) / ((6500 + 2500) / 1000))
|
|
307
|
+
assert.equal('speed' in out.daily[1], false)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('attachCosts 保留槽级 speed 字段供曲线消费', () => {
|
|
311
|
+
const rows = [
|
|
312
|
+
makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', outputTokens: 20, durationMs: 4000 }),
|
|
313
|
+
]
|
|
314
|
+
const result = aggregateRange(rows, 'H', '2020-01-01T00', '2020-01-01T02')
|
|
315
|
+
const out = attachCosts(result, rows, 'H', [ruleOf()])
|
|
316
|
+
assert.equal(out.daily[1].speed, 5)
|
|
317
|
+
})
|
|
318
|
+
|
|
299
319
|
test('attachCosts H 槽按桶起点计价并归集 totals 与 models', () => {
|
|
300
320
|
const rows = [
|
|
301
321
|
makeRow({ bucket: '2020-01-01T01', model: 'm1', provider: 'p1', inputTokens: 1000000, outputTokens: 500000 }),
|
|
@@ -373,6 +393,7 @@ test('attachCosts M 行 unpriced 按父 H 桶去重', () => {
|
|
|
373
393
|
})
|
|
374
394
|
|
|
375
395
|
test('attachCosts D 端点按 H 行桶起点计价折叠到日槽', () => {
|
|
396
|
+
// 双闭:两日期段不相邻不重叠,22/23 桶起点 01-01 落段一,00 桶起点 01-02 落段二
|
|
376
397
|
const costRows = [
|
|
377
398
|
makeRow({ bucket: '2020-01-01T22', model: 'm1', inputTokens: 1000000 }),
|
|
378
399
|
makeRow({ bucket: '2020-01-01T23', model: 'm1', inputTokens: 1000000 }),
|
|
@@ -384,7 +405,7 @@ test('attachCosts D 端点按 H 行桶起点计价折叠到日槽', () => {
|
|
|
384
405
|
]
|
|
385
406
|
const rules = [
|
|
386
407
|
ruleOf({ price: inputPrice(1), conditions: [{ kind: 'dateRange', from: '2020-01-01', to: '2020-01-01' }] }),
|
|
387
|
-
ruleOf({ price: inputPrice(3), conditions: [{ kind: 'dateRange', from: '2020-01-02', to: '2020-01-
|
|
408
|
+
ruleOf({ price: inputPrice(3), conditions: [{ kind: 'dateRange', from: '2020-01-02', to: '2020-01-03' }] }),
|
|
388
409
|
]
|
|
389
410
|
const result = aggregateRange(aggregateRows, 'D', '2020-01-01', '2020-01-02')
|
|
390
411
|
const out = attachCosts(result, costRows, 'D', rules)
|
package/test/routes.test.mjs
CHANGED
|
@@ -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
|
|
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 () => {
|
package/test/stats-line.test.mjs
CHANGED
|
@@ -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: '
|
|
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
|
-
//
|
|
363
|
-
const
|
|
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,
|
|
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
|
|
package/test/store.test.mjs
CHANGED
|
@@ -81,7 +81,9 @@ function fakeDomain({ failUpdate = () => null } = {}) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const facilityOf = (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()
|