@@ -5,7 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
55const __filename = fileURLToPath ( import . meta. url ) ;
66const __dirname = path . dirname ( __filename ) ;
77const logic = await import ( pathToFileURL ( path . join ( __dirname , '..' , '..' , 'web-ui' , 'logic.mjs' ) ) ) ;
8- const { buildUsageChartGroups, buildUsageHeatmap } = logic ;
8+ const { buildUsageChartGroups, buildUsageHeatmap, buildUsageHourlyHeatmap } = logic ;
99
1010test ( 'buildUsageChartGroups aggregates codex and claude sessions into day buckets' , ( ) => {
1111 const now = Date . UTC ( 2026 , 3 , 6 , 12 , 0 , 0 ) ;
@@ -107,3 +107,47 @@ test('buildUsageHeatmap aligns to monday and aggregates sessions per day', () =>
107107 const hasDay = result . weeks . some ( ( week ) => Array . isArray ( week . days ) && week . days . some ( ( cell ) => cell && cell . dateKey === '2026-04-06' && cell . sessionCount === 2 ) ) ;
108108 assert . ok ( hasDay ) ;
109109} ) ;
110+
111+ test ( 'buildUsageHourlyHeatmap produces 7x24 grid with correct aggregation' , ( ) => {
112+ const now = Date . UTC ( 2026 , 3 , 6 , 12 , 0 , 0 ) ;
113+ const result = buildUsageHourlyHeatmap ( [
114+ { source : 'codex' , updatedAt : '2026-04-06T08:00:00.000Z' , messageCount : 5 , totalTokens : 120 } ,
115+ { source : 'claude' , updatedAt : '2026-04-06T08:30:00.000Z' , messageCount : 7 , totalTokens : 230 } ,
116+ { source : 'codex' , updatedAt : '2026-04-05T14:00:00.000Z' , messageCount : 3 , totalTokens : 90 }
117+ ] , { range : '7d' , now } ) ;
118+
119+ assert . strictEqual ( result . range , '7d' ) ;
120+ assert . strictEqual ( result . grid . length , 7 ) ;
121+ assert . strictEqual ( result . grid [ 0 ] . length , 24 ) ;
122+ assert . strictEqual ( result . weekdayLabels . length , 7 ) ;
123+ assert . strictEqual ( result . hourLabels . length , 24 ) ;
124+
125+ const april6Dow = ( new Date ( Date . UTC ( 2026 , 3 , 6 ) ) . getUTCDay ( ) + 6 ) % 7 ;
126+ assert . strictEqual ( result . grid [ april6Dow ] [ 8 ] . sessionCount , 2 ) ;
127+ assert . strictEqual ( result . grid [ april6Dow ] [ 8 ] . messageCount , 12 ) ;
128+ assert . strictEqual ( result . grid [ april6Dow ] [ 8 ] . tokenTotal , 350 ) ;
129+ assert . ok ( result . maxSessionCount >= 2 ) ;
130+ } ) ;
131+
132+ test ( 'buildUsageHourlyHeatmap ignores sessions outside range' , ( ) => {
133+ const now = Date . UTC ( 2026 , 3 , 6 , 12 , 0 , 0 ) ;
134+ const result = buildUsageHourlyHeatmap ( [
135+ { source : 'codex' , updatedAt : '2026-04-06T08:00:00.000Z' , messageCount : 5 , totalTokens : 120 } ,
136+ { source : 'codex' , updatedAt : '2026-03-01T08:00:00.000Z' , messageCount : 99 , totalTokens : 999 }
137+ ] , { range : '7d' , now } ) ;
138+
139+ let totalSessions = 0 ;
140+ for ( const row of result . grid ) {
141+ for ( const cell of row ) {
142+ totalSessions += cell . sessionCount ;
143+ }
144+ }
145+ assert . strictEqual ( totalSessions , 1 ) ;
146+ } ) ;
147+
148+ test ( 'buildUsageHourlyHeatmap returns empty grid for no sessions' , ( ) => {
149+ const result = buildUsageHourlyHeatmap ( [ ] , { range : '7d' } ) ;
150+ assert . strictEqual ( result . grid . length , 7 ) ;
151+ assert . strictEqual ( result . grid [ 0 ] . length , 24 ) ;
152+ assert . strictEqual ( result . maxSessionCount , 1 ) ;
153+ } ) ;
0 commit comments