@mzzsfy/dsh-usage-dash 0.1.0 → 0.2.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 +11 -9
- package/package.json +1 -1
- package/src/client.js +440 -131
- package/src/collector.js +46 -12
- package/src/index.js +2 -2
- package/src/pricing.js +47 -9
- package/src/query.js +22 -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 +145 -37
- 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 +33 -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/client.test.mjs
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
// client 纯函数层测试:滚动窗口映射/信封解析/上限裁剪/文案/格式化/分组/柱状几何
|
|
2
2
|
// Given/When/Then 场景内嵌于用例描述
|
|
3
|
-
// client.js
|
|
3
|
+
// client.js 为经典 script bundle(禁 import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
|
|
4
4
|
import { test } from 'node:test'
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { dirname, join } from 'node:path'
|
|
9
|
+
import { CLIENT_BODY, DECLARATION_NAMES } from './client-eval.mjs'
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
const DECLARATION_NAMES = [
|
|
12
|
-
...new Set(
|
|
13
|
-
[...CLIENT_SOURCE.matchAll(/^(?:const|function|let) ([A-Za-z_$][\w$]*)/gm)].map((match) => match[1])
|
|
14
|
-
),
|
|
15
|
-
]
|
|
16
|
-
const core = new Function(`${CLIENT_SOURCE}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
|
|
11
|
+
const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
|
|
17
12
|
|
|
18
13
|
const {
|
|
19
14
|
CHART_HEIGHT,
|
|
@@ -39,12 +34,16 @@ const {
|
|
|
39
34
|
aggregateCurrencyOf,
|
|
40
35
|
applyCurrencyToRules,
|
|
41
36
|
cacheRateText,
|
|
37
|
+
coerceConditions,
|
|
38
|
+
coercePricingRules,
|
|
42
39
|
costOf,
|
|
43
40
|
costTitleText,
|
|
44
41
|
createTranslator,
|
|
45
42
|
daysInRange,
|
|
43
|
+
defaultCondition,
|
|
46
44
|
defaultPricingRule,
|
|
47
45
|
donutSegments,
|
|
46
|
+
pointStatsMatches,
|
|
48
47
|
formatCompact,
|
|
49
48
|
formatCost,
|
|
50
49
|
formatDuration,
|
|
@@ -59,10 +58,12 @@ const {
|
|
|
59
58
|
hourTickLabel,
|
|
60
59
|
indexOfDay,
|
|
61
60
|
isEmptyRange,
|
|
61
|
+
logTimeOf,
|
|
62
62
|
matchPrice,
|
|
63
63
|
maxSlotsFor,
|
|
64
64
|
minuteTickLabel,
|
|
65
65
|
modelSegmentLabel,
|
|
66
|
+
modelSpeedText,
|
|
66
67
|
modelNameOf,
|
|
67
68
|
niceTicks,
|
|
68
69
|
otherDetailItems,
|
|
@@ -74,7 +75,6 @@ const {
|
|
|
74
75
|
resolveMinuteRange,
|
|
75
76
|
shortDay,
|
|
76
77
|
smoothPath,
|
|
77
|
-
statusLineActive,
|
|
78
78
|
tipPlace,
|
|
79
79
|
translateWith,
|
|
80
80
|
trimSlots,
|
|
@@ -91,7 +91,7 @@ const NOW = new Date(2026, 2, 15, 14, 30, 45)
|
|
|
91
91
|
|
|
92
92
|
test('day 预设映射为最近 N 天闭区间', () => {
|
|
93
93
|
assert.deepEqual(resolveDayRange('7', NOW), { from: '2026-03-09', to: '2026-03-15' })
|
|
94
|
-
assert.deepEqual(resolveDayRange('
|
|
94
|
+
assert.deepEqual(resolveDayRange('30', NOW), { from: '2026-02-14', to: '2026-03-15' })
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
test('day 预设跨年与跨月边界', () => {
|
|
@@ -105,7 +105,7 @@ test('day 90 天预设起点回卷上年', () => {
|
|
|
105
105
|
|
|
106
106
|
test('hour 预设 now-N 舍入到整点桶起点', () => {
|
|
107
107
|
assert.deepEqual(resolveHourRange('24h', NOW), { from: '2026-03-14T14', to: '2026-03-15T14' })
|
|
108
|
-
assert.deepEqual(resolveHourRange('
|
|
108
|
+
assert.deepEqual(resolveHourRange('3d', NOW), { from: '2026-03-12T14', to: '2026-03-15T14' })
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
test('hour 预设跨日且窗口起点落整点', () => {
|
|
@@ -113,30 +113,53 @@ test('hour 预设跨日且窗口起点落整点', () => {
|
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
test('minute 预设起点对齐 10 分钟桶,to 保持原始分钟', () => {
|
|
116
|
-
assert.deepEqual(resolveMinuteRange('
|
|
117
|
-
assert.deepEqual(resolveMinuteRange('
|
|
116
|
+
assert.deepEqual(resolveMinuteRange('3h', NOW), { from: '2026-03-15T11:30', to: '2026-03-15T14:30' })
|
|
117
|
+
assert.deepEqual(resolveMinuteRange('24h', NOW), { from: '2026-03-14T14:30', to: '2026-03-15T14:30' })
|
|
118
118
|
const offGrid = new Date(2026, 2, 15, 14, 37, 20)
|
|
119
|
-
assert.deepEqual(resolveMinuteRange('
|
|
119
|
+
assert.deepEqual(resolveMinuteRange('3h', offGrid), { from: '2026-03-15T11:30', to: '2026-03-15T14:37' })
|
|
120
120
|
})
|
|
121
121
|
|
|
122
122
|
test('minute 预设整桶边界不再回退', () => {
|
|
123
|
-
assert.deepEqual(resolveMinuteRange('
|
|
123
|
+
assert.deepEqual(resolveMinuteRange('3h', new Date(2026, 2, 15, 14, 30, 0)), { from: '2026-03-15T11:30', to: '2026-03-15T14:30' })
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
test('预设定义表覆盖三视图', () => {
|
|
127
|
-
assert.deepEqual(DAY_PRESETS, ['7', '
|
|
128
|
-
assert.deepEqual(HOUR_PRESETS, ['24h', '
|
|
129
|
-
assert.deepEqual(MINUTE_PRESETS, ['
|
|
127
|
+
assert.deepEqual(DAY_PRESETS, ['7', '30', '90'])
|
|
128
|
+
assert.deepEqual(HOUR_PRESETS, ['24h', '3d', '7d', '15d'])
|
|
129
|
+
assert.deepEqual(MINUTE_PRESETS, ['3h', '24h', '3d', '7d'])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('时/分挡位文案键逐挡齐备,含跨表同 id 挡位', () => {
|
|
133
|
+
// Given 时/分挡位表存在同 id('24h') When 逐挡查词典 Then 两语言均有对应键,漏配即回退键名
|
|
134
|
+
for (const id of HOUR_PRESETS) {
|
|
135
|
+
assert.notEqual(MESSAGES_ZH[`hourPreset.${id}`], undefined, `zh hourPreset.${id}`)
|
|
136
|
+
assert.notEqual(MESSAGES_EN[`hourPreset.${id}`], undefined, `en hourPreset.${id}`)
|
|
137
|
+
}
|
|
138
|
+
for (const id of MINUTE_PRESETS) {
|
|
139
|
+
assert.notEqual(MESSAGES_ZH[`minutePreset.${id}`], undefined, `zh minutePreset.${id}`)
|
|
140
|
+
assert.notEqual(MESSAGES_EN[`minutePreset.${id}`], undefined, `en minutePreset.${id}`)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('pointStats 缓存命中须视图与挡位双匹配', () => {
|
|
145
|
+
// Given 同挡位 id 跨视图('24h') When 判定缓存命中 Then 视图不同即不命中,防误用他端点数据
|
|
146
|
+
const cached = { view: 'hour', preset: '24h', value: { daily: [] } }
|
|
147
|
+
assert.equal(pointStatsMatches(cached, 'hour', '24h'), true)
|
|
148
|
+
assert.equal(pointStatsMatches(cached, 'minute', '24h'), false)
|
|
149
|
+
assert.equal(pointStatsMatches(cached, 'hour', '3d'), false)
|
|
150
|
+
assert.equal(pointStatsMatches(null, 'hour', '24h'), false)
|
|
130
151
|
})
|
|
131
152
|
|
|
132
153
|
test('每视图渲染上限等于闭区间桶数', () => {
|
|
133
154
|
assert.equal(maxSlotsFor('day', '90'), DAY_MAX_SLOTS)
|
|
134
155
|
assert.equal(maxSlotsFor('hour', '24h'), 25)
|
|
135
|
-
assert.equal(maxSlotsFor('hour', '
|
|
136
|
-
assert.equal(maxSlotsFor('hour', '
|
|
137
|
-
assert.equal(maxSlotsFor('
|
|
138
|
-
assert.equal(maxSlotsFor('minute', '
|
|
139
|
-
assert.equal(maxSlotsFor('minute', '
|
|
156
|
+
assert.equal(maxSlotsFor('hour', '3d'), 73)
|
|
157
|
+
assert.equal(maxSlotsFor('hour', '7d'), 169)
|
|
158
|
+
assert.equal(maxSlotsFor('hour', '15d'), 361)
|
|
159
|
+
assert.equal(maxSlotsFor('minute', '3h'), 19)
|
|
160
|
+
assert.equal(maxSlotsFor('minute', '24h'), 145)
|
|
161
|
+
assert.equal(maxSlotsFor('minute', '3d'), 433)
|
|
162
|
+
assert.equal(maxSlotsFor('minute', '7d'), 1009)
|
|
140
163
|
})
|
|
141
164
|
|
|
142
165
|
test('trim 超上限裁最旧且恰好达上限不裁', () => {
|
|
@@ -145,6 +168,11 @@ test('trim 超上限裁最旧且恰好达上限不裁', () => {
|
|
|
145
168
|
assert.equal(trimSlots(slots, 6), slots)
|
|
146
169
|
})
|
|
147
170
|
|
|
171
|
+
test('日志条目时刻补零为 HH:mm:ss', () => {
|
|
172
|
+
assert.equal(logTimeOf(new Date(2026, 8, 8, 9, 3, 7).getTime()), '09:03:07')
|
|
173
|
+
assert.equal(logTimeOf(new Date(2026, 8, 8, 19, 13, 47).getTime()), '19:13:47')
|
|
174
|
+
})
|
|
175
|
+
|
|
148
176
|
test('信封解析成功透传 value', () => {
|
|
149
177
|
assert.deepEqual(parseEnvelope({ ok: true, value: { a: 1 } }), { ok: true, value: { a: 1 } })
|
|
150
178
|
})
|
|
@@ -177,19 +205,11 @@ test('translateWith zh/en 占位替换与缺键回退键名', () => {
|
|
|
177
205
|
test('createTranslator 与纯查表同构', () => {
|
|
178
206
|
const enT = createTranslator(MESSAGES_EN)
|
|
179
207
|
assert.equal(enT('rangeCustom'), 'Custom')
|
|
180
|
-
assert.equal(enT('hourPreset'
|
|
208
|
+
assert.equal(enT('hourPreset.15d'), '15 days')
|
|
209
|
+
assert.equal(enT('minutePreset.7d'), '7 days')
|
|
181
210
|
assert.equal(enT('missing.key'), 'missing.key')
|
|
182
211
|
})
|
|
183
212
|
|
|
184
|
-
test('状态行仅在回扫进行或异常存在时可见', () => {
|
|
185
|
-
assert.equal(statusLineActive(null), false)
|
|
186
|
-
assert.equal(statusLineActive({ running: false, skippedSessions: 0, recordFailures: 0 }), false)
|
|
187
|
-
assert.equal(statusLineActive({ running: true, total: 3, done: 1 }), true)
|
|
188
|
-
assert.equal(statusLineActive({ running: false, error: 'boom' }), true)
|
|
189
|
-
assert.equal(statusLineActive({ running: false, skippedSessions: 2 }), true)
|
|
190
|
-
assert.equal(statusLineActive({ running: false, recordFailures: 1 }), true)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
213
|
test('zh/en 词典键集完全一致', () => {
|
|
194
214
|
assert.deepEqual(Object.keys(MESSAGES_ZH).sort(), Object.keys(MESSAGES_EN).sort())
|
|
195
215
|
})
|
|
@@ -536,6 +556,13 @@ test('donut 分段可访问标签格式化名称数值与占比', () => {
|
|
|
536
556
|
assert.equal(modelSegmentLabel('p/m', 1234, 12.34), 'p/m: 1,234 (12.3%)')
|
|
537
557
|
})
|
|
538
558
|
|
|
559
|
+
test('模型速度文本:官方吞吐口径格式化,无速度为空串', () => {
|
|
560
|
+
assert.equal(modelSpeedText(5.5556), '5.6 tok/s')
|
|
561
|
+
assert.equal(modelSpeedText(12.34), '12 tok/s')
|
|
562
|
+
assert.equal(modelSpeedText(0), '0 tok/s')
|
|
563
|
+
assert.equal(modelSpeedText(undefined), '')
|
|
564
|
+
})
|
|
565
|
+
|
|
539
566
|
// —— S14 费用格式化与展示辅助(镜像函数核心语义见 pricing-parity.test.mjs) ——
|
|
540
567
|
|
|
541
568
|
test('formatCost 千分位与两位小数', () => {
|
|
@@ -551,10 +578,10 @@ test('formatCost 微观值四位小数', () => {
|
|
|
551
578
|
})
|
|
552
579
|
|
|
553
580
|
test('镜像函数基本行为:非法输入 null 与命中计价', () => {
|
|
554
|
-
assert.equal(matchPrice(null, 'm', NOW), null)
|
|
555
|
-
assert.equal(matchPrice([], 'm', NOW), null)
|
|
581
|
+
assert.equal(matchPrice(null, 'x/m', NOW), null)
|
|
582
|
+
assert.equal(matchPrice([], 'x/m', NOW), null)
|
|
556
583
|
assert.deepEqual(
|
|
557
|
-
matchPrice([{ model: 'm', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] }], 'm', NOW),
|
|
584
|
+
matchPrice([{ model: 'x/m', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] }], 'x/m', NOW),
|
|
558
585
|
{ input: 1, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
559
586
|
)
|
|
560
587
|
assert.equal(costOf({ input: 2, output: 0, cacheRead: 0, cacheWrite: 0 }, { inputTokens: 1000 * 1000 }), 2)
|
|
@@ -594,17 +621,23 @@ test('defaultPricingRule 承接给定货币,缺省回落首档', () => {
|
|
|
594
621
|
assert.deepEqual(defaultPricingRule('$').conditions, [])
|
|
595
622
|
})
|
|
596
623
|
|
|
597
|
-
test('validatePricingRules 就地校验 model
|
|
624
|
+
test('validatePricingRules 就地校验 model 必填两段式与价格非空非负', () => {
|
|
598
625
|
const valid = [{ model: 'p/m', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] }]
|
|
599
626
|
assert.equal(validatePricingRules(valid).size, 0)
|
|
600
627
|
const broken = [
|
|
601
628
|
{ model: ' ', currency: '¥', price: { input: '', output: -1, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
|
|
629
|
+
{ model: 'solo', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
|
|
630
|
+
{ model: '*', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
|
|
631
|
+
{ model: '/x', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [] },
|
|
602
632
|
]
|
|
603
633
|
const errors = validatePricingRules(broken)
|
|
604
634
|
assert.equal(errors.get('0.model'), 'required')
|
|
605
635
|
assert.equal(errors.get('0.price.input'), 'required')
|
|
606
636
|
assert.equal(errors.get('0.price.output'), 'priceInvalid')
|
|
607
637
|
assert.equal(errors.has('0.price.cacheRead'), false)
|
|
638
|
+
assert.equal(errors.get('1.model'), 'modelFormat')
|
|
639
|
+
assert.equal(errors.get('2.model'), 'modelFormat')
|
|
640
|
+
assert.equal(errors.get('3.model'), 'modelFormat')
|
|
608
641
|
})
|
|
609
642
|
|
|
610
643
|
test('costTitleText 未计价计数后缀', () => {
|
|
@@ -612,3 +645,78 @@ test('costTitleText 未计价计数后缀', () => {
|
|
|
612
645
|
assert.equal(costTitleText(zhT, 0), '按当前费率对历史用量估算,精度为小时级')
|
|
613
646
|
assert.equal(costTitleText(zhT, 3), '按当前费率对历史用量估算,精度为小时级,3 个小时桶未计价')
|
|
614
647
|
})
|
|
648
|
+
|
|
649
|
+
test('defaultCondition 各类型默认值即填即用', () => {
|
|
650
|
+
// Given 四种条件类型 When 取默认 Then 时段全天/周几空/号段全月/日期段当天
|
|
651
|
+
assert.deepEqual(defaultCondition('dailyWindow'), { kind: 'dailyWindow', from: '00:00', to: '00:00' })
|
|
652
|
+
assert.deepEqual(defaultCondition('weekdays'), { kind: 'weekdays', days: [] })
|
|
653
|
+
assert.deepEqual(defaultCondition('monthDays'), { kind: 'monthDays', from: 1, to: 31 })
|
|
654
|
+
assert.deepEqual(defaultCondition('dateRange', new Date(2026, 2, 15)), { kind: 'dateRange', from: '2026-03-15', to: '2026-03-15' })
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
test('validatePricingRules 拒绝非法时刻与时刻字段缺失', () => {
|
|
658
|
+
// Given dailyWindow 时刻超界或缺失 When 校验 Then 标 condTime/required
|
|
659
|
+
const rules = [
|
|
660
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '99:99', to: '08:00' }] },
|
|
661
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '', to: '08:00' }] },
|
|
662
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dailyWindow', from: '23:59', to: '24:00' }] },
|
|
663
|
+
]
|
|
664
|
+
const errors = validatePricingRules(rules)
|
|
665
|
+
assert.equal(errors.get('0.conditions.0.from'), 'condTime')
|
|
666
|
+
assert.equal(errors.get('1.conditions.0.from'), 'required')
|
|
667
|
+
assert.equal(errors.get('2.conditions.0.to'), 'condTime')
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test('validatePricingRules 校验周几与月号段边界', () => {
|
|
671
|
+
// Given weekdays 含 7、monthDays 含 0 或 32 When 校验 Then 标 condWeekday/condMonthDay
|
|
672
|
+
const rules = [
|
|
673
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'weekdays', days: [0, 7] }] },
|
|
674
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 0, to: 31 }] },
|
|
675
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'monthDays', from: 1, to: 32 }] },
|
|
676
|
+
]
|
|
677
|
+
const errors = validatePricingRules(rules)
|
|
678
|
+
assert.equal(errors.get('0.conditions.0.days'), 'condWeekday')
|
|
679
|
+
assert.equal(errors.get('1.conditions.0.from'), 'condMonthDay')
|
|
680
|
+
assert.equal(errors.get('2.conditions.0.to'), 'condMonthDay')
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
test('validatePricingRules 校验日期段格式与倒序', () => {
|
|
684
|
+
// Given dateRange 非规范日期或倒序 When 校验 Then 标 condDate/condRange
|
|
685
|
+
const rules = [
|
|
686
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-3-5', to: '2026-03-08' }] },
|
|
687
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-08', to: '2026-03-05' }] },
|
|
688
|
+
{ model: 'a/b', currency: '¥', price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 }, conditions: [{ kind: 'dateRange', from: '2026-03-05', to: '2026-03-08' }] },
|
|
689
|
+
]
|
|
690
|
+
const errors = validatePricingRules(rules)
|
|
691
|
+
assert.equal(errors.get('0.conditions.0.from'), 'condDate')
|
|
692
|
+
assert.equal(errors.get('1.conditions.0'), 'condRange')
|
|
693
|
+
assert.equal(errors.has('2.conditions.0'), false)
|
|
694
|
+
assert.equal(errors.has('2.conditions.0.from'), false)
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
test('coerceConditions 规整数字字段且不改其余字段', () => {
|
|
698
|
+
// Given 输入框字符串形态 When 规整 Then weekdays days 与 monthDays 号段转数字,时段日期原样
|
|
699
|
+
const conditions = [
|
|
700
|
+
{ kind: 'weekdays', days: ['1', '5'] },
|
|
701
|
+
{ kind: 'monthDays', from: '26', to: '5' },
|
|
702
|
+
{ kind: 'dailyWindow', from: '09:00', to: '18:00' },
|
|
703
|
+
{ kind: 'dateRange', from: '2026-03-01', to: '2026-03-15' },
|
|
704
|
+
]
|
|
705
|
+
const coerced = coerceConditions(conditions)
|
|
706
|
+
assert.deepEqual(coerced[0], { kind: 'weekdays', days: [1, 5] })
|
|
707
|
+
assert.deepEqual(coerced[1], { kind: 'monthDays', from: 26, to: 5 })
|
|
708
|
+
assert.deepEqual(coerced[2], conditions[2])
|
|
709
|
+
assert.deepEqual(coerced[3], conditions[3])
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
test('coercePricingRules 连带规整条件', () => {
|
|
713
|
+
// Given 含条件规则 When POST 前规整 Then 价格与条件数字字段同时规整
|
|
714
|
+
const rules = [{
|
|
715
|
+
model: 'a/b', currency: '¥',
|
|
716
|
+
price: { input: '1', output: '0', cacheRead: '0', cacheWrite: '0' },
|
|
717
|
+
conditions: [{ kind: 'weekdays', days: ['1'] }, { kind: 'monthDays', from: '1', to: '31' }],
|
|
718
|
+
}]
|
|
719
|
+
const coerced = coercePricingRules(rules)
|
|
720
|
+
assert.deepEqual(coerced[0].conditions, [{ kind: 'weekdays', days: [1] }, { kind: 'monthDays', from: 1, to: 31 }])
|
|
721
|
+
assert.deepEqual(coerced[0].price, { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 })
|
|
722
|
+
})
|
package/test/collector.test.mjs
CHANGED
|
@@ -139,6 +139,22 @@ test('step/start 与 llm/retry-started 产生 request 标记样本', async () =>
|
|
|
139
139
|
}
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
+
test('step/start 观测起点:首个 usage 样本附 durationMs(start 到样本时刻)', async () => {
|
|
143
|
+
const { ctx, store } = liveCollector()
|
|
144
|
+
const start = local(2026, 8, 2, 10, 0)
|
|
145
|
+
const sampleAt = start + 6 * 1000 + 500
|
|
146
|
+
ctx.emit('session/event', session('s1'), ev('step/start', 1, start, { turn: 0, step: 0 }))
|
|
147
|
+
ctx.emit('session/event', session('s1'), ev('assistant/message', 2, sampleAt, {
|
|
148
|
+
turn: 0,
|
|
149
|
+
step: 0,
|
|
150
|
+
usage: usage(),
|
|
151
|
+
message: {},
|
|
152
|
+
}))
|
|
153
|
+
await tick()
|
|
154
|
+
const usageSample = store.samples.find((sample) => !sample.request)
|
|
155
|
+
assert.equal(usageSample.durationMs, 6500)
|
|
156
|
+
})
|
|
157
|
+
|
|
142
158
|
test('(session,turn,step) 去重:同键先到样本生效,重复报告吞掉', async () => {
|
|
143
159
|
const { ctx, store } = liveCollector()
|
|
144
160
|
const t = local(2026, 8, 2, 10, 0)
|
|
@@ -158,6 +174,55 @@ test('(session,turn,step) 去重:同键先到样本生效,重复报告吞掉', a
|
|
|
158
174
|
assert.equal(store.samples[0].inputTokens, 100)
|
|
159
175
|
})
|
|
160
176
|
|
|
177
|
+
test('无 step/start 起点的 usage 样本不带 durationMs', async () => {
|
|
178
|
+
const { ctx, store } = liveCollector()
|
|
179
|
+
const t = local(2026, 8, 2, 10, 0)
|
|
180
|
+
ctx.emit('session/event', session('s1'), ev('assistant/message', 1, t, {
|
|
181
|
+
turn: 0,
|
|
182
|
+
step: 0,
|
|
183
|
+
usage: usage(),
|
|
184
|
+
message: {},
|
|
185
|
+
}))
|
|
186
|
+
await tick()
|
|
187
|
+
assert.equal(store.samples.length, 1)
|
|
188
|
+
assert.equal(store.samples[0].durationMs, undefined)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('retry-started 重置起点:时长只含最终尝试', async () => {
|
|
192
|
+
const { ctx, store } = liveCollector()
|
|
193
|
+
const start = local(2026, 8, 2, 10, 0)
|
|
194
|
+
const retryAt = start + 60 * 1000
|
|
195
|
+
const sampleAt = retryAt + 5 * 1000
|
|
196
|
+
ctx.emit('session/event', session('s1'), ev('step/start', 1, start, { turn: 0, step: 0 }))
|
|
197
|
+
ctx.emit('session/event', session('s1'), ev('llm/retry-started', 2, retryAt, { turn: 0, step: 0 }))
|
|
198
|
+
ctx.emit('session/event', session('s1'), ev('assistant/message', 3, sampleAt, {
|
|
199
|
+
turn: 0,
|
|
200
|
+
step: 0,
|
|
201
|
+
usage: usage(),
|
|
202
|
+
message: {},
|
|
203
|
+
}))
|
|
204
|
+
await tick()
|
|
205
|
+
const usageSample = store.samples.find((sample) => !sample.request)
|
|
206
|
+
assert.equal(usageSample.durationMs, 5000)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('同时刻零时长不附 durationMs,request 标记样本不带时长', async () => {
|
|
210
|
+
const { ctx, store } = liveCollector()
|
|
211
|
+
const t = local(2026, 8, 2, 10, 0)
|
|
212
|
+
ctx.emit('session/event', session('s1'), ev('step/start', 1, t, { turn: 0, step: 0 }))
|
|
213
|
+
ctx.emit('session/event', session('s1'), ev('assistant/message', 2, t, {
|
|
214
|
+
turn: 0,
|
|
215
|
+
step: 0,
|
|
216
|
+
usage: usage(),
|
|
217
|
+
message: {},
|
|
218
|
+
}))
|
|
219
|
+
await tick()
|
|
220
|
+
assert.equal(store.samples.length, 2)
|
|
221
|
+
for (const sample of store.samples) {
|
|
222
|
+
assert.equal(sample.durationMs, undefined)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
161
226
|
test('不同 (turn,step) 各自成立,跨会话同键互不影响', async () => {
|
|
162
227
|
const { ctx, store } = liveCollector()
|
|
163
228
|
const t = local(2026, 8, 2, 10, 0)
|
|
@@ -313,6 +378,8 @@ test('采集侧 store 失败计入 recordFailures 且不抛断采集', async ()
|
|
|
313
378
|
await tick()
|
|
314
379
|
await tick()
|
|
315
380
|
assert.equal(collector.status().recordFailures, 2)
|
|
381
|
+
assert.equal(collector.status().log.length, 2)
|
|
382
|
+
assert.ok(collector.status().log.every((entry) => entry.kind === 'record' && typeof entry.time === 'number'))
|
|
316
383
|
assert.equal(store.samples.length, 0)
|
|
317
384
|
})
|
|
318
385
|
|
|
@@ -362,6 +429,28 @@ test('回扫:无边界会话全量重放,归因与游标写回', async () => {
|
|
|
362
429
|
assert.equal(collector.status().scannedSessions, 1)
|
|
363
430
|
})
|
|
364
431
|
|
|
432
|
+
test('回扫:重放路径同样附加 durationMs', async () => {
|
|
433
|
+
const start = local(2026, 8, 2, 14, 0)
|
|
434
|
+
const persistence = fakePersistence([{
|
|
435
|
+
id: 's1',
|
|
436
|
+
events: [
|
|
437
|
+
ev('request/context', 0, start, { provider: 'deepseek', model: 'chat' }),
|
|
438
|
+
ev('step/start', 1, start, { turn: 0, step: 0 }),
|
|
439
|
+
ev('assistant/message', 2, start + 7 * 1000, {
|
|
440
|
+
turn: 0,
|
|
441
|
+
step: 0,
|
|
442
|
+
usage: usage(),
|
|
443
|
+
message: { source: { provider: 'deepseek', model: 'chat' } },
|
|
444
|
+
}),
|
|
445
|
+
],
|
|
446
|
+
}])
|
|
447
|
+
const store = fakeStore()
|
|
448
|
+
const collector = new UsageCollector(fakeCtx({ persistence }), store)
|
|
449
|
+
await collector.backfill(persistence, fakeSessions())
|
|
450
|
+
const usageSample = store.samples.find((sample) => !sample.request)
|
|
451
|
+
assert.equal(usageSample.durationMs, 7000)
|
|
452
|
+
})
|
|
453
|
+
|
|
365
454
|
test('回扫:已入游标会话不再重扫', async () => {
|
|
366
455
|
const t = local(2026, 8, 2, 14, 0)
|
|
367
456
|
const persistence = fakePersistence([{ id: 's1', events: [ev('turn/end', 0, t)] }])
|
|
@@ -473,6 +562,9 @@ test('回扫:单会话失败不中断整轮且不进游标', async () => {
|
|
|
473
562
|
assert.equal(collector.status().error, undefined)
|
|
474
563
|
assert.equal(collector.status().skippedSessions, 1)
|
|
475
564
|
assert.equal(collector.status().done, 2)
|
|
565
|
+
assert.equal(collector.status().log.length, 1)
|
|
566
|
+
assert.equal(collector.status().log[0].kind, 'skipped')
|
|
567
|
+
assert.ok(collector.status().log[0].detail.startsWith('s2'))
|
|
476
568
|
})
|
|
477
569
|
|
|
478
570
|
test('回扫:record 失败使会话失败并保留游标重试机会', async () => {
|
|
@@ -590,6 +682,7 @@ test('status() 返回快照,外部修改不影响内部状态', async () => {
|
|
|
590
682
|
error: undefined,
|
|
591
683
|
recordFailures: 0,
|
|
592
684
|
skippedSessions: 0,
|
|
685
|
+
log: [],
|
|
593
686
|
})
|
|
594
687
|
await collector.backfill(persistence, fakeSessions())
|
|
595
688
|
const snapshot = collector.status()
|
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
// 双实现同源 parity 测试:client.js 顶层定价镜像函数与宿主 src/pricing.js 同输入同输出
|
|
2
2
|
// 向量表驱动:同一批向量喂双侧,deepEqual 双侧结果并对期望值断言
|
|
3
|
-
// client.js
|
|
3
|
+
// client.js 为经典 script bundle(禁 import/export),整文件 IIFE 书挡;经 client-eval 剥壳后整源求值,按顶层声明名收集
|
|
4
4
|
import { test } from 'node:test'
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
|
-
import {
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { dirname, join } from 'node:path'
|
|
6
|
+
import { CLIENT_BODY, DECLARATION_NAMES } from './client-eval.mjs'
|
|
9
7
|
|
|
10
8
|
import {
|
|
11
9
|
matchPrice as hostMatchPrice,
|
|
12
10
|
costOf as hostCostOf,
|
|
13
11
|
conditionMatches as hostConditionMatches,
|
|
12
|
+
isTwoSegmentModel as hostIsTwoSegmentModel,
|
|
14
13
|
UNIT_PER_MILLION as hostUnitPerMillion,
|
|
15
14
|
CURRENCIES as hostCurrencies,
|
|
16
15
|
CONDITION_KINDS as hostConditionKinds,
|
|
17
16
|
TOKENS_PER_MILLION as hostTokensPerMillion,
|
|
18
17
|
} from '../src/pricing.js'
|
|
19
18
|
|
|
20
|
-
const
|
|
21
|
-
const DECLARATION_NAMES = [
|
|
22
|
-
...new Set(
|
|
23
|
-
[...CLIENT_SOURCE.matchAll(/^(?:const|function|let) ([A-Za-z_$][\w$]*)/gm)].map((match) => match[1])
|
|
24
|
-
),
|
|
25
|
-
]
|
|
26
|
-
const core = new Function(`${CLIENT_SOURCE}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
|
|
19
|
+
const core = new Function(`${CLIENT_BODY}\nreturn { ${DECLARATION_NAMES.join(', ')} }`)()
|
|
27
20
|
|
|
28
21
|
const {
|
|
29
22
|
matchPrice: clientMatchPrice,
|
|
30
23
|
costOf: clientCostOf,
|
|
31
24
|
conditionMatches: clientConditionMatches,
|
|
25
|
+
isTwoSegmentModel: clientIsTwoSegmentModel,
|
|
32
26
|
UNIT_PER_MILLION: clientUnitPerMillion,
|
|
33
27
|
CURRENCIES: clientCurrencies,
|
|
34
28
|
CONDITION_KINDS: clientConditionKinds,
|
|
@@ -83,67 +77,112 @@ const MATCH_SUNDAY_1000 = [2026, 2, 15, 10, 0]
|
|
|
83
77
|
|
|
84
78
|
const MATCH_VECTORS = [
|
|
85
79
|
{
|
|
86
|
-
name: '
|
|
87
|
-
rules: [ruleOf('a', 1), ruleOf('b', 2)], model: 'b', date: MATCH_SUNDAY_1000, expected: priceOf(2),
|
|
80
|
+
name: '全名精确命中按数组序取首个',
|
|
81
|
+
rules: [ruleOf('x/a', 1), ruleOf('x/b', 2)], model: 'x/b', date: MATCH_SUNDAY_1000, expected: priceOf(2),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: '同全名多条取数组序第一条',
|
|
85
|
+
rules: [ruleOf('x/a', 1), ruleOf('x/a', 3)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: '模型名通配跨供应商命中',
|
|
89
|
+
rules: [ruleOf('*/a', 5)], model: 'zzz/a', date: MATCH_SUNDAY_1000, expected: priceOf(5),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: '供应商通配命中同供应商模型',
|
|
93
|
+
rules: [ruleOf('x/*', 5)], model: 'x/zzz', date: MATCH_SUNDAY_1000, expected: priceOf(5),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: '无精确回落全通配',
|
|
97
|
+
rules: [ruleOf('*/*', 5)], model: 'zzz/yyy', date: MATCH_SUNDAY_1000, expected: priceOf(5),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: '全名精确优先于全通配',
|
|
101
|
+
rules: [ruleOf('*/*', 5), ruleOf('x/a', 1)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: '匹配链:模型名通配优先于供应商通配',
|
|
105
|
+
rules: [ruleOf('x/*', 3), ruleOf('*/a', 2)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: '匹配链:供应商通配优先于全通配',
|
|
109
|
+
rules: [ruleOf('*/*', 4), ruleOf('x/*', 3)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(3),
|
|
88
110
|
},
|
|
89
111
|
{
|
|
90
|
-
name: '
|
|
91
|
-
rules: [ruleOf('
|
|
112
|
+
name: '模型名段允许含斜杠',
|
|
113
|
+
rules: [ruleOf('x/*', 1)], model: 'x/deepseek/deepseek-chat', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
92
114
|
},
|
|
93
115
|
{
|
|
94
|
-
name: '
|
|
95
|
-
rules: [ruleOf('
|
|
116
|
+
name: '请求裸名归 default 供应商',
|
|
117
|
+
rules: [ruleOf('default/a', 1), ruleOf('*/a', 2)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
96
118
|
},
|
|
97
119
|
{
|
|
98
|
-
name: '
|
|
99
|
-
rules: [ruleOf('
|
|
120
|
+
name: '请求首位斜杠不拆段归 default 供应商',
|
|
121
|
+
rules: [ruleOf('default//x', 1), ruleOf('*/x', 2)], model: '/x', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: '单段旧形态规则失效',
|
|
125
|
+
rules: [ruleOf('*', 5), ruleOf('a', 1)], model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: '供应商段不同不命中',
|
|
129
|
+
rules: [ruleOf('x/*', 1)], model: 'y/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: '模型段不同不命中',
|
|
133
|
+
rules: [ruleOf('*/a', 1)], model: 'x/b', date: MATCH_SUNDAY_1000, expected: null,
|
|
100
134
|
},
|
|
101
135
|
{
|
|
102
136
|
name: '多条件 AND 全过命中',
|
|
103
|
-
rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [0] }, { kind: 'dailyWindow', from: '00:00', to: '23:59' }])],
|
|
104
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
137
|
+
rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [0] }, { kind: 'dailyWindow', from: '00:00', to: '23:59' }])],
|
|
138
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
105
139
|
},
|
|
106
140
|
{
|
|
107
141
|
name: '一条件不过跳至下一条规则',
|
|
108
|
-
rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('a', 2, [])],
|
|
109
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
|
|
142
|
+
rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('x/a', 2, [])],
|
|
143
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: '高档条件不过逐层落低档',
|
|
147
|
+
rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('*/a', 5)],
|
|
148
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(5),
|
|
110
149
|
},
|
|
111
150
|
{
|
|
112
151
|
name: '缺 conditions 形状残缺跳过',
|
|
113
|
-
rules: [{ model: 'a', currency: '¥', price: priceOf(1) }],
|
|
114
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: null,
|
|
152
|
+
rules: [{ model: 'x/a', currency: '¥', price: priceOf(1) }],
|
|
153
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
115
154
|
},
|
|
116
155
|
{
|
|
117
156
|
name: '缺 price 形状残缺跳过',
|
|
118
|
-
rules: [{ model: 'a', currency: '¥', conditions: [] }],
|
|
119
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: null,
|
|
157
|
+
rules: [{ model: 'x/a', currency: '¥', conditions: [] }],
|
|
158
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
120
159
|
},
|
|
121
160
|
{
|
|
122
161
|
name: 'unit 非 perMillion 跳过',
|
|
123
|
-
rules: [{ ...ruleOf('a', 1), unit: 'perThousand' }],
|
|
124
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: null,
|
|
162
|
+
rules: [{ ...ruleOf('x/a', 1), unit: 'perThousand' }],
|
|
163
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
125
164
|
},
|
|
126
165
|
{
|
|
127
166
|
name: 'unit 缺省视为 perMillion 命中',
|
|
128
|
-
rules: [ruleOf('a', 1)],
|
|
129
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
167
|
+
rules: [ruleOf('x/a', 1)],
|
|
168
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
|
|
130
169
|
},
|
|
131
170
|
{
|
|
132
171
|
name: 'rules 非数组为 null',
|
|
133
|
-
rules: 'nope', model: 'a', date: MATCH_SUNDAY_1000, expected: null,
|
|
172
|
+
rules: 'nope', model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
134
173
|
},
|
|
135
174
|
{
|
|
136
175
|
name: 'model 非字符串为 null',
|
|
137
|
-
rules: [ruleOf('a', 1)], model: 7, date: MATCH_SUNDAY_1000, expected: null,
|
|
176
|
+
rules: [ruleOf('x/a', 1)], model: 7, date: MATCH_SUNDAY_1000, expected: null,
|
|
138
177
|
},
|
|
139
178
|
{
|
|
140
179
|
name: '非法时间戳为 null',
|
|
141
|
-
rules: [ruleOf('a', 1)], model: 'a', date: null, expected: null,
|
|
180
|
+
rules: [ruleOf('x/a', 1)], model: 'x/a', date: null, expected: null,
|
|
142
181
|
},
|
|
143
182
|
{
|
|
144
183
|
name: '条件不满足且无通配为 null',
|
|
145
|
-
rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }])],
|
|
146
|
-
model: 'a', date: MATCH_SUNDAY_1000, expected: null,
|
|
184
|
+
rules: [ruleOf('x/a', 1, [{ kind: 'weekdays', days: [1] }])],
|
|
185
|
+
model: 'x/a', date: MATCH_SUNDAY_1000, expected: null,
|
|
147
186
|
},
|
|
148
187
|
]
|
|
149
188
|
|
|
@@ -221,6 +260,30 @@ test(`matchPrice 双侧一致(${MATCH_VECTORS.length} 向量)`, () => {
|
|
|
221
260
|
}
|
|
222
261
|
})
|
|
223
262
|
|
|
263
|
+
const SEGMENT_VECTORS = [
|
|
264
|
+
{ name: '两段合法', model: 'a/b', expected: true },
|
|
265
|
+
{ name: '通配段合法', model: '*/*', expected: true },
|
|
266
|
+
{ name: '多级模型名合法', model: 'a/b/c', expected: true },
|
|
267
|
+
{ name: '单段非法', model: 'a', expected: false },
|
|
268
|
+
{ name: '通配单段非法', model: '*', expected: false },
|
|
269
|
+
{ name: '首位斜杠非法', model: '/b', expected: false },
|
|
270
|
+
{ name: '空串非法', model: '', expected: false },
|
|
271
|
+
{ name: '模型段空非法', model: 'a/', expected: false },
|
|
272
|
+
{ name: '段含前导空白非法', model: ' /b', expected: false },
|
|
273
|
+
{ name: '段含尾随空白非法', model: 'a/ ', expected: false },
|
|
274
|
+
{ name: '段内含空白非法', model: 'a /b', expected: false },
|
|
275
|
+
{ name: '通配段含空白非法', model: '* /b', expected: false },
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
test(`isTwoSegmentModel 双侧一致(${SEGMENT_VECTORS.length} 向量)`, () => {
|
|
279
|
+
for (const vector of SEGMENT_VECTORS) {
|
|
280
|
+
const host = hostIsTwoSegmentModel(vector.model)
|
|
281
|
+
const client = clientIsTwoSegmentModel(vector.model)
|
|
282
|
+
assert.deepEqual(client, host, vector.name)
|
|
283
|
+
assert.equal(host, vector.expected, vector.name)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
|
|
224
287
|
test(`costOf 双侧一致(${COST_VECTORS.length} 向量)`, () => {
|
|
225
288
|
for (const vector of COST_VECTORS) {
|
|
226
289
|
const host = hostCostOf(vector.price, vector.buckets)
|