Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
239
frontend/tests/quotaResetSchedule.test.ts
Normal file
239
frontend/tests/quotaResetSchedule.test.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* Which row on a quota card recovers first, per provider.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
XAI_WEEKLY_ROW_ID,
|
||||
collectQuotaRowInstants,
|
||||
nextRecoveryMs,
|
||||
pickSoonestRowId,
|
||||
pickUrgentRowId,
|
||||
resetCreditRowId,
|
||||
} from '@/features/quota/resetSchedule';
|
||||
import { DAY_MS, HOUR_MS } from '@/utils/time/durations';
|
||||
|
||||
const NOW = new Date(2026, 7, 2, 12).getTime();
|
||||
const iso = (ms: number) => new Date(ms).toISOString();
|
||||
|
||||
const claudeQuota = {
|
||||
status: 'success',
|
||||
windows: [
|
||||
{ id: 'five_hour', resetAtMs: NOW + 3 * HOUR_MS },
|
||||
{ id: 'seven_day', resetAtMs: NOW + 4 * DAY_MS },
|
||||
],
|
||||
};
|
||||
|
||||
const codexQuota = {
|
||||
status: 'success',
|
||||
windows: [
|
||||
{ id: 'primary', resetAtMs: NOW + 3 * HOUR_MS },
|
||||
{ id: 'secondary', resetAtMs: NOW + 6 * DAY_MS },
|
||||
],
|
||||
rateLimitResetCredits: [
|
||||
{ id: 'credit-a', status: 'available', expiresAt: iso(NOW + 11 * DAY_MS) },
|
||||
{ id: 'credit-b', status: 'available', expiresAt: iso(NOW + 2 * DAY_MS) },
|
||||
],
|
||||
};
|
||||
|
||||
describe('collectQuotaRowInstants', () => {
|
||||
test('collects every Claude window', () => {
|
||||
expect(collectQuotaRowInstants('claude', claudeQuota)).toEqual([
|
||||
{ rowId: 'five_hour', atMs: NOW + 3 * HOUR_MS, kind: 'window' },
|
||||
{ rowId: 'seven_day', atMs: NOW + 4 * DAY_MS, kind: 'window' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('collects Codex windows and available reset credits together', () => {
|
||||
const instants = collectQuotaRowInstants('codex', codexQuota);
|
||||
expect(instants).toHaveLength(4);
|
||||
expect(instants.filter((i) => i.kind === 'credit').map((i) => i.rowId)).toEqual([
|
||||
'credit-a',
|
||||
'credit-b',
|
||||
]);
|
||||
});
|
||||
|
||||
test('ignores reset credits that are not available', () => {
|
||||
const consumed = {
|
||||
...codexQuota,
|
||||
rateLimitResetCredits: [
|
||||
{ id: 'used', status: 'consumed', expiresAt: iso(NOW + HOUR_MS) },
|
||||
{ id: 'live', status: 'available', expiresAt: iso(NOW + 2 * HOUR_MS) },
|
||||
],
|
||||
};
|
||||
expect(
|
||||
collectQuotaRowInstants('codex', consumed)
|
||||
.filter((i) => i.kind === 'credit')
|
||||
.map((i) => i.rowId)
|
||||
).toEqual(['live']);
|
||||
});
|
||||
|
||||
test('collects the xAI weekly window', () => {
|
||||
const quota = { status: 'success', billing: { periodType: 'weekly', resetAtMs: NOW + DAY_MS } };
|
||||
expect(collectQuotaRowInstants('xai', quota)).toEqual([
|
||||
{ rowId: XAI_WEEKLY_ROW_ID, atMs: NOW + DAY_MS, kind: 'window' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('ignores an xAI monthly summary — a billing cycle is not capacity returning', () => {
|
||||
const quota = {
|
||||
status: 'success',
|
||||
billing: { periodType: 'monthly', resetAtMs: NOW + DAY_MS },
|
||||
};
|
||||
expect(collectQuotaRowInstants('xai', quota)).toEqual([]);
|
||||
});
|
||||
|
||||
test('flattens Antigravity buckets across groups', () => {
|
||||
const quota = {
|
||||
status: 'success',
|
||||
groups: [
|
||||
{ id: 'g1', buckets: [{ id: 'b1', resetAtMs: NOW + HOUR_MS }] },
|
||||
{ id: 'g2', buckets: [{ id: 'b2', resetAtMs: NOW + 2 * HOUR_MS }] },
|
||||
],
|
||||
};
|
||||
expect(collectQuotaRowInstants('antigravity', quota).map((i) => i.rowId)).toEqual(['b1', 'b2']);
|
||||
});
|
||||
|
||||
test('collects Kimi rows', () => {
|
||||
const quota = { status: 'success', rows: [{ id: 'r1', resetAtMs: NOW + HOUR_MS }] };
|
||||
expect(collectQuotaRowInstants('kimi', quota).map((i) => i.rowId)).toEqual(['r1']);
|
||||
});
|
||||
|
||||
test('returns nothing unless the credential loaded successfully', () => {
|
||||
for (const status of ['idle', 'loading', 'error']) {
|
||||
expect(collectQuotaRowInstants('claude', { ...claudeQuota, status })).toEqual([]);
|
||||
}
|
||||
expect(collectQuotaRowInstants('claude', undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
test('drops rows with no usable instant rather than emitting NaN', () => {
|
||||
const quota = {
|
||||
status: 'success',
|
||||
windows: [
|
||||
{ id: 'ok', resetAtMs: NOW + HOUR_MS },
|
||||
{ id: 'missing' },
|
||||
{ id: 'null', resetAtMs: null },
|
||||
{ id: 'nan', resetAtMs: Number.NaN },
|
||||
],
|
||||
};
|
||||
expect(collectQuotaRowInstants('claude', quota).map((i) => i.rowId)).toEqual(['ok']);
|
||||
});
|
||||
|
||||
test('drops a reset credit whose expiry will not parse', () => {
|
||||
const quota = {
|
||||
status: 'success',
|
||||
windows: [],
|
||||
rateLimitResetCredits: [{ id: 'bad', status: 'available', expiresAt: 'not a date' }],
|
||||
};
|
||||
expect(collectQuotaRowInstants('codex', quota)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickSoonestRowId', () => {
|
||||
test('picks the nearest upcoming instant', () => {
|
||||
expect(pickSoonestRowId(collectQuotaRowInstants('claude', claudeQuota), NOW)).toBe('five_hour');
|
||||
});
|
||||
|
||||
test('a credit expiring before every window wins the emphasis', () => {
|
||||
const quota = {
|
||||
...codexQuota,
|
||||
windows: [{ id: 'primary', resetAtMs: NOW + 5 * DAY_MS }],
|
||||
};
|
||||
expect(pickSoonestRowId(collectQuotaRowInstants('codex', quota), NOW)).toBe('credit-b');
|
||||
});
|
||||
|
||||
test('a window resetting before every credit wins the emphasis', () => {
|
||||
expect(pickSoonestRowId(collectQuotaRowInstants('codex', codexQuota), NOW)).toBe('primary');
|
||||
});
|
||||
|
||||
test('skips instants that have already passed', () => {
|
||||
const instants = [
|
||||
{ rowId: 'past', atMs: NOW - HOUR_MS, kind: 'window' as const },
|
||||
{ rowId: 'exactly-now', atMs: NOW, kind: 'window' as const },
|
||||
{ rowId: 'future', atMs: NOW + HOUR_MS, kind: 'window' as const },
|
||||
];
|
||||
expect(pickSoonestRowId(instants, NOW)).toBe('future');
|
||||
});
|
||||
|
||||
test('returns null when nothing is pending', () => {
|
||||
expect(pickSoonestRowId([], NOW)).toBeNull();
|
||||
expect(pickSoonestRowId([{ rowId: 'past', atMs: NOW - 1, kind: 'window' }], NOW)).toBeNull();
|
||||
});
|
||||
|
||||
test('breaks ties deterministically on row id', () => {
|
||||
const a = [
|
||||
{ rowId: 'b', atMs: NOW + HOUR_MS, kind: 'window' as const },
|
||||
{ rowId: 'a', atMs: NOW + HOUR_MS, kind: 'window' as const },
|
||||
];
|
||||
expect(pickSoonestRowId(a, NOW)).toBe('a');
|
||||
expect(pickSoonestRowId([...a].reverse(), NOW)).toBe('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickUrgentRowId', () => {
|
||||
test('highlights only the nearest reset strictly inside the final hour', () => {
|
||||
const instants = [
|
||||
{ rowId: 'later-urgent', atMs: NOW + 45 * 60_000, kind: 'window' as const },
|
||||
{ rowId: 'nearest-urgent', atMs: NOW + 30 * 60_000, kind: 'window' as const },
|
||||
{ rowId: 'past', atMs: NOW - 1, kind: 'window' as const },
|
||||
];
|
||||
|
||||
expect(pickUrgentRowId(instants, NOW)).toBe('nearest-urgent');
|
||||
});
|
||||
|
||||
test('does not highlight at exactly one hour or beyond', () => {
|
||||
expect(
|
||||
pickUrgentRowId(
|
||||
[
|
||||
{ rowId: 'exactly-one-hour', atMs: NOW + HOUR_MS, kind: 'window' },
|
||||
{ rowId: 'later', atMs: NOW + 2 * HOUR_MS, kind: 'window' },
|
||||
],
|
||||
NOW
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetCreditRowId', () => {
|
||||
test('prefers the credit id', () => {
|
||||
expect(resetCreditRowId({ id: 'credit-a', expiresAt: 'x' }, 3)).toBe('credit-a');
|
||||
});
|
||||
|
||||
test('falls back to expiry and index when the payload carries no id', () => {
|
||||
// Must stay byte-identical to CodexQuotaBody's React key.
|
||||
expect(resetCreditRowId({ id: '', expiresAt: '2026-08-13T00:00:00Z' }, 2)).toBe(
|
||||
'2026-08-13T00:00:00Z-2'
|
||||
);
|
||||
expect(resetCreditRowId({ expiresAt: '2026-08-13T00:00:00Z' }, 0)).toBe(
|
||||
'2026-08-13T00:00:00Z-0'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nextRecoveryMs', () => {
|
||||
test('returns the soonest upcoming instant across windows and credits', () => {
|
||||
expect(nextRecoveryMs('codex', codexQuota, NOW)).toBe(NOW + 3 * HOUR_MS);
|
||||
});
|
||||
|
||||
test('ignores instants already in the past', () => {
|
||||
const quota = {
|
||||
status: 'success',
|
||||
windows: [
|
||||
{ id: 'stale', resetAtMs: NOW - DAY_MS },
|
||||
{ id: 'live', resetAtMs: NOW + DAY_MS },
|
||||
],
|
||||
};
|
||||
expect(nextRecoveryMs('claude', quota, NOW)).toBe(NOW + DAY_MS);
|
||||
});
|
||||
|
||||
test('is null for an unloaded credential, so sorting can sink it', () => {
|
||||
expect(nextRecoveryMs('claude', undefined, NOW)).toBeNull();
|
||||
expect(nextRecoveryMs('claude', { status: 'idle' }, NOW)).toBeNull();
|
||||
expect(nextRecoveryMs('claude', { status: 'error' }, NOW)).toBeNull();
|
||||
});
|
||||
|
||||
test('is null when every known instant has passed', () => {
|
||||
const quota = { status: 'success', windows: [{ id: 'stale', resetAtMs: NOW - 1 }] };
|
||||
expect(nextRecoveryMs('claude', quota, NOW)).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue