Skip to content

Commit dbd86b3

Browse files
authored
Merge pull request #137 from github/asizikov/native-aggregation-tests
Add native AI Credits aggregation coverage
2 parents d67740d + 2afd03a commit dbd86b3

1 file changed

Lines changed: 320 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)