usage-aggregator.test.ts66 lines · main
| 1 | /** |
| 2 | * Hour-boundary tests for the usage aggregator. The DB-touching path |
| 3 | * (aggregateUsageForCompletedHour) is exercised by the post-deploy |
| 4 | * smoke; this file pins the pure boundary math + the conflict-update |
| 5 | * shape so a future tweak surfaces in CI. |
| 6 | */ |
| 7 | |
| 8 | import { describe, expect, test } from 'bun:test'; |
| 9 | |
| 10 | import { currentHourStart } from './usage-aggregator.js'; |
| 11 | |
| 12 | describe('currentHourStart', () => { |
| 13 | test('snaps to the first millisecond of the containing UTC hour', () => { |
| 14 | // 14:23:45.678 UTC → 14:00:00.000 UTC |
| 15 | const mid = new Date('2026-05-11T14:23:45.678Z'); |
| 16 | const out = currentHourStart(mid); |
| 17 | expect(out.toISOString()).toBe('2026-05-11T14:00:00.000Z'); |
| 18 | }); |
| 19 | |
| 20 | test('the first millisecond of an hour stays on that hour', () => { |
| 21 | // 14:00:00.000 UTC → 14:00:00.000 UTC (idempotent on a boundary) |
| 22 | const exact = new Date('2026-05-11T14:00:00.000Z'); |
| 23 | expect(currentHourStart(exact).toISOString()).toBe('2026-05-11T14:00:00.000Z'); |
| 24 | }); |
| 25 | |
| 26 | test('the last millisecond of an hour stays in that hour', () => { |
| 27 | // 14:59:59.999 UTC → 14:00:00.000 UTC (not rolled forward) |
| 28 | const last = new Date('2026-05-11T14:59:59.999Z'); |
| 29 | expect(currentHourStart(last).toISOString()).toBe('2026-05-11T14:00:00.000Z'); |
| 30 | }); |
| 31 | |
| 32 | test('midnight UTC handles correctly', () => { |
| 33 | const midnight = new Date('2026-05-11T00:00:00.000Z'); |
| 34 | expect(currentHourStart(midnight).toISOString()).toBe('2026-05-11T00:00:00.000Z'); |
| 35 | }); |
| 36 | |
| 37 | test('crosses month boundary correctly', () => { |
| 38 | // 23:59:59.999 on the last day of April → 23:00 same day |
| 39 | const lastMin = new Date('2026-04-30T23:59:59.999Z'); |
| 40 | expect(currentHourStart(lastMin).toISOString()).toBe('2026-04-30T23:00:00.000Z'); |
| 41 | }); |
| 42 | }); |
| 43 | |
| 44 | describe('completed-hour window', () => { |
| 45 | // The aggregator rolls up the hour that JUST ENDED — [start, end) |
| 46 | // where end == currentHourStart(now). This pins that the window is |
| 47 | // exactly one hour and ends at a clean boundary. |
| 48 | function completedHourWindow(now: Date): { start: Date; end: Date } { |
| 49 | const end = currentHourStart(now); |
| 50 | const start = new Date(end.getTime() - 60 * 60 * 1000); |
| 51 | return { start, end }; |
| 52 | } |
| 53 | |
| 54 | test('rolls up the previous full hour', () => { |
| 55 | const now = new Date('2026-05-11T14:05:00.000Z'); |
| 56 | const { start, end } = completedHourWindow(now); |
| 57 | expect(start.toISOString()).toBe('2026-05-11T13:00:00.000Z'); |
| 58 | expect(end.toISOString()).toBe('2026-05-11T14:00:00.000Z'); |
| 59 | }); |
| 60 | |
| 61 | test('window is exactly 60 minutes wide', () => { |
| 62 | const now = new Date('2026-05-11T03:30:00.000Z'); |
| 63 | const { start, end } = completedHourWindow(now); |
| 64 | expect(end.getTime() - start.getTime()).toBe(60 * 60 * 1000); |
| 65 | }); |
| 66 | }); |