codeburn/tests/plan-budget-copy.test.ts
iamtoruk 911bd3f486 fix(plans): say monthly budget, not calendar month, and fit 80 columns
The budget window comes from computePeriodFromResetDay, which builds an
anniversary period from plan.resetDay (1-28, settable per plan with
`codeburn plan set --reset-day`). "Calendar-month budget" and "Next
calendar reset" are therefore wrong for anyone who moved the reset day,
which is the same class of inaccuracy this change set exists to remove.
Say "budget" and "Next budget reset" instead, and use one wording across
the TUI and the desktop cards.

Both TUI lines truncate end-first at the terminal width. The headline had
grown past the point where an 80-column terminal still showed the
percentage, so it drops "vs ... /mo" for "/ $300.00 budget", and the
status line drops the clause repeating "budget" from the headline. At 80
columns the longest label (custom plans carry their provider) now fits
the percentage, and the status line still shows the projection.
2026-08-19 11:30:07 -07:00

86 lines
3.3 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { planBudgetHeadline, planStatusText } from '../src/dashboard.js'
import type { PlanUsage } from '../src/plan-usage.js'
function usage(overrides: Partial<PlanUsage> = {}): PlanUsage {
return {
plan: {
id: 'supergrok-heavy',
monthlyUsd: 300,
provider: 'grok',
resetDay: 1,
setAt: '2026-08-01T00:00:00.000Z',
},
periodStart: new Date('2026-08-01T00:00:00'),
periodEnd: new Date('2026-09-01T00:00:00'),
spentApiEquivalentUsd: 33.82,
budgetUsd: 300,
percentUsed: 11.273,
status: 'under',
projectedMonthUsd: 40,
daysUntilReset: 13,
...overrides,
}
}
// The TUI truncates both lines end-first at the terminal width, so the row is
// headline + two spaces + the 10-cell bar + a space + the percentage.
const PLAN_ROW_TAIL = ' ' + '#'.repeat(10) + ' '
function planRow(planUsage: PlanUsage): string {
return planBudgetHeadline(planUsage) + PLAN_ROW_TAIL + planUsage.percentUsed.toFixed(1) + '%'
}
describe('plan budget copy', () => {
it('labels SuperGrok as a budget, not a live window', () => {
expect(planBudgetHeadline(usage())).toBe('SuperGrok Heavy: $33.82 API-equivalent / $300.00 budget')
const status = planStatusText(usage())
expect(status).toBe('Well within budget. Not a live provider window. Projected: $40.00. Next budget reset in 13 days.')
})
it('uses the same budget language for every preset, not only Grok', () => {
const cursor = usage({
plan: {
id: 'cursor-pro',
monthlyUsd: 20,
provider: 'cursor',
resetDay: 1,
setAt: '2026-08-01T00:00:00.000Z',
},
budgetUsd: 20,
spentApiEquivalentUsd: 8.2,
status: 'near',
})
expect(planBudgetHeadline(cursor)).toBe('Cursor Pro: $8.20 API-equivalent / $20.00 budget')
expect(planStatusText(cursor)).toContain('Approaching budget')
})
// computePeriodFromResetDay builds an anniversary window from plan.resetDay
// (1-28, settable with `codeburn plan set --reset-day`), so a plan that resets
// on the 15th is NOT on a calendar month and must never be called one.
it('never calls an anniversary reset window a calendar month', () => {
for (const resetDay of [1, 15, 28]) {
const status = planStatusText(usage({
plan: { id: 'supergrok-heavy', monthlyUsd: 300, provider: 'grok', resetDay, setAt: '2026-08-01T00:00:00.000Z' },
daysUntilReset: 4,
}))
expect(status).not.toMatch(/calendar/i)
expect(status).toContain('Next budget reset in 4 days.')
}
})
// The row and the status each get one truncate-end line. At 80 columns the
// percentage must survive on the row and the projection on the status line.
it('keeps the percentage and the projection readable at 80 columns', () => {
for (const planUsage of [
usage(),
usage({ status: 'over', spentApiEquivalentUsd: 640, percentUsed: 213.3 }),
usage({ plan: { id: 'custom', monthlyUsd: 300, provider: 'openrouter', resetDay: 1, setAt: '2026-08-01T00:00:00.000Z' } }),
]) {
const row = planRow(planUsage)
expect(row.length).toBeLessThanOrEqual(80)
expect(row.slice(0, 80)).toContain(`${planUsage.percentUsed.toFixed(1)}%`)
expect(planStatusText(planUsage).slice(0, 80)).toContain('Projected: $40.00.')
}
})
})