@mzzsfy/dsh-usage-dash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ // 双实现同源 parity 测试:client.js 顶层定价镜像函数与宿主 src/pricing.js 同输入同输出
2
+ // 向量表驱动:同一批向量喂双侧,deepEqual 双侧结果并对期望值断言
3
+ // client.js 为非模块 script(bundle 求值形态,禁 import/export),整源求值后按顶层声明名收集
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { readFileSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { dirname, join } from 'node:path'
9
+
10
+ import {
11
+ matchPrice as hostMatchPrice,
12
+ costOf as hostCostOf,
13
+ conditionMatches as hostConditionMatches,
14
+ UNIT_PER_MILLION as hostUnitPerMillion,
15
+ CURRENCIES as hostCurrencies,
16
+ CONDITION_KINDS as hostConditionKinds,
17
+ TOKENS_PER_MILLION as hostTokensPerMillion,
18
+ } from '../src/pricing.js'
19
+
20
+ const CLIENT_SOURCE = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'client.js'), 'utf8')
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(', ')} }`)()
27
+
28
+ const {
29
+ matchPrice: clientMatchPrice,
30
+ costOf: clientCostOf,
31
+ conditionMatches: clientConditionMatches,
32
+ UNIT_PER_MILLION: clientUnitPerMillion,
33
+ CURRENCIES: clientCurrencies,
34
+ CONDITION_KINDS: clientConditionKinds,
35
+ TOKENS_PER_MILLION: clientTokensPerMillion,
36
+ } = core
37
+
38
+ // 本地时区固定时刻(2026-03-15 为周日):同一 Date 实例喂双侧
39
+ const SUNDAY_0930 = [2026, 2, 15, 9, 30]
40
+ const SUNDAY_1800 = [2026, 2, 15, 18, 0]
41
+ const SUNDAY_0900 = [2026, 2, 15, 9, 0]
42
+ const SUNDAY_2330 = [2026, 2, 15, 23, 30]
43
+ const SUNDAY_0300 = [2026, 2, 15, 3, 0]
44
+ const SUNDAY_1200 = [2026, 2, 15, 12, 0]
45
+ const MON_JAN15 = [2026, 0, 15, 12, 0]
46
+ const MON_JAN16 = [2026, 0, 16, 12, 0]
47
+ const MON_JAN26 = [2026, 0, 26, 12, 0]
48
+ const MON_JAN03 = [2026, 0, 3, 12, 0]
49
+ const FEB_30_ROLLS_TO_MAR02 = [2026, 1, 30, 12, 0]
50
+
51
+ const CONDITION_VECTORS = [
52
+ { name: 'dailyWindow from<to 命中', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_0930, expected: true },
53
+ { name: 'dailyWindow 含头不含尾', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_1800, expected: false },
54
+ { name: 'dailyWindow 含头', condition: { kind: 'dailyWindow', from: '09:00', to: '18:00' }, date: SUNDAY_0900, expected: true },
55
+ { name: 'dailyWindow 跨午夜夜段命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_2330, expected: true },
56
+ { name: 'dailyWindow 跨午夜凌晨命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_0300, expected: true },
57
+ { name: 'dailyWindow 跨午夜日间不命中', condition: { kind: 'dailyWindow', from: '22:00', to: '06:00' }, date: SUNDAY_1200, expected: false },
58
+ { name: 'dailyWindow from===to 全天', condition: { kind: 'dailyWindow', from: '00:00', to: '00:00' }, date: SUNDAY_1200, expected: true },
59
+ { name: 'dailyWindow 非法格式', condition: { kind: 'dailyWindow', from: 'abc', to: '06:00' }, date: SUNDAY_1200, expected: false },
60
+ { name: 'weekdays 命中周日', condition: { kind: 'weekdays', days: [0] }, date: SUNDAY_0930, expected: true },
61
+ { name: 'weekdays 空 days 不成立', condition: { kind: 'weekdays', days: [] }, date: SUNDAY_0930, expected: false },
62
+ { name: 'weekdays 未命中', condition: { kind: 'weekdays', days: [1] }, date: SUNDAY_0930, expected: false },
63
+ { name: 'weekdays days 非数组', condition: { kind: 'weekdays', days: 'x' }, date: SUNDAY_0930, expected: false },
64
+ { name: 'monthDays 双闭含端', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN15, expected: true },
65
+ { name: 'monthDays 界外', condition: { kind: 'monthDays', from: 1, to: 15 }, date: MON_JAN16, expected: false },
66
+ { name: 'monthDays 环绕起点命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN26, expected: true },
67
+ { name: 'monthDays 环绕中段不命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN15, expected: false },
68
+ { name: 'monthDays 环绕终点命中', condition: { kind: 'monthDays', from: 26, to: 5 }, date: MON_JAN03, expected: true },
69
+ { name: 'monthDays 非整数不成立', condition: { kind: 'monthDays', from: 1.5, to: 5 }, date: MON_JAN15, expected: false },
70
+ { name: 'dateRange 命中', condition: { kind: 'dateRange', from: '2026-01-01', to: '2026-01-31' }, date: MON_JAN15, expected: true },
71
+ { name: 'dateRange 倒序不成立', condition: { kind: 'dateRange', from: '2026-01-31', to: '2026-01-01' }, date: MON_JAN15, expected: false },
72
+ { name: 'dateRange 非零填充不成立', condition: { kind: 'dateRange', from: '2026-1-1', to: '2026-1-31' }, date: MON_JAN15, expected: false },
73
+ { name: 'dateRange 2 月 30 号滚月后不命中', condition: { kind: 'dateRange', from: '2026-02-01', to: '2026-02-28' }, date: FEB_30_ROLLS_TO_MAR02, expected: false },
74
+ { name: '未知 kind 不成立', condition: { kind: 'hourly', days: [0] }, date: SUNDAY_0930, expected: false },
75
+ { name: '非法 Date 不成立', condition: { kind: 'weekdays', days: [0] }, date: null, expected: false },
76
+ { name: 'condition 形状残缺不成立', condition: null, date: SUNDAY_0930, expected: false },
77
+ ]
78
+
79
+ const priceOf = (input) => ({ input, output: 0, cacheRead: 0, cacheWrite: 0 })
80
+ const ruleOf = (model, input, conditions = []) => ({ model, currency: '¥', price: priceOf(input), conditions })
81
+
82
+ const MATCH_SUNDAY_1000 = [2026, 2, 15, 10, 0]
83
+
84
+ const MATCH_VECTORS = [
85
+ {
86
+ name: '精确命中按数组序取首个',
87
+ rules: [ruleOf('a', 1), ruleOf('b', 2)], model: 'b', date: MATCH_SUNDAY_1000, expected: priceOf(2),
88
+ },
89
+ {
90
+ name: '同 model 多条取数组序第一条',
91
+ rules: [ruleOf('a', 1), ruleOf('a', 3)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
92
+ },
93
+ {
94
+ name: '无精确回落通配',
95
+ rules: [ruleOf('*', 5)], model: 'zzz', date: MATCH_SUNDAY_1000, expected: priceOf(5),
96
+ },
97
+ {
98
+ name: '精确优先于通配',
99
+ rules: [ruleOf('*', 5), ruleOf('a', 1)], model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
100
+ },
101
+ {
102
+ 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),
105
+ },
106
+ {
107
+ name: '一条件不过跳至下一条规则',
108
+ rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }]), ruleOf('a', 2, [])],
109
+ model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(2),
110
+ },
111
+ {
112
+ name: '缺 conditions 形状残缺跳过',
113
+ rules: [{ model: 'a', currency: '¥', price: priceOf(1) }],
114
+ model: 'a', date: MATCH_SUNDAY_1000, expected: null,
115
+ },
116
+ {
117
+ name: '缺 price 形状残缺跳过',
118
+ rules: [{ model: 'a', currency: '¥', conditions: [] }],
119
+ model: 'a', date: MATCH_SUNDAY_1000, expected: null,
120
+ },
121
+ {
122
+ name: 'unit 非 perMillion 跳过',
123
+ rules: [{ ...ruleOf('a', 1), unit: 'perThousand' }],
124
+ model: 'a', date: MATCH_SUNDAY_1000, expected: null,
125
+ },
126
+ {
127
+ name: 'unit 缺省视为 perMillion 命中',
128
+ rules: [ruleOf('a', 1)],
129
+ model: 'a', date: MATCH_SUNDAY_1000, expected: priceOf(1),
130
+ },
131
+ {
132
+ name: 'rules 非数组为 null',
133
+ rules: 'nope', model: 'a', date: MATCH_SUNDAY_1000, expected: null,
134
+ },
135
+ {
136
+ name: 'model 非字符串为 null',
137
+ rules: [ruleOf('a', 1)], model: 7, date: MATCH_SUNDAY_1000, expected: null,
138
+ },
139
+ {
140
+ name: '非法时间戳为 null',
141
+ rules: [ruleOf('a', 1)], model: 'a', date: null, expected: null,
142
+ },
143
+ {
144
+ name: '条件不满足且无通配为 null',
145
+ rules: [ruleOf('a', 1, [{ kind: 'weekdays', days: [1] }])],
146
+ model: 'a', date: MATCH_SUNDAY_1000, expected: null,
147
+ },
148
+ ]
149
+
150
+ const MILLION = 1000 * 1000
151
+ const COST_VECTORS = [
152
+ {
153
+ name: '四桶全带求和',
154
+ price: { input: 1, output: 2, cacheRead: 3, cacheWrite: 4 },
155
+ buckets: { inputTokens: MILLION, outputTokens: MILLION, cacheReadTokens: MILLION, cacheWriteTokens: MILLION },
156
+ expected: 10,
157
+ },
158
+ {
159
+ name: '缺桶按 0',
160
+ price: { input: 1, output: 2, cacheRead: 3, cacheWrite: 4 },
161
+ buckets: { inputTokens: MILLION },
162
+ expected: 1,
163
+ },
164
+ {
165
+ name: '非有限 token 按 0',
166
+ price: { input: 1, output: 2, cacheRead: 3, cacheWrite: 4 },
167
+ buckets: { inputTokens: Number.NaN, outputTokens: MILLION, cacheReadTokens: Number.POSITIVE_INFINITY, cacheWriteTokens: 0 },
168
+ expected: 2,
169
+ },
170
+ {
171
+ name: '非有限单价按 0',
172
+ price: { input: 1, output: Number.POSITIVE_INFINITY, cacheRead: Number.NaN, cacheWrite: 4 },
173
+ buckets: { inputTokens: MILLION, outputTokens: MILLION, cacheReadTokens: MILLION, cacheWriteTokens: 0 },
174
+ expected: 1,
175
+ },
176
+ {
177
+ name: '零价为 0',
178
+ price: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
179
+ buckets: { inputTokens: MILLION, outputTokens: MILLION, cacheReadTokens: MILLION, cacheWriteTokens: MILLION },
180
+ expected: 0,
181
+ },
182
+ {
183
+ name: '原始浮点不圆整',
184
+ price: { input: 1, output: 0, cacheRead: 0, cacheWrite: 0 },
185
+ buckets: { inputTokens: 3 },
186
+ expected: 3 / MILLION,
187
+ },
188
+ ]
189
+
190
+ test('镜像常量与宿主逐项一致', () => {
191
+ assert.equal(clientUnitPerMillion, hostUnitPerMillion)
192
+ assert.deepEqual(clientCurrencies, hostCurrencies)
193
+ assert.deepEqual(clientConditionKinds, hostConditionKinds)
194
+ assert.equal(clientTokensPerMillion, hostTokensPerMillion)
195
+ })
196
+
197
+ // 向量总量下限锁定:S14 要求 ≥25 组,防止后续裁剪 quietly 缩水
198
+ const VECTOR_TOTAL = CONDITION_VECTORS.length + MATCH_VECTORS.length + COST_VECTORS.length
199
+
200
+ test('parity 向量总量不低于 25 组', () => {
201
+ assert.ok(VECTOR_TOTAL >= 25, `向量总量 ${VECTOR_TOTAL} 不足 25`)
202
+ })
203
+
204
+ test(`conditionMatches 双侧一致(${CONDITION_VECTORS.length} 向量)`, () => {
205
+ for (const vector of CONDITION_VECTORS) {
206
+ const date = vector.date ? new Date(...vector.date) : new Date(Number.NaN)
207
+ const host = hostConditionMatches(vector.condition, date)
208
+ const client = clientConditionMatches(vector.condition, date)
209
+ assert.deepEqual(client, host, vector.name)
210
+ assert.equal(host, vector.expected, vector.name)
211
+ }
212
+ })
213
+
214
+ test(`matchPrice 双侧一致(${MATCH_VECTORS.length} 向量)`, () => {
215
+ for (const vector of MATCH_VECTORS) {
216
+ const date = vector.date ? new Date(...vector.date) : new Date(Number.NaN)
217
+ const host = hostMatchPrice(vector.rules, vector.model, date)
218
+ const client = clientMatchPrice(vector.rules, vector.model, date)
219
+ assert.deepEqual(client, host, vector.name)
220
+ assert.deepEqual(host, vector.expected, vector.name)
221
+ }
222
+ })
223
+
224
+ test(`costOf 双侧一致(${COST_VECTORS.length} 向量)`, () => {
225
+ for (const vector of COST_VECTORS) {
226
+ const host = hostCostOf(vector.price, vector.buckets)
227
+ const client = clientCostOf(vector.price, vector.buckets)
228
+ assert.deepEqual(client, host, vector.name)
229
+ assert.deepEqual(host, vector.expected, vector.name)
230
+ }
231
+ })
@@ -0,0 +1,285 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ CURRENCIES,
6
+ CONDITION_KINDS,
7
+ TOKENS_PER_MILLION,
8
+ UNIT_PER_MILLION,
9
+ conditionMatches,
10
+ costOf,
11
+ matchPrice,
12
+ } from '../src/pricing.js'
13
+
14
+ // 锚定日期一律本地时区构造,期望只依赖本地分量,与运行机时区无关
15
+ const SUNDAY = new Date(2026, 8, 6)
16
+ const MONDAY = new Date(2026, 8, 7)
17
+ const SATURDAY = new Date(2026, 8, 12)
18
+
19
+ const localTime = (day, hour, minute = 0) => new Date(2026, 8, day, hour, minute)
20
+
21
+ const makeRule = (overrides = {}) => ({
22
+ model: '*',
23
+ unit: UNIT_PER_MILLION,
24
+ currency: '¥',
25
+ price: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 1 },
26
+ conditions: [],
27
+ ...overrides,
28
+ })
29
+
30
+ // 浮点比较容差
31
+ const EPSILON = 1e-9
32
+
33
+ test('常量为文档定值', () => {
34
+ assert.equal(UNIT_PER_MILLION, 'perMillion')
35
+ assert.deepEqual(CURRENCIES, ['¥', '$'])
36
+ assert.deepEqual(CONDITION_KINDS, ['dailyWindow', 'weekdays', 'monthDays', 'dateRange'])
37
+ assert.equal(TOKENS_PER_MILLION, 1000 * 1000)
38
+ })
39
+
40
+ test('锚定日期星期与本地分量自检', () => {
41
+ // Given 本地构造的锚定日期 When 读本地分量 Then 与写死期望一致,防锚点拍脑袋
42
+ assert.equal(SUNDAY.getDay(), 0)
43
+ assert.equal(MONDAY.getDay(), 1)
44
+ assert.equal(SATURDAY.getDay(), 6)
45
+ assert.equal(localTime(6, 23, 30).getHours(), 23)
46
+ assert.equal(localTime(6, 23, 30).getMinutes(), 30)
47
+ })
48
+
49
+ test('无条件规则恒命中', () => {
50
+ // Given conditions 空数组规则 When 匹配其模型 Then 返回该规则 price
51
+ const target = makeRule({ model: 'a/b' })
52
+ assert.deepEqual(matchPrice([target], 'a/b', SUNDAY), target.price)
53
+ })
54
+
55
+ test('精确模型优先于通配规则', () => {
56
+ // Given 精确规则与 '*' 规则并存 When 匹配 Then 取精确规则 price
57
+ const exact = makeRule({ model: 'a/b', price: { input: 5 } })
58
+ const wildcard = makeRule({ model: '*', price: { input: 1 } })
59
+ assert.deepEqual(matchPrice([wildcard, exact], 'a/b', SUNDAY), exact.price)
60
+ })
61
+
62
+ test('精确子集按数组序取首个命中', () => {
63
+ // Given 两条同模型无条件规则 When 匹配 Then 取数组序第一
64
+ const first = makeRule({ model: 'a/b', price: { input: 1 } })
65
+ const second = makeRule({ model: 'a/b', price: { input: 2 } })
66
+ assert.deepEqual(matchPrice([first, second], 'a/b', SUNDAY), first.price)
67
+ })
68
+
69
+ test('精确子集全不命中回落通配子集', () => {
70
+ // Given 精确规则条件不满足、'*' 规则无条件 When 匹配 Then 回落 '*' 而非 null
71
+ const exact = makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })
72
+ const wildcard = makeRule({ model: '*', price: { input: 3 } })
73
+ assert.deepEqual(matchPrice([exact, wildcard], 'a/b', SUNDAY), wildcard.price)
74
+ })
75
+
76
+ test('通配子集按数组序取首个命中', () => {
77
+ // Given 首条 '*' 条件满足、次条无条件 When 匹配 Then 取首条
78
+ const first = makeRule({ model: '*', conditions: [{ kind: 'weekdays', days: [0] }], price: { input: 1 } })
79
+ const second = makeRule({ model: '*', price: { input: 2 } })
80
+ assert.deepEqual(matchPrice([first, second], 'a/b', SUNDAY), first.price)
81
+ })
82
+
83
+ test('无精确子集直接查通配子集', () => {
84
+ // Given 仅 '*' 规则 When 匹配未列出模型 Then 命中通配
85
+ const wildcard = makeRule({ model: '*', price: { input: 4 } })
86
+ assert.deepEqual(matchPrice([wildcard], 'other/c', SUNDAY), wildcard.price)
87
+ })
88
+
89
+ test('两子集全不命中返回 null 计 unpriced', () => {
90
+ // Given 仅一条精确规则且条件不满足 When 匹配本模型与陌生模型 Then 均为 null
91
+ const rules = [makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })]
92
+ assert.equal(matchPrice(rules, 'a/b', SUNDAY), null)
93
+ assert.equal(matchPrice(rules, 'x/y', SUNDAY), null)
94
+ })
95
+
96
+ test('多条件 AND 一过一不过整规则不命中', () => {
97
+ // Given 周日 + 10:00~12:00 两条件 When 周日 09:00 与 11:00 Then 前者 null 后者命中
98
+ const target = makeRule({
99
+ model: 'a/b',
100
+ conditions: [
101
+ { kind: 'weekdays', days: [0] },
102
+ { kind: 'dailyWindow', from: '10:00', to: '12:00' },
103
+ ],
104
+ })
105
+ assert.equal(matchPrice([target], 'a/b', localTime(6, 9)), null)
106
+ assert.deepEqual(matchPrice([target], 'a/b', localTime(6, 11)), target.price)
107
+ })
108
+
109
+ test('规则形状残缺跳过不崩溃', () => {
110
+ // Given 缺 model/缺 price/非法 unit/缺 conditions 的规则在前 When 匹配 Then 跳过并命中合法规则
111
+ const good = makeRule({ model: 'a/b' })
112
+ const rules = [
113
+ { unit: UNIT_PER_MILLION, price: { input: 9 }, conditions: [] },
114
+ { model: 'a/b', conditions: [] },
115
+ makeRule({ model: 'a/b', unit: 'perKilo' }),
116
+ makeRule({ model: 'a/b', conditions: undefined }),
117
+ good,
118
+ ]
119
+ assert.deepEqual(matchPrice(rules, 'a/b', SUNDAY), good.price)
120
+ })
121
+
122
+ test('rules 非数组或 timestamp 非法返回 null', () => {
123
+ // Given 非法入参 When matchPrice Then 返回 null 不抛错
124
+ assert.equal(matchPrice([], 'a/b', SUNDAY), null)
125
+ assert.equal(matchPrice(undefined, 'a/b', SUNDAY), null)
126
+ assert.equal(matchPrice([makeRule()], 'a/b', Number.NaN), null)
127
+ })
128
+
129
+ test('dailyWindow from<to 含头不含尾', () => {
130
+ // Given 10:00~12:00 When 逐时刻判定 Then 10:00 与 11:59 命中,09:59 与 12:00 不命中
131
+ const condition = { kind: 'dailyWindow', from: '10:00', to: '12:00' }
132
+ assert.equal(conditionMatches(condition, localTime(6, 10)), true)
133
+ assert.equal(conditionMatches(condition, localTime(6, 11, 59)), true)
134
+ assert.equal(conditionMatches(condition, localTime(6, 9, 59)), false)
135
+ assert.equal(conditionMatches(condition, localTime(6, 12)), false)
136
+ })
137
+
138
+ test('dailyWindow 跨午夜窗口按本地分量命中', () => {
139
+ // Given 22:00~02:00 When 本地 23:30 与次日 01:00 Then 命中;本地正午不命中
140
+ const condition = { kind: 'dailyWindow', from: '22:00', to: '02:00' }
141
+ assert.equal(conditionMatches(condition, localTime(6, 23, 30)), true)
142
+ assert.equal(conditionMatches(condition, new Date(2026, 8, 7, 1)), true)
143
+ assert.equal(conditionMatches(condition, localTime(6, 12)), false)
144
+ })
145
+
146
+ test('dailyWindow from===to 全天生效', () => {
147
+ // Given 10:00~10:00 When 判定 00:00/10:00/23:59 Then 全部命中
148
+ const condition = { kind: 'dailyWindow', from: '10:00', to: '10:00' }
149
+ assert.equal(conditionMatches(condition, localTime(6, 0)), true)
150
+ assert.equal(conditionMatches(condition, localTime(6, 10)), true)
151
+ assert.equal(conditionMatches(condition, localTime(6, 23, 59)), true)
152
+ })
153
+
154
+ test('dailyWindow 分钟级评估', () => {
155
+ // Given 10:05~10:10 When 判定 10:05/10:09/10:10 Then 前两者命中
156
+ const condition = { kind: 'dailyWindow', from: '10:05', to: '10:10' }
157
+ assert.equal(conditionMatches(condition, localTime(6, 10, 5)), true)
158
+ assert.equal(conditionMatches(condition, localTime(6, 10, 9)), true)
159
+ assert.equal(conditionMatches(condition, localTime(6, 10, 10)), false)
160
+ })
161
+
162
+ test('weekdays 集合按 getDay 命中', () => {
163
+ // Given days [0,6] When 判定锚定周日/周六/周一 Then 前两者命中
164
+ const weekend = { kind: 'weekdays', days: [0, 6] }
165
+ assert.equal(conditionMatches(weekend, SUNDAY), true)
166
+ assert.equal(conditionMatches(weekend, SATURDAY), true)
167
+ assert.equal(conditionMatches(weekend, MONDAY), false)
168
+ })
169
+
170
+ test('weekdays 空数组条件不成立', () => {
171
+ assert.equal(conditionMatches({ kind: 'weekdays', days: [] }, SUNDAY), false)
172
+ })
173
+
174
+ test('monthDays 正向段双闭含单日', () => {
175
+ // Given 1~15 When 判定 1/15/16 号 Then 双闭;Given 5~5 When 判定 5/6 号 Then 单日
176
+ const span = { kind: 'monthDays', from: 1, to: 15 }
177
+ assert.equal(conditionMatches(span, new Date(2026, 8, 1)), true)
178
+ assert.equal(conditionMatches(span, new Date(2026, 8, 15)), true)
179
+ assert.equal(conditionMatches(span, new Date(2026, 8, 16)), false)
180
+ const single = { kind: 'monthDays', from: 5, to: 5 }
181
+ assert.equal(conditionMatches(single, new Date(2026, 8, 5)), true)
182
+ assert.equal(conditionMatches(single, new Date(2026, 8, 6)), false)
183
+ })
184
+
185
+ test('monthDays 单日 31 号在 2 月自然不触发', () => {
186
+ // Given 31~31 When 判定 1 月 31 号与 2 月末 Then 前者命中;2 月无 31 号,该月任意日不命中
187
+ const condition = { kind: 'monthDays', from: 31, to: 31 }
188
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
189
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 28)), false)
190
+ })
191
+
192
+ test('monthDays 26~25 跨月环绕覆盖全月', () => {
193
+ // Given 26~25 账单周期 When 判定 26/31/1/25/15 号 Then 26..31 并 1..25 覆盖全月,含 15
194
+ const condition = { kind: 'monthDays', from: 26, to: 25 }
195
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 26)), true)
196
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
197
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), true)
198
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 25)), true)
199
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), true)
200
+ })
201
+
202
+ test('monthDays 27~10 环绕存在不命中间隙', () => {
203
+ // Given 27~10 When 判定 27/31/1/10 与 15/26 号 Then 前四命中,间隙内不命中
204
+ const condition = { kind: 'monthDays', from: 27, to: 10 }
205
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 27)), true)
206
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
207
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), true)
208
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 10)), true)
209
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), false)
210
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 26)), false)
211
+ })
212
+
213
+ test('dateRange 双闭区间', () => {
214
+ // Given 2026-01-01~2026-01-31 When 判定首末日与界外 Then 端点命中,两侧不命中
215
+ const condition = { kind: 'dateRange', from: '2026-01-01', to: '2026-01-31' }
216
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 1)), true)
217
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), true)
218
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 31)), true)
219
+ assert.equal(conditionMatches(condition, new Date(2025, 11, 31)), false)
220
+ assert.equal(conditionMatches(condition, new Date(2026, 1, 1)), false)
221
+ })
222
+
223
+ test('dateRange 倒序属配置错误不成立', () => {
224
+ const condition = { kind: 'dateRange', from: '2026-02-01', to: '2026-01-01' }
225
+ assert.equal(conditionMatches(condition, new Date(2026, 0, 15)), false)
226
+ })
227
+
228
+ test('未知 kind 与非法日期不成立', () => {
229
+ // Given 未知 kind/null 条件/非法 Date When conditionMatches Then 均不成立不抛错
230
+ assert.equal(conditionMatches({ kind: 'lunarPhase' }, SUNDAY), false)
231
+ assert.equal(conditionMatches(null, SUNDAY), false)
232
+ assert.equal(conditionMatches({ kind: 'weekdays', days: [0] }, new Date(Number.NaN)), false)
233
+ })
234
+
235
+ test('同值 Date 与毫秒数结果一致', () => {
236
+ // Given 命中与不命中规则各一 When 分别传 Date 与 getTime() Then 两次结果一致
237
+ const hit = makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [0] }] })
238
+ const miss = makeRule({ model: 'a/b', conditions: [{ kind: 'weekdays', days: [1] }] })
239
+ const stamp = localTime(6, 10, 30)
240
+ assert.deepEqual(matchPrice([hit], 'a/b', stamp), matchPrice([hit], 'a/b', stamp.getTime()))
241
+ assert.deepEqual(matchPrice([miss], 'a/b', stamp), matchPrice([miss], 'a/b', stamp.getTime()))
242
+ })
243
+
244
+ test('四桶计价混合 token 算术', () => {
245
+ // Given 四桶单价与部分桶 token When costOf Then Σ(桶token×单价)/百万
246
+ const price = { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 1 }
247
+ const buckets = {
248
+ inputTokens: TOKENS_PER_MILLION,
249
+ outputTokens: TOKENS_PER_MILLION / 2,
250
+ cacheReadTokens: 0,
251
+ cacheWriteTokens: 0,
252
+ }
253
+ assert.ok(Math.abs(costOf(price, buckets) - 5) < EPSILON)
254
+ })
255
+
256
+ test('四桶全非零按权重求和', () => {
257
+ // Given 四桶单价 1/2/3/4 且各桶百万 token When costOf Then 权重和 10
258
+ const price = { input: 1, output: 2, cacheRead: 3, cacheWrite: 4 }
259
+ const buckets = {
260
+ inputTokens: TOKENS_PER_MILLION,
261
+ outputTokens: TOKENS_PER_MILLION,
262
+ cacheReadTokens: TOKENS_PER_MILLION,
263
+ cacheWriteTokens: TOKENS_PER_MILLION,
264
+ }
265
+ assert.ok(Math.abs(costOf(price, buckets) - (1 + 2 + 3 + 4)) < EPSILON)
266
+ })
267
+
268
+ test('全零 token 费用为 0', () => {
269
+ const buckets = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
270
+ assert.equal(costOf({ input: 2, output: 6, cacheRead: 0.5, cacheWrite: 1 }, buckets), 0)
271
+ })
272
+
273
+ test('缺桶或价格字段按 0 计', () => {
274
+ // Given buckets 缺部分桶、price 只配部分单价 When costOf Then 只计两侧齐备的桶
275
+ assert.ok(Math.abs(costOf({ input: 2 }, { inputTokens: TOKENS_PER_MILLION, outputTokens: TOKENS_PER_MILLION / 2 }) - 2) < EPSILON)
276
+ assert.equal(costOf({ input: 2 }, {}), 0)
277
+ assert.equal(costOf({}, { inputTokens: TOKENS_PER_MILLION }), 0)
278
+ })
279
+
280
+ test('费用保留原始浮点不圆整', () => {
281
+ // Given 0.1+0.2 类单价 When costOf Then 得原始浮点和,不 toFixed 圆整
282
+ const price = { input: 0.1, output: 0.2 }
283
+ const buckets = { inputTokens: TOKENS_PER_MILLION, outputTokens: TOKENS_PER_MILLION }
284
+ assert.ok(Math.abs(costOf(price, buckets) - 0.3) < EPSILON)
285
+ })