codeburn/tests/quota-codex-banked-resets.test.ts
ozymandiashh 6be422ac52 feat(quota): show Codex limit resets in the Plan tab, hover card and CLI
The Plan tab row #724 added showed a count and an expiry. It now also shows how
many of those credits can be applied right now and when the most recent one
landed, which is what a user deciding whether to push through or throttle back
actually needs. The same sentence appears in the agent-tab quota hover card and
in `codeburn quota`.

One formatter produces it, mirrored line for line in Swift and TypeScript, down
to the compact-age rules — deliberately not RelativeDateTimeFormatter or
Intl.RelativeTimeFormat, because the two surfaces have to print the same
characters, and a locale-aware formatter would not. `codeburn quota` grows a
`notes` channel for provider facts that are not a window; only Codex sets it, so
every other provider's output is byte-identical to before.

The CLI reads the credits off the `rate_limit_reset_credits` block of the usage
response it already fetches and never calls the companion endpoint, so it prints
the count without the grant and expiry captions that only the per-credit list
carries.

Refs #725. No new polling loop and no new data source: this reads fields from a
response already fetched on the existing cadence.
2026-09-12 14:37:24 -07:00

156 lines
6.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { compactAge, decodeCodexUsage, decodeResetCredits, resetCreditsLine } from '../src/quota/codex.js'
import { renderQuotaTable, toCommandProvider } from '../src/quota/index.js'
// 2027-01-15T08:00:00Z, the same instant the Swift suite pins, so the two sets
// of expected strings can be compared by eye.
const NOW = 1_800_000_000_000
function usageBody(block: unknown) {
return {
plan_type: 'plus',
rate_limit: {
primary_window: { used_percent: 12, limit_window_seconds: 18_000 },
},
rate_limit_reset_credits: block,
}
}
describe('Codex limit-reset credits', () => {
it('names the count and the most recent grant, in the menubar wording', () => {
const quota = decodeCodexUsage(usageBody({
available_count: 2,
credits: [
{ id: 'c1', reset_type: 'weekly', status: 'available', granted_at: '2027-01-15T06:00:00Z' },
{ id: 'c2', reset_type: 'weekly', status: 'available', granted_at: '2027-01-15T07:50:00Z' },
],
}), NOW)
expect(quota.notes).toEqual(['Limit resets · 2 available · latest weekly reset granted 10m ago'])
expect(quota.footerLines).toContain('Limit resets · 2 available · latest weekly reset granted 10m ago')
})
it('states the usable count only when it disagrees with the headline', () => {
const same = decodeCodexUsage(usageBody({ available_count: 1, applicable_available_count: 1 }), NOW)
expect(same.notes).toEqual(['Limit resets · 1 available'])
const differs = decodeCodexUsage(usageBody({ available_count: 2, applicable_available_count: 1 }), NOW)
expect(differs.notes).toEqual(['Limit resets · 2 available · 1 usable now'])
})
it('keeps the expiry clause when the payload carries per-credit expiries', () => {
const line = resetCreditsLine(decodeResetCredits({
available_count: 1,
credits: [{
id: 'c1', reset_type: 'weekly', status: 'available',
granted_at: '2027-01-14T08:00:00Z', expires_at: '2027-01-16T00:00:00Z',
}],
}, NOW), NOW)
expect(line).toBe('Limit resets · 1 available · latest weekly reset granted 1d ago · next expires in 16h')
})
it('the expiry reported is the soonest one, not the last', () => {
const credits = decodeResetCredits({
available_count: 2,
credits: [
{ id: 'c1', status: 'available', expires_at: '2027-01-20T08:00:00Z' },
{ id: 'c2', status: 'available', expires_at: '2027-01-16T00:00:00Z' },
],
}, NOW)
expect(credits?.nextExpiresAt).toBe(Date.parse('2027-01-16T00:00:00Z'))
expect(resetCreditsLine(credits, NOW)).toBe('Limit resets · 2 available · next expires in 16h')
})
it('only available credits count toward the grant and the expiry', () => {
const credits = decodeResetCredits({
available_count: 1,
credits: [
{ id: 'c1', reset_type: 'weekly', status: 'available', granted_at: '2027-01-14T08:00:00Z' },
{
id: 'c2', reset_type: 'weekly', status: 'redeemed',
granted_at: '2027-01-15T07:00:00Z', expires_at: '2027-01-15T09:00:00Z',
},
],
}, NOW)
expect(credits?.grants.map(grant => grant.id)).toEqual(['c1'])
expect(credits?.nextExpiresAt).toBeNull()
})
it('an absent applicable count is unknown, never zero', () => {
expect(decodeResetCredits({ available_count: 0 }, NOW)?.applicableAvailableCount).toBeNull()
expect(decodeResetCredits({ available_count: 0, applicable_available_count: 0 }, NOW)
?.applicableAvailableCount).toBe(0)
})
it('falls back to granted_at as the identity, and drops a credit with neither', () => {
const credits = decodeResetCredits({
available_count: 2,
credits: [
{ status: 'available', granted_at: '2027-01-14T08:00:00Z' },
{ status: 'available' },
],
}, NOW)
expect(credits?.grants.map(grant => grant.id)).toEqual(['2027-01-14T08:00:00Z'])
// Still counted — the server said two are available — but only one can ever
// be told apart from the next grant.
expect(credits?.availableCount).toBe(2)
})
it('holds nothing, says nothing', () => {
const quota = decodeCodexUsage(usageBody({ available_count: 0, applicable_available_count: 0 }), NOW)
expect(quota.notes).toBeUndefined()
expect(quota.footerLines).toEqual([])
})
it('a malformed or missing block hides the line without failing the read', () => {
for (const block of [undefined, null, 'nonsense', {}, { available_count: -1 }, { available_count: 'two' }]) {
const quota = decodeCodexUsage(usageBody(block), NOW)
expect(quota.connection).toBe('connected')
expect(quota.primary?.percent).toBe(0.12)
expect(quota.notes).toBeUndefined()
}
})
it('a grant timestamped in the future reads as landing, not as granted', () => {
const line = resetCreditsLine(decodeResetCredits({
available_count: 1,
credits: [{ id: 'c1', reset_type: 'weekly', status: 'available', granted_at: '2027-01-15T10:00:00Z' }],
}, NOW), NOW)
expect(line).toBe('Limit resets · 1 available · next weekly reset lands in 2h')
})
it('an absent reset_type degrades to the bare noun', () => {
const line = resetCreditsLine(decodeResetCredits({
available_count: 1,
credits: [{ id: 'c1', status: 'available', granted_at: '2027-01-15T07:00:00Z' }],
}, NOW), NOW)
expect(line).toBe('Limit resets · 1 available · latest limit reset granted 1h ago')
})
it('compact ages read the same as the Swift port', () => {
expect(compactAge(NOW, NOW)).toBe('just now')
expect(compactAge(NOW - 59_000, NOW)).toBe('just now')
expect(compactAge(NOW - 60_000, NOW)).toBe('1m ago')
expect(compactAge(NOW - 7_200_000, NOW)).toBe('2h ago')
// Elapsed time is floored, never rounded up: 2h50m is still "2h ago".
expect(compactAge(NOW - 10_200_000, NOW)).toBe('2h ago')
expect(compactAge(NOW - 3 * 86_400_000, NOW)).toBe('3d ago')
expect(compactAge(NOW - 3.9 * 86_400_000, NOW)).toBe('3d ago')
expect(compactAge(NOW + 30_000, NOW)).toBe('in under a minute')
expect(compactAge(NOW + 7_200_000, NOW)).toBe('in 2h')
expect(compactAge(NOW + 2 * 86_400_000, NOW)).toBe('in 2d')
})
it('`codeburn quota` prints the line under the provider windows', () => {
const quota = decodeCodexUsage(usageBody({ available_count: 2, applicable_available_count: 1 }), NOW)
const provider = toCommandProvider('codex', 'Codex', quota)
expect(provider.notes).toEqual(['Limit resets · 2 available · 1 usable now'])
const table = renderQuotaTable({ providers: [provider] })
expect(table).toContain('Limit resets · 2 available · 1 usable now')
})
it('a provider that reports no notes is rendered exactly as before', () => {
const quota = decodeCodexUsage(usageBody(undefined), NOW)
expect(toCommandProvider('codex', 'Codex', quota).notes).toBeUndefined()
})
})