Skip to content

Commit 1317f10

Browse files
asizikovCopilot
andcommitted
test: add native AI Credits aggregation coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d67740d commit 1317f10

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { getDisplayModelName } from './modelLabels'
4+
import { parseNativeAiCreditsUsageRecord, parseTokenUsageHeader, type TokenUsageRecord } from './parser'
5+
import { getFriendlyProductName } from './productClassification'
6+
import { getReportUsageMetrics, type CanonicalAiCreditsMetrics } from './reportUsageMetrics'
7+
8+
const NATIVE_AI_CREDITS_HEADER = [
9+
'date',
10+
'username',
11+
'product',
12+
'sku',
13+
'model',
14+
'quantity',
15+
'unit_type',
16+
'applied_cost_per_quantity',
17+
'gross_amount',
18+
'discount_amount',
19+
'net_amount',
20+
'total_monthly_quota',
21+
'organization',
22+
'cost_center_name',
23+
'aic_quantity',
24+
'aic_gross_amount',
25+
].join(',')
26+
27+
type ProductRollup = {
28+
totals: CanonicalAiCreditsMetrics
29+
models: Record<string, CanonicalAiCreditsMetrics>
30+
}
31+
32+
type NativeUsageRollups = {
33+
byDate: Record<string, CanonicalAiCreditsMetrics>
34+
byUser: Record<string, CanonicalAiCreditsMetrics>
35+
byModel: Record<string, CanonicalAiCreditsMetrics>
36+
byProduct: Record<string, ProductRollup>
37+
}
38+
39+
function createMetrics(): CanonicalAiCreditsMetrics {
40+
return {
41+
quantity: 0,
42+
grossAmount: 0,
43+
discountAmount: 0,
44+
netAmount: 0,
45+
}
46+
}
47+
48+
function ensureMetrics(
49+
rollup: Record<string, CanonicalAiCreditsMetrics>,
50+
key: string,
51+
): CanonicalAiCreditsMetrics {
52+
rollup[key] ??= createMetrics()
53+
return rollup[key]
54+
}
55+
56+
function ensureProduct(rollup: Record<string, ProductRollup>, product: string): ProductRollup {
57+
rollup[product] ??= {
58+
totals: createMetrics(),
59+
models: {},
60+
}
61+
return rollup[product]
62+
}
63+
64+
function addMetrics(total: CanonicalAiCreditsMetrics, metric: CanonicalAiCreditsMetrics): void {
65+
total.quantity += metric.quantity
66+
total.grossAmount += metric.grossAmount
67+
total.discountAmount += metric.discountAmount
68+
total.netAmount += metric.netAmount
69+
}
70+
71+
function aggregateNativeReportUsageMetrics(records: TokenUsageRecord[]): NativeUsageRollups {
72+
const rollups: NativeUsageRollups = {
73+
byDate: {},
74+
byUser: {},
75+
byModel: {},
76+
byProduct: {},
77+
}
78+
79+
for (const record of records) {
80+
const usage = getReportUsageMetrics(record, 'native-ai-credits')
81+
const model = getDisplayModelName(record.model)
82+
const product = getFriendlyProductName(record)
83+
84+
addMetrics(ensureMetrics(rollups.byDate, record.date), usage.aiCredits)
85+
addMetrics(ensureMetrics(rollups.byUser, record.username), usage.aiCredits)
86+
addMetrics(ensureMetrics(rollups.byModel, model), usage.aiCredits)
87+
88+
const productRollup = ensureProduct(rollups.byProduct, product)
89+
addMetrics(productRollup.totals, usage.aiCredits)
90+
addMetrics(ensureMetrics(productRollup.models, model), usage.aiCredits)
91+
}
92+
93+
return rollups
94+
}
95+
96+
function buildRow(values: string[]): string {
97+
return values.join(',')
98+
}
99+
100+
function nativeRecord(values: string[]): TokenUsageRecord {
101+
return parseNativeAiCreditsUsageRecord(buildRow(values), parseTokenUsageHeader(NATIVE_AI_CREDITS_HEADER))
102+
}
103+
104+
function nativeRecords(): TokenUsageRecord[] {
105+
return [
106+
nativeRecord([
107+
'5/29/26',
108+
'mona',
109+
'copilot',
110+
'copilot_ai_credit',
111+
'GPT-5.2',
112+
'10',
113+
'ai-credits',
114+
'0.01',
115+
'100',
116+
'20',
117+
'80',
118+
'3900',
119+
'example-org',
120+
'Cost Center A',
121+
'999',
122+
'999',
123+
]),
124+
nativeRecord([
125+
'05/29/2026',
126+
'hubot',
127+
'spark',
128+
'spark_ai_credit',
129+
' GPT-5.2 ',
130+
'25',
131+
'ai-credits',
132+
'0.01',
133+
'250',
134+
'50',
135+
'200',
136+
'7000',
137+
'octodemo',
138+
'',
139+
'',
140+
'',
141+
]),
142+
nativeRecord([
143+
'2026-05-30',
144+
'octocat',
145+
'copilot',
146+
'coding_agent_ai_credit',
147+
'Copilot Coding Agent: Claude Sonnet 4.6',
148+
'40',
149+
'ai-credits',
150+
'0.01',
151+
'400',
152+
'75',
153+
'325',
154+
'7000',
155+
'example-org',
156+
'Cost Center A',
157+
'4000',
158+
'4000',
159+
]),
160+
nativeRecord([
161+
'6/1/26',
162+
'mona',
163+
'copilot',
164+
'copilot_ai_credit',
165+
' ',
166+
'5',
167+
'ai-credits',
168+
'0.01',
169+
'50',
170+
'10',
171+
'40',
172+
'3900',
173+
'example-org',
174+
'',
175+
'5000',
176+
'5000',
177+
]),
178+
]
179+
}
180+
181+
describe('native AI Credits report usage aggregation harness', () => {
182+
it('rolls up parsed native rows by normalized ISO date', () => {
183+
const rollups = aggregateNativeReportUsageMetrics(nativeRecords())
184+
185+
expect(rollups.byDate).toEqual({
186+
'2026-05-29': {
187+
quantity: 35,
188+
grossAmount: 350,
189+
discountAmount: 70,
190+
netAmount: 280,
191+
},
192+
'2026-05-30': {
193+
quantity: 40,
194+
grossAmount: 400,
195+
discountAmount: 75,
196+
netAmount: 325,
197+
},
198+
'2026-06-01': {
199+
quantity: 5,
200+
grossAmount: 50,
201+
discountAmount: 10,
202+
netAmount: 40,
203+
},
204+
})
205+
expect(rollups.byDate).not.toHaveProperty('5/29/26')
206+
expect(rollups.byDate).not.toHaveProperty('05/29/2026')
207+
})
208+
209+
it('rolls up native AI Credits by user using native actual cost fields', () => {
210+
const rollups = aggregateNativeReportUsageMetrics(nativeRecords())
211+
212+
expect(rollups.byUser).toEqual({
213+
hubot: {
214+
quantity: 25,
215+
grossAmount: 250,
216+
discountAmount: 50,
217+
netAmount: 200,
218+
},
219+
mona: {
220+
quantity: 15,
221+
grossAmount: 150,
222+
discountAmount: 30,
223+
netAmount: 120,
224+
},
225+
octocat: {
226+
quantity: 40,
227+
grossAmount: 400,
228+
discountAmount: 75,
229+
netAmount: 325,
230+
},
231+
})
232+
})
233+
234+
it('rolls up native AI Credits by display model label', () => {
235+
const rollups = aggregateNativeReportUsageMetrics(nativeRecords())
236+
237+
expect(rollups.byModel).toEqual({
238+
'Copilot Coding Agent: Claude Sonnet 4.6': {
239+
quantity: 40,
240+
grossAmount: 400,
241+
discountAmount: 75,
242+
netAmount: 325,
243+
},
244+
'GPT-5.2': {
245+
quantity: 35,
246+
grossAmount: 350,
247+
discountAmount: 70,
248+
netAmount: 280,
249+
},
250+
Unlabeled: {
251+
quantity: 5,
252+
grossAmount: 50,
253+
discountAmount: 10,
254+
netAmount: 40,
255+
},
256+
})
257+
})
258+
259+
it('rolls up native AI Credits by friendly product and nested model labels', () => {
260+
const rollups = aggregateNativeReportUsageMetrics(nativeRecords())
261+
262+
expect(rollups.byProduct).toEqual({
263+
Copilot: {
264+
totals: {
265+
quantity: 15,
266+
grossAmount: 150,
267+
discountAmount: 30,
268+
netAmount: 120,
269+
},
270+
models: {
271+
'GPT-5.2': {
272+
quantity: 10,
273+
grossAmount: 100,
274+
discountAmount: 20,
275+
netAmount: 80,
276+
},
277+
Unlabeled: {
278+
quantity: 5,
279+
grossAmount: 50,
280+
discountAmount: 10,
281+
netAmount: 40,
282+
},
283+
},
284+
},
285+
'Copilot Cloud Agent': {
286+
totals: {
287+
quantity: 40,
288+
grossAmount: 400,
289+
discountAmount: 75,
290+
netAmount: 325,
291+
},
292+
models: {
293+
'Copilot Coding Agent: Claude Sonnet 4.6': {
294+
quantity: 40,
295+
grossAmount: 400,
296+
discountAmount: 75,
297+
netAmount: 325,
298+
},
299+
},
300+
},
301+
Spark: {
302+
totals: {
303+
quantity: 25,
304+
grossAmount: 250,
305+
discountAmount: 50,
306+
netAmount: 200,
307+
},
308+
models: {
309+
'GPT-5.2': {
310+
quantity: 25,
311+
grossAmount: 250,
312+
discountAmount: 50,
313+
netAmount: 200,
314+
},
315+
},
316+
},
317+
})
318+
})
319+
})

0 commit comments

Comments
 (0)