11import { describe , expect , it } from "vitest" ;
2- import { recordAiUsageEvent , sumAiCostForTenantSince } from "../../src/db/repositories" ;
2+ import { listAiCostByTenantSince , recordAiUsageEvent , sumAiCostForTenantSince } from "../../src/db/repositories" ;
33import { createTestEnv } from "../helpers/d1" ;
44
55// #7176: ai_usage_events gained a nullable installation_id tenant column for centralized hosted billing, plus a
@@ -18,6 +18,14 @@ async function installationOf(env: Env, id: string): Promise<string | null> {
1818 return row ?. installation_id ?? null ;
1919}
2020
21+ /** Insert one ai_usage_events row and backdate it, so time-window filters (sinceIso) are actually exercised
22+ * instead of every row landing at "now". Shared by the per-tenant sum tests and the fleet-wide breakdown tests
23+ * below, which seed the identical fixture shape. */
24+ async function seedCostEvent ( env : Env , installationId : string | null , costUsd : number , createdAt : string ) : Promise < void > {
25+ await recordAiUsageEvent ( env , { ...base , costUsd, installationId } ) ;
26+ await env . DB . prepare ( "UPDATE ai_usage_events SET created_at = ? WHERE created_at = (SELECT max(created_at) FROM ai_usage_events)" ) . bind ( createdAt ) . run ( ) ;
27+ }
28+
2129describe ( "ai_usage_events tenant column + billing aggregate (#7176)" , ( ) => {
2230 it ( "writes installation_id when a hosted caller supplies it" , async ( ) => {
2331 const env = createTestEnv ( ) ;
@@ -42,20 +50,51 @@ describe("ai_usage_events tenant column + billing aggregate (#7176)", () => {
4250 const env = createTestEnv ( ) ;
4351 // Two events for inst-1 after the window, one before it, one for inst-2, one self-host (null tenant).
4452 const since = "2026-07-10T00:00:00.000Z" ;
45- const seed = async ( installationId : string | null , costUsd : number , createdAt : string ) => {
46- await recordAiUsageEvent ( env , { ...base , costUsd, installationId } ) ;
47- // Backdate the just-inserted row (recordAiUsageEvent stamps nowIso()), so the window filter is exercised.
48- await env . DB . prepare ( "UPDATE ai_usage_events SET created_at = ? WHERE created_at = (SELECT max(created_at) FROM ai_usage_events)" ) . bind ( createdAt ) . run ( ) ;
49- } ;
50- await seed ( "inst-1" , 1.25 , "2026-07-11T00:00:00.000Z" ) ;
51- await seed ( "inst-1" , 0.75 , "2026-07-12T00:00:00.000Z" ) ;
52- await seed ( "inst-1" , 9.0 , "2026-07-01T00:00:00.000Z" ) ; // before the window
53- await seed ( "inst-2" , 4.0 , "2026-07-13T00:00:00.000Z" ) ; // different tenant
54- await seed ( null , 3.0 , "2026-07-14T00:00:00.000Z" ) ; // self-host, null tenant
53+ await seedCostEvent ( env , "inst-1" , 1.25 , "2026-07-11T00:00:00.000Z" ) ;
54+ await seedCostEvent ( env , "inst-1" , 0.75 , "2026-07-12T00:00:00.000Z" ) ;
55+ await seedCostEvent ( env , "inst-1" , 9.0 , "2026-07-01T00:00:00.000Z" ) ; // before the window
56+ await seedCostEvent ( env , "inst-2" , 4.0 , "2026-07-13T00:00:00.000Z" ) ; // different tenant
57+ await seedCostEvent ( env , null , 3.0 , "2026-07-14T00:00:00.000Z" ) ; // self-host, null tenant
5558
5659 expect ( await sumAiCostForTenantSince ( env , "inst-1" , since ) ) . toBeCloseTo ( 2.0 , 5 ) ;
5760 expect ( await sumAiCostForTenantSince ( env , "inst-2" , since ) ) . toBeCloseTo ( 4.0 , 5 ) ;
5861 // A tenant with no rows sums to 0, not an error.
5962 expect ( await sumAiCostForTenantSince ( env , "inst-none" , since ) ) . toBe ( 0 ) ;
6063 } ) ;
6164} ) ;
65+
66+ describe ( "listAiCostByTenantSince (#4916): fleet-wide per-tenant breakdown for the operator dashboard" , ( ) => {
67+ it ( "groups by tenant, sums correctly, and orders highest-cost-first" , async ( ) => {
68+ const env = createTestEnv ( ) ;
69+ const since = "2026-07-10T00:00:00.000Z" ;
70+ await seedCostEvent ( env , "inst-1" , 1.25 , "2026-07-11T00:00:00.000Z" ) ;
71+ await seedCostEvent ( env , "inst-1" , 0.75 , "2026-07-12T00:00:00.000Z" ) ; // inst-1 total: 2.0
72+ await seedCostEvent ( env , "inst-2" , 4.0 , "2026-07-13T00:00:00.000Z" ) ; // inst-2 total: 4.0 (highest)
73+ await seedCostEvent ( env , "inst-3" , 0.5 , "2026-07-13T00:00:00.000Z" ) ; // inst-3 total: 0.5 (lowest)
74+
75+ const rows = await listAiCostByTenantSince ( env , since ) ;
76+
77+ expect ( rows ) . toEqual ( [
78+ { installationId : "inst-2" , totalCostUsd : 4.0 } ,
79+ { installationId : "inst-1" , totalCostUsd : 2.0 } ,
80+ { installationId : "inst-3" , totalCostUsd : 0.5 } ,
81+ ] ) ;
82+ } ) ;
83+
84+ it ( "excludes self-host rows (null installation_id) and rows outside the time window" , async ( ) => {
85+ const env = createTestEnv ( ) ;
86+ const since = "2026-07-10T00:00:00.000Z" ;
87+ await seedCostEvent ( env , "inst-1" , 2.0 , "2026-07-11T00:00:00.000Z" ) ; // in window
88+ await seedCostEvent ( env , "inst-1" , 9.0 , "2026-07-01T00:00:00.000Z" ) ; // before the window
89+ await seedCostEvent ( env , null , 5.0 , "2026-07-11T00:00:00.000Z" ) ; // self-host, must never appear
90+
91+ const rows = await listAiCostByTenantSince ( env , since ) ;
92+
93+ expect ( rows ) . toEqual ( [ { installationId : "inst-1" , totalCostUsd : 2.0 } ] ) ;
94+ } ) ;
95+
96+ it ( "returns an empty list, not an error, when there are no hosted rows at all (the self-host default)" , async ( ) => {
97+ const env = createTestEnv ( ) ;
98+ expect ( await listAiCostByTenantSince ( env , "2026-07-10T00:00:00.000Z" ) ) . toEqual ( [ ] ) ;
99+ } ) ;
100+ } ) ;
0 commit comments