Files

523 lines
17 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import {
DAY_MS,
HOUR_MS,
buildTimelineLane,
laneHasWindow,
pickLaneWindow,
projectLane,
projectResetCredits,
startOfDay,
startOfWeek,
timelineSpan,
windowsIn,
} from '../src/features/quota/quotaTimelineModel';
import type { TimelineLane } from '../src/features/quota/quotaTimelineModel';
const at = (y: number, m: number, d: number, h = 0, min = 0) => new Date(y, m, d, h, min).getTime();
describe('windowsIn', () => {
test('projects backwards and forwards from the anchor', () => {
// Anchor is a known reset; the span opens before the current window did.
const anchor = at(2026, 6, 29, 12);
const from = anchor - 2.5 * DAY_MS;
const to = anchor + 1.5 * DAY_MS;
const windows = windowsIn(anchor, DAY_MS, from, to);
// A 4-day span of daily windows needs 5 bars: the span edges fall mid-window,
// so there's a partial window at each end.
expect(windows.length).toBe(5);
// Every boundary sits on a whole period from the anchor.
for (const window of windows) {
// `+ 0` normalizes JS's -0 from a negative remainder (windows before the anchor).
expect(((window.endMs - anchor) % DAY_MS) + 0).toBe(0);
expect(window.endMs - window.startMs).toBe(DAY_MS);
}
expect(windows.some((w) => w.startMs <= anchor && w.endMs >= anchor)).toBe(true);
// Fully covers the requested range.
expect(windows[0].startMs).toBeLessThanOrEqual(from);
expect(windows[windows.length - 1].endMs).toBeGreaterThanOrEqual(to);
});
test('covers the whole span with no gaps or overlaps', () => {
const windows = windowsIn(at(2026, 6, 29), 5 * HOUR_MS, at(2026, 6, 28), at(2026, 6, 30));
for (let i = 1; i < windows.length; i += 1) {
expect(windows[i].startMs).toBe(windows[i - 1].endMs);
}
});
test('rejects a degenerate period, span or anchor', () => {
expect(windowsIn(1000, 0, 0, 5000)).toEqual([]);
expect(windowsIn(1000, -5, 0, 5000)).toEqual([]);
expect(windowsIn(NaN, 1000, 0, 5000)).toEqual([]);
expect(windowsIn(1000, 1000, 5000, 5000)).toEqual([]);
});
test('bails out rather than looping forever on an absurd period', () => {
// A bad payload could give a 1ms period over a fortnight.
expect(windowsIn(0, 1, 0, 14 * DAY_MS)).toEqual([]);
});
});
describe('span boundaries', () => {
test('startOfDay and startOfWeek land on local midnight', () => {
const mid = at(2026, 6, 29, 14, 37);
expect(new Date(startOfDay(mid)).getHours()).toBe(0);
expect(new Date(startOfWeek(mid)).getDay()).toBe(0);
expect(new Date(startOfWeek(mid)).getHours()).toBe(0);
});
test('weekly span is a fortnight from the containing Sunday', () => {
const now = at(2026, 6, 29, 14, 0); // a Wednesday
const span = timelineSpan('weekly', 0, now);
expect(new Date(span.startMs).getDay()).toBe(0);
expect(span.days).toBe(14);
expect(span.startMs).toBeLessThanOrEqual(now);
expect(span.endMs).toBeGreaterThan(now);
});
test('offsets step a week in weekly mode and a day in session mode', () => {
const now = at(2026, 6, 29, 14, 0);
const weekly = timelineSpan('weekly', 0, now);
const weeklyNext = timelineSpan('weekly', 1, now);
expect(Math.round((weeklyNext.startMs - weekly.startMs) / DAY_MS)).toBe(7);
const session = timelineSpan('session', 0, now);
const sessionNext = timelineSpan('session', 1, now);
expect(Math.round((sessionNext.startMs - session.startMs) / DAY_MS)).toBe(1);
expect(session.days).toBe(3);
});
test('spans a whole number of days even across a DST transition', () => {
// US DST springs forward 2026-03-08; a fixed +14*DAY_MS would land at 23:00.
const span = timelineSpan('weekly', 0, at(2026, 2, 10, 12));
expect(new Date(span.startMs).getHours()).toBe(0);
expect(new Date(span.endMs).getHours()).toBe(0);
});
});
describe('projectLane', () => {
const lane = (over: Partial<TimelineLane> = {}): TimelineLane => ({
name: 'a.json',
displayName: 'Alice',
provider: 'claude',
anchorMs: at(2026, 6, 29, 20),
periodHours: 24 * 7,
remaining: 40,
limits: [],
resetCredits: [],
...over,
});
const span = timelineSpan('weekly', 0, at(2026, 6, 29, 12));
test('classifies past, live and next windows against now', () => {
const now = at(2026, 6, 29, 12);
const windows = projectLane(lane(), span.startMs, span.endMs, now, 'weekly');
expect(windows.length).toBeGreaterThan(0);
const live = windows.filter((w) => w.state === 'live');
expect(live.length).toBe(1);
expect(live[0].startMs).toBeLessThanOrEqual(now);
expect(live[0].endMs).toBeGreaterThan(now);
expect(live[0].remaining).toBe(40);
expect(windows.filter((w) => w.state === 'past').every((w) => w.endMs <= now)).toBe(true);
expect(windows.filter((w) => w.state === 'next').every((w) => w.startMs > now)).toBe(true);
});
test('does not carry stale remaining usage past the reported reset', () => {
const nowAfterReset = at(2026, 6, 30, 12);
const windows = projectLane(lane(), span.startMs, span.endMs, nowAfterReset, 'weekly');
const live = windows.find((window) => window.state === 'live');
expect(live).toBeDefined();
expect(live?.startMs).toBe(at(2026, 6, 29, 20));
expect(live?.remaining).toBeNull();
});
test('clips bars to the visible span', () => {
const windows = projectLane(lane(), span.startMs, span.endMs, at(2026, 6, 29, 12), 'weekly');
for (const window of windows) {
expect(window.leftPercent).toBeGreaterThanOrEqual(0);
expect(window.widthPercent).toBeGreaterThan(0);
expect(window.leftPercent + window.widthPercent).toBeLessThanOrEqual(100.0001);
}
});
test('returns nothing when the lane has no anchor or period', () => {
const now = at(2026, 6, 29, 12);
expect(projectLane(lane({ anchorMs: null }), span.startMs, span.endMs, now, 'weekly')).toEqual(
[]
);
expect(
projectLane(lane({ periodHours: null }), span.startMs, span.endMs, now, 'weekly')
).toEqual([]);
});
test('session mode projects only real 5-hour windows', () => {
const now = at(2026, 6, 29, 12);
const sessionSpan = timelineSpan('session', 0, now);
expect(projectLane(lane(), sessionSpan.startMs, sessionSpan.endMs, now, 'session')).toEqual([]);
const windows = projectLane(
lane({ periodHours: 5 }),
sessionSpan.startMs,
sessionSpan.endMs,
now,
'session'
);
expect(windows.some((w) => w.endMs - w.startMs === 5 * HOUR_MS)).toBe(true);
});
test('projects only unexpired reset credits inside the visible span', () => {
const now = at(2026, 6, 29, 12);
const visibleExpiry = at(2026, 7, 2, 12);
const marks = projectResetCredits(
lane({
resetCredits: [
{ id: 'expired', grantedAtMs: null, expiresAtMs: now - HOUR_MS },
{ id: 'visible', grantedAtMs: now - DAY_MS, expiresAtMs: visibleExpiry },
{ id: 'outside', grantedAtMs: now, expiresAtMs: span.endMs + HOUR_MS },
],
}),
span.startMs,
span.endMs,
now
);
expect(marks).toHaveLength(1);
expect(marks[0].id).toBe('visible');
expect(marks[0].leftPercent).toBe(
((visibleExpiry - span.startMs) / (span.endMs - span.startMs)) * 100
);
});
});
describe('pickLaneWindow', () => {
test('ignores windows with no reset instant', () => {
const chosen = pickLaneWindow([
{ resetAtMs: null, periodHours: 168 },
{ resetAtMs: 1000, periodHours: 5 },
{ resetAtMs: undefined, periodHours: 168 },
]);
expect(chosen?.resetAtMs).toBe(1000);
});
/**
* The bug this rule exists for: across a fortnight, the 5-hour window always
* resets soonest, so "pick the soonest" drew ~67 slivers per lane instead of
* two readable weekly bars.
*/
test('prefers the longest window that fits the span, not the soonest reset', () => {
const fiveHour = { resetAtMs: 1_000, periodHours: 5 };
const weekly = { resetAtMs: 9_000, periodHours: 168 };
expect(pickLaneWindow([fiveHour, weekly], 14 * 24)).toBe(weekly);
// A three-day span can't fit a weekly window, so the short one wins.
expect(pickLaneWindow([fiveHour, weekly], 3 * 24)).toBe(fiveHour);
});
test('breaks a period tie on the soonest reset', () => {
const later = { resetAtMs: 9_000, periodHours: 168 };
const sooner = { resetAtMs: 5_000, periodHours: 168 };
expect(pickLaneWindow([later, sooner], 14 * 24)).toBe(sooner);
});
test('falls back to the shortest available rather than drawing nothing', () => {
// Every window is longer than the span — still better to draw one.
const monthly = { resetAtMs: 5_000, periodHours: 720 };
expect(pickLaneWindow([monthly], 3 * 24)).toBe(monthly);
});
test('returns null when nothing qualifies', () => {
expect(pickLaneWindow([{ resetAtMs: null }])).toBeNull();
expect(pickLaneWindow([])).toBeNull();
});
});
describe('buildTimelineLane', () => {
const base = { name: 'a.json', displayName: 'Alice' };
test('claude/codex: anchors on the span-appropriate window, remaining from used', () => {
const soon = at(2026, 6, 29, 20);
const later = at(2026, 7, 1, 20);
const quota = {
status: 'success',
windows: [
{ label: '7-day', usedPercent: 93, resetAtMs: later, periodHours: 168 },
{ label: '5-hour', usedPercent: 20, resetAtMs: soon, periodHours: 5 },
],
};
// Fortnight view: the weekly window, even though the 5-hour resets sooner.
const weekly = buildTimelineLane({
...base,
provider: 'claude',
quota,
maxPeriodHours: 14 * 24,
});
expect(weekly.anchorMs).toBe(later);
expect(weekly.periodHours).toBe(168);
expect(weekly.remaining).toBe(7); // stored USED
// Three-day view: the weekly window doesn't fit, so the short one is used.
const session = buildTimelineLane({
...base,
provider: 'claude',
quota,
maxPeriodHours: 3 * 24,
});
expect(session.anchorMs).toBe(soon);
expect(session.periodHours).toBe(5);
expect(session.remaining).toBe(80);
// Every limit is still summarized in the lane head regardless of the pick.
expect(weekly.limits).toEqual([
{ label: '7-day', remaining: 7 },
{ label: '5-hour', remaining: 80 },
]);
});
test('codex: keeps the weekly lane on the account quota instead of Spark quota', () => {
const accountReset = at(2026, 7, 1, 20);
const sparkReset = at(2026, 6, 29, 20);
const lane = buildTimelineLane({
...base,
provider: 'codex',
quota: {
status: 'success',
windows: [
{
id: 'weekly',
label: 'Weekly limit',
usedPercent: 70,
resetAtMs: accountReset,
periodHours: 168,
},
{
id: 'gpt-5-3-codex-spark-weekly-0',
label: 'GPT-5.3-Codex-Spark weekly limit',
usedPercent: 2,
resetAtMs: sparkReset,
periodHours: 168,
},
],
},
maxPeriodHours: 14 * 24,
});
expect(lane.anchorMs).toBe(accountReset);
expect(lane.periodHours).toBe(168);
expect(lane.remaining).toBe(30);
});
test('codex: includes available reset credits with parseable expiry dates', () => {
const expiresAt = '2026-08-02T12:00:00Z';
const lane = buildTimelineLane({
...base,
provider: 'codex',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 90, resetAtMs: 5000, periodHours: 168 }],
rateLimitResetCredits: [
{
id: 'credit-1',
status: 'available',
grantedAt: '2026-07-01T12:00:00Z',
expiresAt,
},
{
id: 'spent',
status: 'consumed',
grantedAt: '2026-07-01T12:00:00Z',
expiresAt,
},
{ id: 'invalid', status: 'available', grantedAt: '', expiresAt: 'not-a-date' },
],
},
});
expect(lane.resetCredits).toEqual([
{
id: 'credit-1',
grantedAtMs: new Date('2026-07-01T12:00:00Z').getTime(),
expiresAtMs: new Date(expiresAt).getTime(),
},
]);
});
test('kimi: derives remaining from raw used/limit counts', () => {
const lane = buildTimelineLane({
...base,
provider: 'kimi',
quota: {
status: 'success',
rows: [
{ label: 'Daily', used: 540, limit: 1000, resetAtMs: 5000, periodHours: 24 },
{ label: 'Monthly', used: 8100, limit: 30000, resetAtMs: 9000, periodHours: 720 },
],
},
// A fortnight fits the daily window but not the monthly one.
maxPeriodHours: 14 * 24,
});
expect(lane.anchorMs).toBe(5000);
expect(lane.remaining).toBe(46);
expect(lane.limits).toEqual([
{ label: 'Daily', remaining: 46 },
{ label: 'Monthly', remaining: 73 },
]);
});
test('antigravity anchors on its bucket reset, with remaining from the fraction', () => {
const lane = buildTimelineLane({
...base,
provider: 'antigravity',
quota: {
status: 'success',
groups: [
{
buckets: [
{ label: '5h', remainingFraction: 0.4, resetAtMs: 1000, periodHours: 5 },
{ label: 'Weekly', remainingFraction: 0.82, resetAtMs: 5000, periodHours: 168 },
],
},
],
},
maxPeriodHours: 336,
});
// Longest window that fits wins, exactly as it does for claude/codex.
expect(lane.anchorMs).toBe(5000);
expect(lane.periodHours).toBe(168);
// remainingFraction is REMAINING, so it is not inverted.
expect(lane.remaining).toBe(82);
expect(lane.limits).toEqual([
{ label: '5h', remaining: 40 },
{ label: 'Weekly', remaining: 82 },
]);
});
test('antigravity buckets without a parseable reset do not anchor the lane', () => {
const lane = buildTimelineLane({
...base,
provider: 'antigravity',
quota: {
status: 'success',
groups: [{ buckets: [{ label: '5h', remainingFraction: 0.4, resetAtMs: null }] }],
},
});
expect(lane.anchorMs).toBeNull();
expect(lane.limits).toEqual([]);
});
test('xai anchors on the weekly limit, with per-product limits', () => {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: {
periodType: 'weekly',
usagePercent: 5,
resetAtMs: 9000,
periodHours: 168,
productUsage: [
{ product: 'GrokBuild', usagePercent: 5 },
{ product: 'GrokChat', usagePercent: null },
],
},
},
});
expect(lane.anchorMs).toBe(9000);
expect(lane.periodHours).toBe(168);
expect(lane.remaining).toBe(95);
// GrokChat has no percentage, so it is not summarized.
expect(lane.limits).toEqual([{ label: 'GrokBuild', remaining: 95 }]);
});
test('xai without a weekly limit produces no window at all', () => {
// A monthly summary carries periodEnd too, but that is a billing cycle.
for (const periodType of ['monthly', 'unknown'] as const) {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: { periodType, usagePercent: 69, resetAtMs: 9000, periodHours: 720 },
},
});
expect(lane.anchorMs).toBeNull();
expect(laneHasWindow(lane)).toBe(false);
}
});
test('xai defaults to a 7-day period when the payload states no start', () => {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: { periodType: 'weekly', usagePercent: 5, resetAtMs: 9000, periodHours: null },
},
});
expect(lane.periodHours).toBe(24 * 7);
});
test('laneHasWindow drops only lanes that can never draw a bar', () => {
const drawable = buildTimelineLane({
...base,
provider: 'claude',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 10, resetAtMs: 5000, periodHours: 168 }],
},
});
expect(laneHasWindow(drawable)).toBe(true);
// Unloaded quota has nothing to show yet, so it takes no row.
expect(
laneHasWindow(buildTimelineLane({ ...base, provider: 'claude', quota: undefined }))
).toBe(false);
});
test('providers with no usable reset produce an empty lane, not a dropped one', () => {
// buildTimelineLane always returns a lane; dropping is the caller's job via
// laneHasWindow, so the two concerns stay separately testable.
for (const provider of ['antigravity', 'xai'] as const) {
const lane = buildTimelineLane({
...base,
provider,
quota: { status: 'success', groups: [] },
});
expect(lane.name).toBe('a.json');
expect(lane.anchorMs).toBeNull();
expect(lane.limits).toEqual([]);
}
});
test('unloaded or errored quota produces an empty lane', () => {
expect(
buildTimelineLane({ ...base, provider: 'claude', quota: undefined }).anchorMs
).toBeNull();
expect(
buildTimelineLane({ ...base, provider: 'claude', quota: { status: 'error' } }).anchorMs
).toBeNull();
});
test('windows without a reset instant do not anchor the lane', () => {
const lane = buildTimelineLane({
...base,
provider: 'claude',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 50, resetAtMs: null, periodHours: 168 }],
},
});
expect(lane.anchorMs).toBeNull();
});
});