@robhowley/pi-openrouter 0.9.0 → 0.9.1
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 +39 -4
- package/extensions/openrouter/__tests__/cache.test.ts +769 -0
- package/extensions/openrouter/__tests__/client.test.ts +333 -15
- package/extensions/openrouter/__tests__/commands.test.ts +816 -0
- package/extensions/openrouter/__tests__/fixtures.ts +140 -1
- package/extensions/openrouter/__tests__/format.test.ts +19 -0
- package/extensions/openrouter/__tests__/hooks.test.ts +276 -0
- package/extensions/openrouter/__tests__/index.test.ts +112 -363
- package/extensions/openrouter/__tests__/local-usage.test.ts +777 -0
- package/extensions/openrouter/__tests__/normalizers.test.ts +288 -0
- package/extensions/openrouter/__tests__/overlay.test.ts +225 -0
- package/extensions/openrouter/__tests__/session-state.test.ts +233 -0
- package/extensions/openrouter/__tests__/session.test.ts +44 -43
- package/extensions/openrouter/account-client.ts +11 -61
- package/extensions/openrouter/cache.ts +203 -91
- package/extensions/openrouter/client.ts +49 -3
- package/extensions/openrouter/commands.ts +555 -0
- package/extensions/openrouter/format.ts +7 -4
- package/extensions/openrouter/hooks.ts +229 -0
- package/extensions/openrouter/index.ts +13 -990
- package/extensions/openrouter/local-usage.ts +145 -22
- package/extensions/openrouter/models/__tests__/cache.test.ts +63 -2
- package/extensions/openrouter/models/__tests__/mapper.test.ts +29 -0
- package/extensions/openrouter/models/__tests__/override-commands.test.ts +668 -0
- package/extensions/openrouter/models/__tests__/sync.test.ts +156 -4
- package/extensions/openrouter/models/cache.ts +27 -2
- package/extensions/openrouter/models/mapper.ts +35 -69
- package/extensions/openrouter/models/override-commands.ts +434 -0
- package/extensions/openrouter/models/skip-hints.ts +19 -0
- package/extensions/openrouter/models/sync.ts +22 -10
- package/extensions/openrouter/models/types.ts +2 -1
- package/extensions/openrouter/normalizers.ts +128 -0
- package/extensions/openrouter/overlay.ts +19 -8
- package/extensions/openrouter/session-state.ts +110 -0
- package/extensions/openrouter/session.ts +16 -0
- package/extensions/openrouter/types.ts +28 -9
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
|
|
3
|
+
import type { GetCurrentKeyData, ListData } from '@openrouter/sdk/models/operations/index.js';
|
|
4
|
+
import {
|
|
5
|
+
normalizeOpenRouterModel,
|
|
6
|
+
normalizeSdkKeyMetadata,
|
|
7
|
+
sdkModelToOpenRouterModel,
|
|
8
|
+
} from '../normalizers.js';
|
|
9
|
+
import { createValidModel } from './fixtures.js';
|
|
10
|
+
|
|
11
|
+
function createSdkModel(overrides: Partial<SDKModel> = {}): SDKModel {
|
|
12
|
+
return {
|
|
13
|
+
architecture: {
|
|
14
|
+
inputModalities: ['text'],
|
|
15
|
+
modality: 'text',
|
|
16
|
+
outputModalities: ['text'],
|
|
17
|
+
},
|
|
18
|
+
canonicalSlug: 'test/model',
|
|
19
|
+
contextLength: 128000,
|
|
20
|
+
created: 0,
|
|
21
|
+
defaultParameters: null,
|
|
22
|
+
id: 'test/model',
|
|
23
|
+
links: {} as SDKModel['links'],
|
|
24
|
+
name: 'Test Model',
|
|
25
|
+
perRequestLimits: null,
|
|
26
|
+
pricing: {
|
|
27
|
+
prompt: '0.0000005',
|
|
28
|
+
completion: '0.0000015',
|
|
29
|
+
},
|
|
30
|
+
supportedParameters: [],
|
|
31
|
+
topProvider: {
|
|
32
|
+
isModerated: false,
|
|
33
|
+
},
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createCurrentKeyData(overrides: Partial<GetCurrentKeyData> = {}): GetCurrentKeyData {
|
|
39
|
+
return {
|
|
40
|
+
byokUsage: 0,
|
|
41
|
+
byokUsageDaily: 0,
|
|
42
|
+
byokUsageMonthly: 0,
|
|
43
|
+
byokUsageWeekly: 0,
|
|
44
|
+
creatorUserId: null,
|
|
45
|
+
includeByokInLimit: true,
|
|
46
|
+
isFreeTier: false,
|
|
47
|
+
isManagementKey: false,
|
|
48
|
+
isProvisioningKey: false,
|
|
49
|
+
label: 'sk-or-v1-current',
|
|
50
|
+
limit: 100,
|
|
51
|
+
limitRemaining: 40,
|
|
52
|
+
limitReset: 'monthly',
|
|
53
|
+
rateLimit: {
|
|
54
|
+
interval: 'day',
|
|
55
|
+
note: 'deprecated',
|
|
56
|
+
requests: -1,
|
|
57
|
+
},
|
|
58
|
+
usage: 60,
|
|
59
|
+
usageDaily: 0,
|
|
60
|
+
usageMonthly: 0,
|
|
61
|
+
usageWeekly: 0,
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createListKeyData(overrides: Partial<ListData> = {}): ListData {
|
|
67
|
+
return {
|
|
68
|
+
byokUsage: 0,
|
|
69
|
+
byokUsageDaily: 0,
|
|
70
|
+
byokUsageMonthly: 0,
|
|
71
|
+
byokUsageWeekly: 0,
|
|
72
|
+
createdAt: '2026-05-22T00:00:00.000Z',
|
|
73
|
+
creatorUserId: null,
|
|
74
|
+
disabled: false,
|
|
75
|
+
hash: 'hash-123',
|
|
76
|
+
includeByokInLimit: true,
|
|
77
|
+
label: 'sk-or-v1-list',
|
|
78
|
+
limit: 100,
|
|
79
|
+
limitRemaining: 40,
|
|
80
|
+
limitReset: 'monthly',
|
|
81
|
+
name: 'Workspace Key',
|
|
82
|
+
updatedAt: null,
|
|
83
|
+
usage: 60,
|
|
84
|
+
usageDaily: 0,
|
|
85
|
+
usageMonthly: 0,
|
|
86
|
+
usageWeekly: 0,
|
|
87
|
+
workspaceId: 'ws-1',
|
|
88
|
+
...overrides,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('sdkModelToOpenRouterModel', () => {
|
|
93
|
+
it('normalizes SDK camelCase fields into canonical snake_case model shape', () => {
|
|
94
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
95
|
+
createSdkModel({
|
|
96
|
+
architecture: {
|
|
97
|
+
inputModalities: ['text', 'image'],
|
|
98
|
+
modality: 'text',
|
|
99
|
+
outputModalities: ['text'],
|
|
100
|
+
},
|
|
101
|
+
contextLength: null,
|
|
102
|
+
perRequestLimits: { completionTokens: 0, promptTokens: 123 },
|
|
103
|
+
pricing: {
|
|
104
|
+
prompt: '0.0000005',
|
|
105
|
+
completion: '0.0000015',
|
|
106
|
+
inputCacheRead: undefined,
|
|
107
|
+
inputCacheWrite: undefined,
|
|
108
|
+
},
|
|
109
|
+
supportedParameters: ['reasoning'],
|
|
110
|
+
topProvider: {
|
|
111
|
+
contextLength: null,
|
|
112
|
+
isModerated: false,
|
|
113
|
+
maxCompletionTokens: null,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(normalized).toEqual({
|
|
119
|
+
id: 'test/model',
|
|
120
|
+
name: 'Test Model',
|
|
121
|
+
architecture: {
|
|
122
|
+
input_modalities: ['text', 'image'],
|
|
123
|
+
output_modalities: ['text'],
|
|
124
|
+
},
|
|
125
|
+
context_length: 0,
|
|
126
|
+
pricing: {
|
|
127
|
+
prompt: '0.0000005',
|
|
128
|
+
completion: '0.0000015',
|
|
129
|
+
input_cache_read: '0',
|
|
130
|
+
input_cache_write: '0',
|
|
131
|
+
},
|
|
132
|
+
supported_parameters: ['reasoning'],
|
|
133
|
+
top_provider: {
|
|
134
|
+
context_length: 0,
|
|
135
|
+
max_completion_tokens: 0,
|
|
136
|
+
},
|
|
137
|
+
per_request_limits: {
|
|
138
|
+
completion_tokens: 0,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('omits optional provider/request blocks when the SDK omits them', () => {
|
|
144
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
145
|
+
createSdkModel({
|
|
146
|
+
topProvider: null as unknown as SDKModel['topProvider'],
|
|
147
|
+
perRequestLimits: null,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(normalized).not.toHaveProperty('top_provider');
|
|
152
|
+
expect(normalized).not.toHaveProperty('per_request_limits');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('leaves canonical models unchanged when normalization is a no-op', () => {
|
|
156
|
+
const model = createValidModel({
|
|
157
|
+
top_provider: { context_length: 64000, max_completion_tokens: 8192 },
|
|
158
|
+
per_request_limits: { completion_tokens: 4096 },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(normalizeOpenRouterModel(model)).toBe(model);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('guards against null architecture without crashing', () => {
|
|
165
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
166
|
+
createSdkModel({
|
|
167
|
+
architecture: null as unknown as SDKModel['architecture'],
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(normalized).toMatchObject({
|
|
172
|
+
id: 'test/model',
|
|
173
|
+
name: 'Test Model',
|
|
174
|
+
context_length: 128000,
|
|
175
|
+
pricing: {
|
|
176
|
+
prompt: '0.0000005',
|
|
177
|
+
completion: '0.0000015',
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
expect(normalized).not.toHaveProperty('architecture');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('guards against null pricing without pretending the model is free', () => {
|
|
184
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
185
|
+
createSdkModel({
|
|
186
|
+
pricing: null as unknown as SDKModel['pricing'],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(normalized).toMatchObject({
|
|
191
|
+
id: 'test/model',
|
|
192
|
+
name: 'Test Model',
|
|
193
|
+
context_length: 128000,
|
|
194
|
+
});
|
|
195
|
+
expect(normalized).not.toHaveProperty('pricing');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('guards against undefined architecture without crashing', () => {
|
|
199
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
200
|
+
createSdkModel({
|
|
201
|
+
architecture: undefined as unknown as SDKModel['architecture'],
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(normalized).not.toHaveProperty('architecture');
|
|
206
|
+
expect(normalized.pricing).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('guards against undefined pricing without crashing', () => {
|
|
210
|
+
const normalized = sdkModelToOpenRouterModel(
|
|
211
|
+
createSdkModel({
|
|
212
|
+
pricing: undefined as unknown as SDKModel['pricing'],
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(normalized).not.toHaveProperty('pricing');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('normalizeSdkKeyMetadata', () => {
|
|
221
|
+
it('preserves zero limits and zero remaining distinctly from absent values', () => {
|
|
222
|
+
const normalized = normalizeSdkKeyMetadata(
|
|
223
|
+
createCurrentKeyData({
|
|
224
|
+
includeByokInLimit: false,
|
|
225
|
+
limit: 0,
|
|
226
|
+
limitRemaining: 0,
|
|
227
|
+
limitReset: 'DAILY',
|
|
228
|
+
usage: 0,
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(normalized).toMatchObject({
|
|
233
|
+
name: 'sk-or-v1-current',
|
|
234
|
+
label: 'sk-or-v1-current',
|
|
235
|
+
used: 0,
|
|
236
|
+
limit: 0,
|
|
237
|
+
remaining: 0,
|
|
238
|
+
byok: 'excl',
|
|
239
|
+
resetCadence: 'daily',
|
|
240
|
+
hash: 'unknown',
|
|
241
|
+
disabled: false,
|
|
242
|
+
});
|
|
243
|
+
expect('limit' in normalized).toBe(true);
|
|
244
|
+
expect('remaining' in normalized).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('omits null limits and keeps unknown byok/reset cases partial', () => {
|
|
248
|
+
const normalized = normalizeSdkKeyMetadata(
|
|
249
|
+
createCurrentKeyData({
|
|
250
|
+
includeByokInLimit: undefined as never,
|
|
251
|
+
limit: null,
|
|
252
|
+
limitRemaining: null,
|
|
253
|
+
limitReset: undefined as never,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(normalized).toMatchObject({
|
|
258
|
+
name: 'sk-or-v1-current',
|
|
259
|
+
byok: '?',
|
|
260
|
+
resetCadence: 'partial',
|
|
261
|
+
hash: 'unknown',
|
|
262
|
+
disabled: false,
|
|
263
|
+
});
|
|
264
|
+
expect(normalized).not.toHaveProperty('limit');
|
|
265
|
+
expect(normalized).not.toHaveProperty('remaining');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('keeps list-only metadata such as name, hash, disabled, and never resets', () => {
|
|
269
|
+
const normalized = normalizeSdkKeyMetadata(
|
|
270
|
+
createListKeyData({
|
|
271
|
+
disabled: true,
|
|
272
|
+
includeByokInLimit: true,
|
|
273
|
+
limitReset: 'never',
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(normalized).toMatchObject({
|
|
278
|
+
name: 'Workspace Key',
|
|
279
|
+
label: 'sk-or-v1-list',
|
|
280
|
+
byok: 'incl',
|
|
281
|
+
resetCadence: 'never',
|
|
282
|
+
hash: 'hash-123',
|
|
283
|
+
disabled: true,
|
|
284
|
+
limit: 100,
|
|
285
|
+
remaining: 40,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { UsageOverlayComponent } from '../overlay.js';
|
|
3
|
+
import { createUsageSummary, createUsageAggregate } from './fixtures.js';
|
|
4
|
+
import type { Theme } from '@mariozechner/pi-coding-agent';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal identity theme for testing.
|
|
8
|
+
* Returns text unchanged to simplify assertions.
|
|
9
|
+
*/
|
|
10
|
+
function createIdentityTheme(): Theme {
|
|
11
|
+
return {
|
|
12
|
+
bold: (text: string) => text,
|
|
13
|
+
fg: (_style: string, text: string) => text,
|
|
14
|
+
} as Theme;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Test suite for overlay Today-row rendering safety net.
|
|
19
|
+
*
|
|
20
|
+
* These tests prove the fix for the Today-row / local-window mismatch bug.
|
|
21
|
+
* The contract: the row labeled `Today` must render `summary.today`,
|
|
22
|
+
* not `summary.local.cost` or `summary.local.requests`.
|
|
23
|
+
*/
|
|
24
|
+
describe('UsageOverlayComponent - Today row rendering', () => {
|
|
25
|
+
const components: UsageOverlayComponent[] = [];
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
// Dispose all components to prevent interval timer leaks
|
|
29
|
+
for (const component of components) {
|
|
30
|
+
component.dispose();
|
|
31
|
+
}
|
|
32
|
+
components.length = 0;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function createComponent(summary: ReturnType<typeof createUsageSummary>): UsageOverlayComponent {
|
|
36
|
+
const component = new UsageOverlayComponent(
|
|
37
|
+
summary,
|
|
38
|
+
null, // no error
|
|
39
|
+
null, // no cached age
|
|
40
|
+
createIdentityTheme(),
|
|
41
|
+
() => {}, // onClose
|
|
42
|
+
() => {}, // requestRender
|
|
43
|
+
);
|
|
44
|
+
components.push(component);
|
|
45
|
+
return component;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find the Today row line in rendered output.
|
|
50
|
+
* Returns the line or null if not found.
|
|
51
|
+
*/
|
|
52
|
+
function findTodayLine(lines: string[]): string | null {
|
|
53
|
+
return lines.find((line) => line.includes('Today')) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('Activity API absent / 30-day local fallback', () => {
|
|
57
|
+
it.each([
|
|
58
|
+
{
|
|
59
|
+
label: 'local aggregate includes older + today usage',
|
|
60
|
+
today: 0.5,
|
|
61
|
+
localCost: 3.5,
|
|
62
|
+
localRequests: 15,
|
|
63
|
+
expectedTodayDisplay: 'Today ~$0.50',
|
|
64
|
+
excludedAmount: '3.50',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'local aggregate is zero',
|
|
68
|
+
today: 0,
|
|
69
|
+
localCost: 0,
|
|
70
|
+
localRequests: 0,
|
|
71
|
+
expectedTodayDisplay: 'Today ~$0.00',
|
|
72
|
+
excludedAmount: null,
|
|
73
|
+
},
|
|
74
|
+
])(
|
|
75
|
+
'renders Today using summary.today when $label',
|
|
76
|
+
({ today, localCost, localRequests, expectedTodayDisplay, excludedAmount }) => {
|
|
77
|
+
const summary = createUsageSummary({
|
|
78
|
+
today,
|
|
79
|
+
hasActivityData: false,
|
|
80
|
+
local: createUsageAggregate({
|
|
81
|
+
cost: localCost,
|
|
82
|
+
requests: localRequests,
|
|
83
|
+
}),
|
|
84
|
+
combined: createUsageAggregate({
|
|
85
|
+
cost: localCost,
|
|
86
|
+
requests: localRequests,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const component = createComponent(summary);
|
|
91
|
+
const lines = component.render(120);
|
|
92
|
+
const todayLine = findTodayLine(lines);
|
|
93
|
+
|
|
94
|
+
expect(todayLine).toBeTruthy();
|
|
95
|
+
expect(todayLine).toContain(expectedTodayDisplay);
|
|
96
|
+
if (excludedAmount) {
|
|
97
|
+
expect(todayLine).not.toContain(excludedAmount);
|
|
98
|
+
}
|
|
99
|
+
expect(todayLine).not.toContain('reqs');
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Activity API behind / local spans multiple post-official days', () => {
|
|
105
|
+
it.each([
|
|
106
|
+
{
|
|
107
|
+
label: 'API through yesterday, local has yesterday + today',
|
|
108
|
+
today: 2.0,
|
|
109
|
+
week: 0,
|
|
110
|
+
month: 0,
|
|
111
|
+
officialThroughDate: '2026-05-24',
|
|
112
|
+
officialCost: 10.0,
|
|
113
|
+
officialRequests: 50,
|
|
114
|
+
localCost: 4.0,
|
|
115
|
+
localRequests: 20,
|
|
116
|
+
combinedCost: 14.0,
|
|
117
|
+
combinedRequests: 70,
|
|
118
|
+
expectedTodayDisplay: 'Today ~$2.00',
|
|
119
|
+
excludedAmount: '4.00',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'API 5 days behind, local has 5 days',
|
|
123
|
+
today: 1.5,
|
|
124
|
+
week: 15.0,
|
|
125
|
+
month: 50.0,
|
|
126
|
+
officialThroughDate: '2026-05-20',
|
|
127
|
+
officialCost: 40.0,
|
|
128
|
+
officialRequests: 200,
|
|
129
|
+
localCost: 10.0,
|
|
130
|
+
localRequests: 50,
|
|
131
|
+
combinedCost: 50.0,
|
|
132
|
+
combinedRequests: 250,
|
|
133
|
+
expectedTodayDisplay: 'Today ~$1.50',
|
|
134
|
+
excludedAmount: '10.00',
|
|
135
|
+
},
|
|
136
|
+
])(
|
|
137
|
+
'renders Today using summary.today when $label',
|
|
138
|
+
({
|
|
139
|
+
today,
|
|
140
|
+
week,
|
|
141
|
+
month,
|
|
142
|
+
officialThroughDate,
|
|
143
|
+
officialCost,
|
|
144
|
+
officialRequests,
|
|
145
|
+
localCost,
|
|
146
|
+
localRequests,
|
|
147
|
+
combinedCost,
|
|
148
|
+
combinedRequests,
|
|
149
|
+
expectedTodayDisplay,
|
|
150
|
+
excludedAmount,
|
|
151
|
+
}) => {
|
|
152
|
+
const summary = createUsageSummary({
|
|
153
|
+
today,
|
|
154
|
+
week,
|
|
155
|
+
month,
|
|
156
|
+
hasActivityData: true,
|
|
157
|
+
officialThroughDate,
|
|
158
|
+
official: createUsageAggregate({
|
|
159
|
+
cost: officialCost,
|
|
160
|
+
requests: officialRequests,
|
|
161
|
+
}),
|
|
162
|
+
local: createUsageAggregate({
|
|
163
|
+
cost: localCost,
|
|
164
|
+
requests: localRequests,
|
|
165
|
+
}),
|
|
166
|
+
combined: createUsageAggregate({
|
|
167
|
+
cost: combinedCost,
|
|
168
|
+
requests: combinedRequests,
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const component = createComponent(summary);
|
|
173
|
+
const lines = component.render(120);
|
|
174
|
+
const todayLine = findTodayLine(lines);
|
|
175
|
+
|
|
176
|
+
expect(todayLine).toBeTruthy();
|
|
177
|
+
expect(todayLine).toContain(expectedTodayDisplay);
|
|
178
|
+
expect(todayLine).not.toContain(excludedAmount);
|
|
179
|
+
expect(todayLine).not.toContain('reqs');
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Activity API has today / local is empty', () => {
|
|
185
|
+
it.each([
|
|
186
|
+
{
|
|
187
|
+
label: 'API includes today',
|
|
188
|
+
today: 3.0,
|
|
189
|
+
officialThroughDate: '2026-05-25',
|
|
190
|
+
officialCost: 20.0,
|
|
191
|
+
officialRequests: 100,
|
|
192
|
+
expectedTodayDisplay: 'Today ~$3.00',
|
|
193
|
+
},
|
|
194
|
+
])(
|
|
195
|
+
'renders Today using summary.today when $label',
|
|
196
|
+
({ today, officialThroughDate, officialCost, officialRequests, expectedTodayDisplay }) => {
|
|
197
|
+
const summary = createUsageSummary({
|
|
198
|
+
today,
|
|
199
|
+
hasActivityData: true,
|
|
200
|
+
officialThroughDate,
|
|
201
|
+
official: createUsageAggregate({
|
|
202
|
+
cost: officialCost,
|
|
203
|
+
requests: officialRequests,
|
|
204
|
+
}),
|
|
205
|
+
local: createUsageAggregate({
|
|
206
|
+
cost: 0,
|
|
207
|
+
requests: 0,
|
|
208
|
+
}),
|
|
209
|
+
combined: createUsageAggregate({
|
|
210
|
+
cost: officialCost,
|
|
211
|
+
requests: officialRequests,
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const component = createComponent(summary);
|
|
216
|
+
const lines = component.render(120);
|
|
217
|
+
const todayLine = findTodayLine(lines);
|
|
218
|
+
|
|
219
|
+
expect(todayLine).toBeTruthy();
|
|
220
|
+
expect(todayLine).toContain(expectedTodayDisplay);
|
|
221
|
+
expect(todayLine).not.toContain('reqs');
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
});
|