@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.
- package/README.md +13 -11
- package/package.json +1 -1
- package/src/client.js +588 -160
- package/src/collector.js +46 -12
- package/src/index.js +2 -2
- package/src/pricing.js +47 -9
- package/src/query.js +35 -3
- package/src/routes.js +3 -2
- package/src/store.js +123 -43
- package/test/client-eval.mjs +23 -0
- package/test/client-scope.test.mjs +60 -0
- package/test/client.test.mjs +253 -43
- package/test/collector.test.mjs +93 -0
- package/test/pricing-parity.test.mjs +99 -36
- package/test/pricing.test.mjs +112 -10
- package/test/query.test.mjs +53 -3
- package/test/routes.test.mjs +11 -7
- package/test/stats-line.test.mjs +8 -16
- package/test/store.test.mjs +145 -22
- package/test/turn-tail.test.mjs +12 -20
package/test/store.test.mjs
CHANGED
|
@@ -94,17 +94,17 @@ const tokenSample = (time, extra = {}) => ({
|
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
test('分钟保留天数默认常量与上限和计划一致', () => {
|
|
97
|
-
assert.equal(DEFAULT_MINUTE_RETENTION_DAYS,
|
|
98
|
-
assert.equal(MINUTE_RETENTION_MAX_DAYS,
|
|
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(
|
|
106
|
-
assert.equal(clampMinuteRetentionDays(-1),
|
|
107
|
-
assert.equal(clampMinuteRetentionDays('x'),
|
|
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('
|
|
230
|
+
test('flush 中单粒度失败:错误经 onFlushError 上抛,失败行留 pending 重试,成功粒度落盘', async () => {
|
|
231
|
+
const failedKeys = new Set()
|
|
188
232
|
const domain = fakeDomain({
|
|
189
|
-
failUpdate: (key) =>
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 到
|
|
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-
|
|
264
|
-
await table.put('M|2026-08-
|
|
265
|
-
await table.put('M|2026-08-
|
|
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-
|
|
268
|
-
assert.equal(domain.rows.has('M|2026-08-
|
|
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('
|
|
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
|
+
})
|
package/test/turn-tail.test.mjs
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
// 注入点B(S15)纯函数层测试:select 矩阵/模型键回退/单轮行文本/title 口径/文案键/显隐 CSS 镜像
|
|
2
|
-
// client.js
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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: [] }),
|
|
64
|
-
assert.equal(turnModelOf({}),
|
|
65
|
-
assert.equal(turnModelOf(null),
|
|
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:
|
|
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),
|
|
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
|
|