codeburn/app/renderer/lib/generation.test.ts
Resham Joshi 246cb37c83
desktop: light theme default, refreshed type and spacing, plugin and plan cards (#1485)
Fresh installs now default to the light theme; an explicit theme choice is still honored. Type and spacing move one step up (larger body, titles, sidebar rows and page padding), sidebar shortcut chips move to hover tooltips, and primary text gets full contrast in both themes.

The Plugins page shows the Menu bar and Teams plugins as two cards side by side with embossed background art. The Menu bar card carries the Capacity Dock switch and Settings, Quit and Uninstall as icon buttons with tooltips, an info modal describing the menu bar and the Capacity Dock with the real supported-provider list, and it can install, update, open, quit and uninstall the menu bar app, versioned so an older one shows Update, with a Settings action that opens the menu bar app's own settings. The Teams card shows what is planned in an info modal and a Register for beta testing link.

The Plans page is redesigned as provider cards in a responsive grid, each with its own logo embossed. Connection status is now stable: a single empty or slow poll no longer flips a connected provider to disconnected, a connected provider keeps its quota bars through a transient refresh, and a stuck waiting resolves to an actionable state after twenty seconds. A login-expired error offers Connect, and Gemini's expired login is now a terminal actionable state instead of an endless wait, matching the menu bar.

CLI report, models and menubar-json numbers are identical to the cent before and after; only the Gemini quota error string changes.
2026-09-18 15:09:01 -07:00

126 lines
5.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { __resetGeneration, generationAt, generationHeadline, generationModels, periodTotalsBreach, rememberGeneration, type PeriodTotals } from './generation'
import type { MenubarPayload } from './types'
const MODELS = [{ name: 'gen-opus', cost: 400, savingsUSD: 0, savingsBaselineModel: '', calls: 200 }] as MenubarPayload['current']['topModels']
function payloadWithModels(periodTotals: PeriodTotals, models: MenubarPayload['current']['topModels']): MenubarPayload {
return { periodTotals, current: { topModels: models } } as MenubarPayload
}
function totals(over: Partial<Record<keyof PeriodTotals, [number, number]>> = {}): PeriodTotals {
const base: Record<string, [number, number]> = {
today: [700, 6000],
week: [1900, 15000],
'30days': [14000, 87000],
month: [4700, 29000],
all: [37700, 270000],
lifetime: [37800, 272000],
...over,
}
return Object.fromEntries(Object.entries(base).map(([k, [cost, calls]]) => [k, { cost, calls }])) as PeriodTotals
}
function payload(periodTotals: PeriodTotals | undefined): MenubarPayload {
return { periodTotals } as MenubarPayload
}
beforeEach(__resetGeneration)
describe('periodTotalsBreach', () => {
it('accepts nested windows', () => {
expect(periodTotalsBreach(totals())).toBeNull()
})
it('catches a wider window costing less than a narrower one', () => {
expect(periodTotalsBreach(totals({ lifetime: [37000, 272000] }))).toMatch(/lifetime cost/)
})
it('catches calls going backwards, the Lifetime-under-6M case', () => {
expect(periodTotalsBreach(totals({ lifetime: [37800, 269000] }))).toMatch(/lifetime calls/)
})
it('skips a window the generation does not carry rather than comparing across the gap', () => {
const partial = totals({ lifetime: [10, 10] })
delete partial.all
delete partial['30days']
expect(periodTotalsBreach(partial)).toMatch(/lifetime/)
const scanned = totals()
delete scanned.all
expect(periodTotalsBreach(scanned)).toBeNull()
})
it('tolerates a cent of float drift between two sums', () => {
expect(periodTotalsBreach(totals({ all: [37800.004, 272000] , lifetime: [37800, 272000] }))).toBeNull()
})
})
describe('rememberGeneration', () => {
it('serves a period from a generation newer than the payload on screen', () => {
rememberGeneration(payload(totals()), 2_000)
expect(generationHeadline('today', 1_000)).toEqual({ cost: 700, calls: 6000 })
expect(generationHeadline('lifetime', 1_000)).toEqual({ cost: 37800, calls: 272000 })
expect(generationAt()).toBe(2_000)
})
it('never replaces a payload that is itself the newest answer', () => {
rememberGeneration(payload(totals()), 1_000)
expect(generationHeadline('today', 1_000)).toBeNull()
expect(generationHeadline('today', 5_000)).toBeNull()
})
it('does not answer for a window the scan did not cover', () => {
const scanned = totals()
delete scanned.lifetime
delete scanned.all
rememberGeneration(payload(scanned), 2_000)
expect(generationHeadline('today', 1_000)).toEqual({ cost: 700, calls: 6000 })
expect(generationHeadline('lifetime', 1_000)).toBeNull()
})
it('keeps the newest generation and ignores an older payload arriving late', () => {
rememberGeneration(payload(totals()), 3_000)
rememberGeneration(payload(totals({ today: [1, 1] })), 2_000)
expect(generationHeadline('today', 1_000)).toEqual({ cost: 700, calls: 6000 })
expect(generationAt()).toBe(3_000)
})
it('holds the last good generation when a payload carries no totals', () => {
rememberGeneration(payload(totals()), 2_000)
rememberGeneration(payload(undefined), 5_000)
expect(generationHeadline('all', 1_000)).toEqual({ cost: 37700, calls: 270000 })
expect(generationAt()).toBe(2_000)
})
it('reports a non-nested generation in dev instead of showing it silently', () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
rememberGeneration(payload(totals({ lifetime: [1, 1] })), 1_000)
expect(error).toHaveBeenCalledWith(expect.stringContaining('not nested'))
error.mockRestore()
})
})
describe('generationModels', () => {
it('serves the models when the generation is newer and captured from the shown period', () => {
rememberGeneration(payloadWithModels(totals(), MODELS), 2_000, '30days')
expect(generationModels('30days', 1_000)).toEqual(MODELS)
})
it('withholds the models when the generation was captured from another period', () => {
rememberGeneration(payloadWithModels(totals(), MODELS), 2_000, 'week')
// The headline can still take the 30days total, but the models belong to week.
expect(generationHeadline('30days', 1_000)).toEqual({ cost: 14000, calls: 87000 })
expect(generationModels('30days', 1_000)).toBeNull()
})
it('withholds the models when the payload on screen is already the newest', () => {
rememberGeneration(payloadWithModels(totals(), MODELS), 1_000, '30days')
expect(generationModels('30days', 1_000)).toBeNull()
})
it('withholds an empty model list rather than blanking the table', () => {
rememberGeneration(payloadWithModels(totals(), []), 2_000, '30days')
expect(generationModels('30days', 1_000)).toBeNull()
})
})