@mzzsfy/dsh-usage-dash 0.5.0 → 0.7.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 +4 -4
- package/package.json +1 -1
- package/src/client.js +237 -71
- package/src/query.js +254 -249
- package/test/client.test.mjs +238 -25
- package/test/query.test.mjs +53 -3
- package/test/turn-tail.test.mjs +16 -0
package/test/client.test.mjs
CHANGED
|
@@ -67,6 +67,9 @@ const {
|
|
|
67
67
|
matchPrice,
|
|
68
68
|
maxSlotsFor,
|
|
69
69
|
minuteTickLabel,
|
|
70
|
+
modelIoPercentText,
|
|
71
|
+
hasTipContent,
|
|
72
|
+
tipModelEntries,
|
|
70
73
|
modelSegmentLabel,
|
|
71
74
|
modelSpeedText,
|
|
72
75
|
modelNameOf,
|
|
@@ -75,6 +78,7 @@ const {
|
|
|
75
78
|
otherDetailItems,
|
|
76
79
|
parseEnvelope,
|
|
77
80
|
partitionGroupOf,
|
|
81
|
+
pointerAt,
|
|
78
82
|
providerOf,
|
|
79
83
|
rateAxisTicks,
|
|
80
84
|
removeRulesAt,
|
|
@@ -82,6 +86,11 @@ const {
|
|
|
82
86
|
resolveDayRange,
|
|
83
87
|
resolveHourRange,
|
|
84
88
|
resolveMinuteRange,
|
|
89
|
+
resolveHourCustomRange,
|
|
90
|
+
resolveMinuteCustomRange,
|
|
91
|
+
resolvePointQuery,
|
|
92
|
+
formatDateTimeInput,
|
|
93
|
+
parseLocalDateTime,
|
|
85
94
|
shortDay,
|
|
86
95
|
smoothPath,
|
|
87
96
|
tipPlace,
|
|
@@ -177,6 +186,112 @@ test('pointStats 缓存命中须视图与挡位双匹配', () => {
|
|
|
177
186
|
assert.equal(pointStatsMatches(null, 'hour', '24h'), false)
|
|
178
187
|
})
|
|
179
188
|
|
|
189
|
+
// —— 时/分自定义时间范围:归一语义与预设挡一致(小时 floor 整点桶,分钟 from 对齐 10 分钟桶、to 保原分钟) ——
|
|
190
|
+
|
|
191
|
+
const CUSTOM_FROM = '2026-03-14T09:45'
|
|
192
|
+
const CUSTOM_TO = '2026-03-15T14:10'
|
|
193
|
+
|
|
194
|
+
test('hour 自定义范围两端 floor 到所在小时桶', () => {
|
|
195
|
+
// Given datetime 输入含分钟偏移 When 解析 Then 两端均取所在小时桶起点(闭区间)
|
|
196
|
+
assert.deepEqual(resolveHourCustomRange(CUSTOM_FROM, CUSTOM_TO), { from: '2026-03-14T09', to: '2026-03-15T14' })
|
|
197
|
+
assert.deepEqual(resolveHourCustomRange('2026-03-14T09:00', '2026-03-15T14:59'), { from: '2026-03-14T09', to: '2026-03-15T14' })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('hour 自定义跨度超保留期钳起点,临界窗经 floor 与原窗输出等价', () => {
|
|
201
|
+
// Given 跨度 16 天 When 解析 Then 起点钳到终点前 15 天整点;15 天余零头的窗钳后 floor 输出不变
|
|
202
|
+
assert.deepEqual(
|
|
203
|
+
resolveHourCustomRange('2026-02-27T00:00', '2026-03-15T14:10'),
|
|
204
|
+
{ from: '2026-02-28T14', to: '2026-03-15T14' },
|
|
205
|
+
)
|
|
206
|
+
assert.deepEqual(
|
|
207
|
+
resolveHourCustomRange('2026-02-28T14:00', '2026-03-15T14:10'),
|
|
208
|
+
{ from: '2026-02-28T14', to: '2026-03-15T14' },
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('hour 自定义非法输入返回 null', () => {
|
|
213
|
+
// Given 空值/垃圾串/from 晚于 to When 解析 Then 恒 null,不发请求
|
|
214
|
+
assert.equal(resolveHourCustomRange('', CUSTOM_TO), null)
|
|
215
|
+
assert.equal(resolveHourCustomRange(CUSTOM_FROM, ''), null)
|
|
216
|
+
assert.equal(resolveHourCustomRange('junk', CUSTOM_TO), null)
|
|
217
|
+
assert.equal(resolveHourCustomRange('2026-13-40T09:00', CUSTOM_TO), null)
|
|
218
|
+
assert.equal(resolveHourCustomRange(CUSTOM_TO, CUSTOM_FROM), null)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('parseLocalDateTime 拒绝数字分量回卷的日历非法值', () => {
|
|
222
|
+
// Given 超界分量(13 月/2 月 30 日/25 时)在 Date 构造下静默回卷 When 解析 Then 逐分量回读拦截为 null
|
|
223
|
+
assert.equal(parseLocalDateTime('2026-13-01T00:00'), null)
|
|
224
|
+
assert.equal(parseLocalDateTime('2026-02-30T10:00'), null)
|
|
225
|
+
assert.equal(parseLocalDateTime('2026-03-14T25:00'), null)
|
|
226
|
+
assert.deepEqual(parseLocalDateTime('2026-03-14T09:45'), new Date(2026, 2, 14, 9, 45))
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('hour 自定义拒绝回卷后仍保序的非法日历输入', () => {
|
|
230
|
+
// Given 13 月回卷为次年 1 月且整体仍早于 to When 解析 Then 恒 null,不静默改写查询时段
|
|
231
|
+
assert.equal(resolveHourCustomRange('2026-13-01T00:00', '2027-06-01T00:00'), null)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('minute 自定义 from 对齐 10 分钟桶,to 保持原始分钟', () => {
|
|
235
|
+
// Given from 含非对齐分钟 When 解析 Then from floor 到桶边界,to 原值保留(闭区间上界)
|
|
236
|
+
assert.deepEqual(resolveMinuteCustomRange(CUSTOM_FROM, CUSTOM_TO), { from: '2026-03-14T09:40', to: '2026-03-15T14:10' })
|
|
237
|
+
assert.deepEqual(resolveMinuteCustomRange('2026-03-14T09:40', '2026-03-15T14:17'), { from: '2026-03-14T09:40', to: '2026-03-15T14:17' })
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('minute 自定义跨度超保留期钳起点', () => {
|
|
241
|
+
// Given 跨度 8 天 When 解析 Then 起点钳到终点前 7 天的 10 分钟桶边界
|
|
242
|
+
assert.deepEqual(
|
|
243
|
+
resolveMinuteCustomRange('2026-03-06T00:00', '2026-03-15T14:10'),
|
|
244
|
+
{ from: '2026-03-08T14:10', to: '2026-03-15T14:10' },
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('minute 自定义非法输入返回 null', () => {
|
|
249
|
+
assert.equal(resolveMinuteCustomRange('', CUSTOM_TO), null)
|
|
250
|
+
assert.equal(resolveMinuteCustomRange('junk', CUSTOM_TO), null)
|
|
251
|
+
assert.equal(resolveMinuteCustomRange(CUSTOM_TO, CUSTOM_FROM), null)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('resolvePointQuery 预设挡返回挡位键与滚动窗口请求', () => {
|
|
255
|
+
// Given 时/分预设挡 When 解析查询 Then 键为挡位 id,请求与既有 resolve 同构
|
|
256
|
+
const hour = resolvePointQuery('hour', '24h', '', '', NOW)
|
|
257
|
+
assert.equal(hour.key, '24h')
|
|
258
|
+
assert.deepEqual(hour.request, resolveHourRange('24h', NOW))
|
|
259
|
+
const minute = resolvePointQuery('minute', '3h', '', '', NOW)
|
|
260
|
+
assert.equal(minute.key, '3h')
|
|
261
|
+
assert.deepEqual(minute.request, resolveMinuteRange('3h', NOW))
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('resolvePointQuery 自定义挡键含归一范围且随范围变化', () => {
|
|
265
|
+
// Given 同视图同挡不同范围 When 解析查询 Then 键互异;同范围键稳定(缓存可命中)
|
|
266
|
+
const first = resolvePointQuery('hour', 'custom', CUSTOM_FROM, CUSTOM_TO, NOW)
|
|
267
|
+
assert.deepEqual(first.request, { from: '2026-03-14T09', to: '2026-03-15T14' })
|
|
268
|
+
assert.equal(first.key, 'custom:2026-03-14T09:2026-03-15T14')
|
|
269
|
+
const again = resolvePointQuery('hour', 'custom', CUSTOM_FROM, CUSTOM_TO, NOW)
|
|
270
|
+
assert.equal(again.key, first.key)
|
|
271
|
+
const shifted = resolvePointQuery('hour', 'custom', '2026-03-14T10:45', CUSTOM_TO, NOW)
|
|
272
|
+
assert.notEqual(shifted.key, first.key)
|
|
273
|
+
const minute = resolvePointQuery('minute', 'custom', CUSTOM_FROM, CUSTOM_TO, NOW)
|
|
274
|
+
assert.deepEqual(minute.request, { from: '2026-03-14T09:40', to: '2026-03-15T14:10' })
|
|
275
|
+
assert.notEqual(minute.key, first.key)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
test('resolvePointQuery 自定义输入不完整返回 null', () => {
|
|
279
|
+
// Given 任一端为空 When 解析查询 Then null,面板不发请求不误清缓存
|
|
280
|
+
assert.equal(resolvePointQuery('hour', 'custom', '', CUSTOM_TO, NOW), null)
|
|
281
|
+
assert.equal(resolvePointQuery('minute', 'custom', CUSTOM_FROM, '', NOW), null)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('maxSlotsFor custom 挡取保留期桶数上限', () => {
|
|
285
|
+
// Given 自定义挡受保留期钳制 When 取渲染上限 Then 小时为 15 天小时桶数,分钟为 7 天 10 分钟桶数
|
|
286
|
+
assert.equal(maxSlotsFor('hour', 'custom'), 15 * 24 + 1)
|
|
287
|
+
assert.equal(maxSlotsFor('minute', 'custom'), (7 * 24 * 60) / 10 + 1)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('formatDateTimeInput 产出 datetime-local 分钟粒度值', () => {
|
|
291
|
+
assert.equal(formatDateTimeInput(new Date(2026, 2, 15, 9, 5)), '2026-03-15T09:05')
|
|
292
|
+
assert.equal(formatDateTimeInput(NOW), '2026-03-15T14:30')
|
|
293
|
+
})
|
|
294
|
+
|
|
180
295
|
test('每视图渲染上限等于闭区间桶数', () => {
|
|
181
296
|
assert.equal(maxSlotsFor('day', '90'), DAY_MAX_SLOTS)
|
|
182
297
|
assert.equal(maxSlotsFor('hour', '24h'), 25)
|
|
@@ -476,50 +591,57 @@ test('heatLevel 零值为空档其余按峰值四分位', () => {
|
|
|
476
591
|
assert.equal(heatLevel(1, 1), 5)
|
|
477
592
|
})
|
|
478
593
|
|
|
479
|
-
const TIP_ANCHOR = {
|
|
594
|
+
const TIP_ANCHOR = { x: 100, y: 100 }
|
|
480
595
|
const TIP_SIZE = { width: 80, height: 40 }
|
|
481
596
|
const TIP_BOUNDS = { left: 0, top: 0, right: 800, bottom: 600 }
|
|
482
597
|
|
|
483
|
-
test('tipPlace
|
|
484
|
-
assert.deepEqual(tipPlace(TIP_ANCHOR, TIP_SIZE, TIP_BOUNDS), { left:
|
|
598
|
+
test('tipPlace 指针右下空位放右下', () => {
|
|
599
|
+
assert.deepEqual(tipPlace(TIP_ANCHOR, TIP_SIZE, TIP_BOUNDS), { left: 108, top: 108 })
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test('tipPlace 右侧越界翻左侧', () => {
|
|
603
|
+
assert.deepEqual(tipPlace({ x: 730, y: 100 }, TIP_SIZE, TIP_BOUNDS), { left: 642, top: 108 })
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
test('tipPlace 下方越界翻上方', () => {
|
|
607
|
+
assert.deepEqual(tipPlace({ x: 100, y: 570 }, TIP_SIZE, TIP_BOUNDS), { left: 108, top: 522 })
|
|
485
608
|
})
|
|
486
609
|
|
|
487
|
-
test('tipPlace
|
|
488
|
-
|
|
489
|
-
assert.deepEqual(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS), { left: 130, top: 108 })
|
|
610
|
+
test('tipPlace 右下均越界双翻', () => {
|
|
611
|
+
assert.deepEqual(tipPlace({ x: 730, y: 570 }, TIP_SIZE, TIP_BOUNDS), { left: 642, top: 522 })
|
|
490
612
|
})
|
|
491
613
|
|
|
492
|
-
test('tipPlace
|
|
493
|
-
|
|
494
|
-
assert.equal(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS).top, 68)
|
|
614
|
+
test('tipPlace 双翻后仍越界钳进边界', () => {
|
|
615
|
+
assert.deepEqual(tipPlace({ x: 798, y: 598 }, TIP_SIZE, TIP_BOUNDS), { left: 710, top: 550 })
|
|
495
616
|
})
|
|
496
617
|
|
|
497
|
-
test('tipPlace
|
|
498
|
-
assert.deepEqual(tipPlace({
|
|
499
|
-
assert.equal(tipPlace({ left: 100, top: 56, right: 140, bottom: 96 }, TIP_SIZE, TIP_BOUNDS).top, 8)
|
|
500
|
-
assert.equal(tipPlace({ left: 100, top: 8, right: 140, bottom: 48 }, TIP_SIZE, { left: 0, top: 0, right: 800, bottom: 104 }).top, 56)
|
|
618
|
+
test('tipPlace 恰贴右缘等号归属放右侧', () => {
|
|
619
|
+
assert.deepEqual(tipPlace({ x: 704, y: 100 }, TIP_SIZE, TIP_BOUNDS), { left: 712, top: 108 })
|
|
501
620
|
})
|
|
502
621
|
|
|
503
|
-
test('tipPlace
|
|
504
|
-
|
|
505
|
-
assert.equal(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS).top, 492)
|
|
622
|
+
test('tipPlace 左上角指针放右下不翻转', () => {
|
|
623
|
+
assert.deepEqual(tipPlace({ x: 2, y: 2 }, TIP_SIZE, TIP_BOUNDS), { left: 10, top: 10 })
|
|
506
624
|
})
|
|
507
625
|
|
|
508
|
-
test('tipPlace
|
|
509
|
-
|
|
510
|
-
const anchor = { left: 100, top: 20, right: 140, bottom: 50 }
|
|
511
|
-
assert.equal(tipPlace(anchor, TIP_SIZE, bounds).top, 8)
|
|
626
|
+
test('tipPlace 提示框宽于边界钳贴左缘', () => {
|
|
627
|
+
assert.equal(tipPlace({ x: 400, y: 100 }, { width: 900, height: 40 }, TIP_BOUNDS).left, 8)
|
|
512
628
|
})
|
|
513
629
|
|
|
514
|
-
test('
|
|
515
|
-
|
|
516
|
-
assert.equal(tipPlace(anchor, TIP_SIZE, TIP_BOUNDS).left, 8)
|
|
630
|
+
test('pointerAt 指针事件取指针坐标', () => {
|
|
631
|
+
assert.deepEqual(pointerAt({ clientX: 12, clientY: 34 }), { x: 12, y: 34 })
|
|
517
632
|
})
|
|
518
633
|
|
|
519
|
-
test('
|
|
634
|
+
test('pointerAt 焦点事件回退目标矩形中心', () => {
|
|
635
|
+
const event = {
|
|
636
|
+
currentTarget: { getBoundingClientRect: () => ({ left: 10, top: 20, right: 30, bottom: 60 }) },
|
|
637
|
+
}
|
|
638
|
+
assert.deepEqual(pointerAt(event), { x: 20, y: 40 })
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
test('tipPlace 零尺寸或非法锚点返回隐藏', () => {
|
|
520
642
|
assert.equal(tipPlace(TIP_ANCHOR, { width: 0, height: 0 }, TIP_BOUNDS), null)
|
|
521
|
-
assert.equal(tipPlace({ left: 0, top: 0, right: 0, bottom: 0 }, TIP_SIZE, TIP_BOUNDS), null)
|
|
522
643
|
assert.equal(tipPlace(null, TIP_SIZE, TIP_BOUNDS), null)
|
|
644
|
+
assert.equal(tipPlace({ x: Number.NaN, y: 100 }, TIP_SIZE, TIP_BOUNDS), null)
|
|
523
645
|
})
|
|
524
646
|
|
|
525
647
|
// —— S8 命中率曲线 ——
|
|
@@ -691,6 +813,56 @@ test('donutSegments 全零 total 回落下限防除零', () => {
|
|
|
691
813
|
assert.equal(segments[0].percent, 0)
|
|
692
814
|
})
|
|
693
815
|
|
|
816
|
+
test('groupStats 哨兵折叠四桶求和,top 模型四桶透传', () => {
|
|
817
|
+
// Given 超 top 上限模型条目带四桶 When 分组 Then 哨兵四桶为 rest 求和,top 条目原字段保留
|
|
818
|
+
const stats = {
|
|
819
|
+
models: [
|
|
820
|
+
{ model: 'p/m1', tokens: 500, inputTokens: 300, outputTokens: 200, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
821
|
+
{ model: 'p/m2', tokens: 400, inputTokens: 100, outputTokens: 300, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
822
|
+
{ model: 'p/m3', tokens: 300, inputTokens: 0, outputTokens: 100, cacheReadTokens: 150, cacheWriteTokens: 50 },
|
|
823
|
+
{ model: 'p/m4', tokens: 200, inputTokens: 80, outputTokens: 120, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
824
|
+
{ model: 'p/m5', tokens: 100, inputTokens: 60, outputTokens: 40, cacheReadTokens: 0, cacheWriteTokens: 0 },
|
|
825
|
+
{ model: 'p/m6', tokens: 50, inputTokens: 10, outputTokens: 30, cacheReadTokens: 5, cacheWriteTokens: 5 },
|
|
826
|
+
],
|
|
827
|
+
daily: [],
|
|
828
|
+
}
|
|
829
|
+
const grouped = groupStats(stats)
|
|
830
|
+
assert.equal(grouped.models[0].inputTokens, 300)
|
|
831
|
+
assert.equal(grouped.models[0].outputTokens, 200)
|
|
832
|
+
assert.deepEqual(grouped.models[5], {
|
|
833
|
+
model: OTHER_MODEL,
|
|
834
|
+
tokens: 50,
|
|
835
|
+
inputTokens: 10,
|
|
836
|
+
outputTokens: 30,
|
|
837
|
+
cacheReadTokens: 5,
|
|
838
|
+
cacheWriteTokens: 5,
|
|
839
|
+
items: [stats.models[5]],
|
|
840
|
+
})
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
test('groupStats 哨兵费用同源:无 cost 输入不挂 cost,有 cost 求和', () => {
|
|
844
|
+
// Given rest 条目无 cost When 折叠 Then 哨兵无 cost 字段;Given rest 带 cost When 折叠 Then 哨兵求和
|
|
845
|
+
const bare = groupStats({ models: [
|
|
846
|
+
{ model: 'p/m1', tokens: 500 },
|
|
847
|
+
{ model: 'p/m2', tokens: 400 },
|
|
848
|
+
{ model: 'p/m3', tokens: 300 },
|
|
849
|
+
{ model: 'p/m4', tokens: 200 },
|
|
850
|
+
{ model: 'p/m5', tokens: 100 },
|
|
851
|
+
{ model: 'p/m6', tokens: 50 },
|
|
852
|
+
], daily: [] })
|
|
853
|
+
assert.equal('cost' in bare.models[5], false)
|
|
854
|
+
const priced = groupStats({ models: [
|
|
855
|
+
{ model: 'p/m1', tokens: 500, cost: 1 },
|
|
856
|
+
{ model: 'p/m2', tokens: 400, cost: 2 },
|
|
857
|
+
{ model: 'p/m3', tokens: 300, cost: undefined },
|
|
858
|
+
{ model: 'p/m4', tokens: 200, cost: 4 },
|
|
859
|
+
{ model: 'p/m5', tokens: 100, cost: 8 },
|
|
860
|
+
{ model: 'p/m6', tokens: 50, cost: 16 },
|
|
861
|
+
{ model: 'p/m7', tokens: 25, cost: 32 },
|
|
862
|
+
], daily: [] })
|
|
863
|
+
assert.equal(priced.models[5].cost, 16 + 32)
|
|
864
|
+
})
|
|
865
|
+
|
|
694
866
|
test('groupStats 哨兵保留其他模型明细供展开与提示', () => {
|
|
695
867
|
const stats = {
|
|
696
868
|
models: [
|
|
@@ -705,6 +877,8 @@ test('groupStats 哨兵保留其他模型明细供展开与提示', () => {
|
|
|
705
877
|
}
|
|
706
878
|
const grouped = groupStats(stats)
|
|
707
879
|
assert.deepEqual(grouped.models[5].items, [{ model: 'p/m6', tokens: 50 }])
|
|
880
|
+
// Given 旧形条目无四桶 When 折叠 Then 哨兵不合成零值四桶,与 top 条目降级形态一致
|
|
881
|
+
assert.equal('inputTokens' in grouped.models[5], false)
|
|
708
882
|
})
|
|
709
883
|
|
|
710
884
|
test('groupStats 逐日保留其他明细映射供 tooltip', () => {
|
|
@@ -737,6 +911,45 @@ test('模型速度文本:官方吞吐口径格式化,无速度为空串', () =>
|
|
|
737
911
|
assert.equal(modelSpeedText(undefined), '')
|
|
738
912
|
})
|
|
739
913
|
|
|
914
|
+
test('模型输入输出占比构成:缓存/输入/输出 三段各占该模型 token 总量', () => {
|
|
915
|
+
// Given 四桶齐备的模型条目 When 格式化 Then 三段占比相对 tokens 总量,缓存=读+写,合计 100%,顺序缓存/输入/输出
|
|
916
|
+
const zhT = createTranslator(MESSAGES_ZH)
|
|
917
|
+
const enT = createTranslator(MESSAGES_EN)
|
|
918
|
+
const item = { tokens: 1000, inputTokens: 31, cacheReadTokens: 954, cacheWriteTokens: 10, outputTokens: 5 }
|
|
919
|
+
assert.equal(modelIoPercentText(item, zhT), '缓存 96.4% · 输入 3.1% · 输出 0.5%')
|
|
920
|
+
assert.equal(modelIoPercentText(item, enT), 'Cache 96.4% · Input 3.1% · Output 0.5%')
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
test('趋势 tooltip 模型行:仅含可见且当前时段有用量的模型', () => {
|
|
924
|
+
// Given 槽内 byModel 含零值与非零值,OTHER 明细含零值与无序多元素 When 取行数据 Then 主行与子行均过滤零用量,子行按 tokens 降序,OTHER 不可见时子行为空
|
|
925
|
+
const hoverSlot = {
|
|
926
|
+
total: 300,
|
|
927
|
+
byModel: { 'p/a': 200, 'p/b': 0, [OTHER_MODEL]: 100 },
|
|
928
|
+
otherByModel: { 'p/y': 0, 'p/x': 20, 'p/z': 50 },
|
|
929
|
+
}
|
|
930
|
+
const visible = new Set(['p/a', 'p/b', OTHER_MODEL])
|
|
931
|
+
const rows = tipModelEntries(hoverSlot, visible)
|
|
932
|
+
assert.deepEqual(rows.main, [{ model: 'p/a', tokens: 200 }, { model: OTHER_MODEL, tokens: 100 }])
|
|
933
|
+
assert.deepEqual(rows.other, [['p/z', 50], ['p/x', 20]])
|
|
934
|
+
const hiddenOther = tipModelEntries(hoverSlot, new Set(['p/a']))
|
|
935
|
+
assert.deepEqual(hiddenOther.main, [{ model: 'p/a', tokens: 200 }])
|
|
936
|
+
assert.deepEqual(hiddenOther.other, [])
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
test('趋势 tooltip 内容门:仅 total>0 的槽渲染', () => {
|
|
940
|
+
// Given 全零槽/常规槽 When 判定 Then 全零槽无内容,常规槽有内容,null 槽无内容
|
|
941
|
+
assert.equal(hasTipContent(null), false)
|
|
942
|
+
assert.equal(hasTipContent({ total: 0 }), false)
|
|
943
|
+
assert.equal(hasTipContent({ total: 5 }), true)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
test('模型输入输出占比构成:缺四桶或零总量为空串', () => {
|
|
947
|
+
const zhT = createTranslator(MESSAGES_ZH)
|
|
948
|
+
assert.equal(modelIoPercentText({ tokens: 100 }, zhT), '')
|
|
949
|
+
assert.equal(modelIoPercentText(undefined, zhT), '')
|
|
950
|
+
assert.equal(modelIoPercentText({ tokens: 0, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 }, zhT), '')
|
|
951
|
+
})
|
|
952
|
+
|
|
740
953
|
test('模型首字文本:语言中立短时长,无 ttft 为空串', () => {
|
|
741
954
|
assert.equal(modelTtftText(200), 'TTFT 0.2s')
|
|
742
955
|
assert.equal(modelTtftText(162000), 'TTFT 2m42s')
|
package/test/query.test.mjs
CHANGED
|
@@ -117,8 +117,26 @@ test('D 粒度聚合总量排序与占比正确', () => {
|
|
|
117
117
|
assert.equal(out.topModel, 'deepseek/deepseek-chat')
|
|
118
118
|
assert.equal(out.topProvider, 'deepseek')
|
|
119
119
|
assert.deepEqual(out.models, [
|
|
120
|
-
{
|
|
121
|
-
|
|
120
|
+
{
|
|
121
|
+
model: 'deepseek/deepseek-chat',
|
|
122
|
+
provider: 'deepseek',
|
|
123
|
+
tokens: 350,
|
|
124
|
+
percent: (350 / 410) * 100,
|
|
125
|
+
inputTokens: 100,
|
|
126
|
+
outputTokens: 50,
|
|
127
|
+
cacheReadTokens: 200,
|
|
128
|
+
cacheWriteTokens: 0,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
model: 'deepseek-chat',
|
|
132
|
+
provider: 'default',
|
|
133
|
+
tokens: 60,
|
|
134
|
+
percent: (60 / 410) * 100,
|
|
135
|
+
inputTokens: 10,
|
|
136
|
+
outputTokens: 20,
|
|
137
|
+
cacheReadTokens: 0,
|
|
138
|
+
cacheWriteTokens: 30,
|
|
139
|
+
},
|
|
122
140
|
])
|
|
123
141
|
assert.deepEqual(out.providers, [
|
|
124
142
|
{ provider: 'deepseek', tokens: 350, percent: (350 / 410) * 100 },
|
|
@@ -351,6 +369,38 @@ test('ttft 模型级聚合:加权平均,无 ttft 行不参与,无数据不挂字
|
|
|
351
369
|
assert.equal('ttft' in m2, false)
|
|
352
370
|
})
|
|
353
371
|
|
|
372
|
+
test('模型级四桶聚合:models 条目带输入输出与缓存拆分,按该模型行累加', () => {
|
|
373
|
+
// Given 两模型多行四桶用量 When D 粒度聚合 Then models 每项四桶为该模型行累加,排序与占比不变
|
|
374
|
+
const rows = [
|
|
375
|
+
makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', inputTokens: 100, outputTokens: 50, cacheReadTokens: 200, cacheWriteTokens: 10 }),
|
|
376
|
+
makeRow({ bucket: '2020-01-02', model: 'm1', provider: 'p1', inputTokens: 30, outputTokens: 70 }),
|
|
377
|
+
makeRow({ bucket: '2020-01-01', model: 'm2', provider: 'p2', inputTokens: 5, outputTokens: 25, cacheWriteTokens: 8 }),
|
|
378
|
+
]
|
|
379
|
+
const out = aggregateRange(rows, 'D', '2020-01-01', '2020-01-02')
|
|
380
|
+
assert.deepEqual(out.models, [
|
|
381
|
+
{
|
|
382
|
+
model: 'm1',
|
|
383
|
+
provider: 'p1',
|
|
384
|
+
tokens: 460,
|
|
385
|
+
percent: (460 / 498) * 100,
|
|
386
|
+
inputTokens: 130,
|
|
387
|
+
outputTokens: 120,
|
|
388
|
+
cacheReadTokens: 200,
|
|
389
|
+
cacheWriteTokens: 10,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
model: 'm2',
|
|
393
|
+
provider: 'p2',
|
|
394
|
+
tokens: 38,
|
|
395
|
+
percent: (38 / 498) * 100,
|
|
396
|
+
inputTokens: 5,
|
|
397
|
+
outputTokens: 25,
|
|
398
|
+
cacheReadTokens: 0,
|
|
399
|
+
cacheWriteTokens: 8,
|
|
400
|
+
},
|
|
401
|
+
])
|
|
402
|
+
})
|
|
403
|
+
|
|
354
404
|
test('槽级 ttft 聚合:同槽配对,无 ttft 槽不挂字段,attachCosts 保留', () => {
|
|
355
405
|
const rows = [
|
|
356
406
|
makeRow({ bucket: '2020-01-01', model: 'm1', provider: 'p1', outputTokens: 20, ttftMs: 2000, ttftSteps: 1 }),
|
|
@@ -373,7 +423,7 @@ test('attachCosts H 槽按桶起点计价并归集 totals 与 models', () => {
|
|
|
373
423
|
assert.equal(out.daily[1].cost, 2)
|
|
374
424
|
assert.equal(out.cost, 2)
|
|
375
425
|
assert.equal(out.unpriced, 0)
|
|
376
|
-
assert.deepEqual(out.models, [{ model: 'm1', provider: 'p1', tokens: 1500000, percent: 100, cost: 2 }])
|
|
426
|
+
assert.deepEqual(out.models, [{ model: 'm1', provider: 'p1', tokens: 1500000, inputTokens: 1000000, outputTokens: 500000, cacheReadTokens: 0, cacheWriteTokens: 0, percent: 100, cost: 2 }])
|
|
377
427
|
assert.equal(out.from, '2020-01-01T00')
|
|
378
428
|
assert.equal(out.tokens, 1500000)
|
|
379
429
|
// 纯函数:入参 result 不被修改
|
package/test/turn-tail.test.mjs
CHANGED
|
@@ -151,6 +151,22 @@ test('反查:容器换引用(快照更替)后新节点可见', () => {
|
|
|
151
151
|
assert.equal(turnTokenUsageOfMessage(oldNodes, 'm-1'), oldUsage)
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
+
test('反查:仓库身份恒定而节点集更替时新增回合可见', () => {
|
|
155
|
+
// Given 官方 chat 仓库语义:store 实例身份跨回合恒定,values() 返回的节点集数组随 upsert 换新引用
|
|
156
|
+
const usage1 = { uncachedInputTokens: 1, outputTokens: 1, totalTokens: 2 }
|
|
157
|
+
const usage2 = { uncachedInputTokens: 7, outputTokens: 7, totalTokens: 14 }
|
|
158
|
+
let snapshot = [tailNode('m-1', usage1)]
|
|
159
|
+
const store = { values: () => snapshot }
|
|
160
|
+
assert.equal(turnUsageSourceOfMessage(store, 'm-1'), usage1)
|
|
161
|
+
// When 第二回合 turn-tail 节点入库(同一 store,节点集数组换引用)
|
|
162
|
+
snapshot = [...snapshot, tailNode('m-2', usage2)]
|
|
163
|
+
// Then 新回合反查命中,不依赖页面刷新重建索引
|
|
164
|
+
assert.equal(turnUsageSourceOfMessage(store, 'm-2'), usage2)
|
|
165
|
+
assert.equal(turnTokenUsageOfMessage(store, 'm-2'), usage2)
|
|
166
|
+
// 旧回合反查不受重建影响
|
|
167
|
+
assert.equal(turnUsageSourceOfMessage(store, 'm-1'), usage1)
|
|
168
|
+
})
|
|
169
|
+
|
|
154
170
|
test('计价模型键:双全拼两段,仅 model 用裸名,双缺回退全通配键', () => {
|
|
155
171
|
assert.equal(turnModelOf({ routes: [{ provider: 'p', model: 'm' }] }), 'p/m')
|
|
156
172
|
assert.equal(turnModelOf({ routes: [{ model: 'm' }] }), 'm')
|