Files
Aditya Vikram Singh 4228c87595 fix(models): treat subscription SKUs as honestly $0
Unpriced warnings were firing on flat-rate product ids and telling
users to model-alias them, which invents spend.
2026-08-20 01:35:39 +05:30

276 lines
8.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { renderOverview } from '../src/overview.js'
import type { ProjectSummary } from '../src/types.js'
function makeProject(opts: {
project: string
projectPath: string
cost: number
calls: number
model: string
provider: string
tokens: { input: number; output: number; cacheR: number; cacheW: number }
estimated?: boolean
}): ProjectSummary {
const usage = {
inputTokens: opts.tokens.input,
outputTokens: opts.tokens.output,
cacheReadInputTokens: opts.tokens.cacheR,
cacheCreationInputTokens: opts.tokens.cacheW,
}
return {
project: opts.project,
projectPath: opts.projectPath,
totalCostUSD: opts.cost,
totalSavingsUSD: 0,
totalProxiedCostUSD: 0,
totalApiCalls: opts.calls,
sessions: [{
sessionId: 's1',
project: opts.project,
totalInputTokens: opts.tokens.input,
totalOutputTokens: opts.tokens.output,
totalCacheReadTokens: opts.tokens.cacheR,
totalCacheWriteTokens: opts.tokens.cacheW,
apiCalls: opts.calls,
modelBreakdown: { [opts.model]: { calls: opts.calls, costUSD: opts.cost, savingsUSD: 0, estimatedCostUSD: opts.estimated ? opts.cost : 0, tokens: usage } },
categoryBreakdown: { coding: { turns: 1, costUSD: opts.cost, savingsUSD: 0, retries: 0, editTurns: 1, oneShotTurns: 1 } },
toolBreakdown: { Bash: { calls: 5 }, Read: { calls: 2 } },
mcpBreakdown: {},
bashBreakdown: {},
skillBreakdown: {},
subagentBreakdown: {},
turns: [{
userMessage: 'hi',
timestamp: '2026-06-15T10:00:00Z',
sessionId: 's1',
category: 'coding',
retries: 0,
hasEdits: true,
assistantCalls: [{ provider: opts.provider, model: opts.model, costUSD: opts.cost, usage }],
}],
}],
} as unknown as ProjectSummary
}
describe('renderOverview', () => {
it('renders the detailed sections from real aggregation', () => {
const out = renderOverview([makeProject({
project: 'myproject',
projectPath: '/Users/test/myproject',
cost: 12.5,
calls: 3,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 5000, cacheW: 100 },
})], { label: 'June 2026', color: false })
for (const section of ['Totals', 'By tool', 'Top models', 'Highest-value days', 'Top projects', 'Daily', 'By activity', 'Tools']) {
expect(out).toContain(section)
}
expect(out).toContain('Opus 4.8') // model display name
expect(out).toContain('claude') // provider in By tool
expect(out).toContain('myproject') // clean project name from path basename
expect(out).toContain('$12.50')
expect(out).toContain('2026-06-15')
expect(out).toContain('Coding')
expect(out).toContain('Bash')
})
it('uses thousands separators and a B unit, and strips color in no-color mode', () => {
const out = renderOverview([makeProject({
project: 'big',
projectPath: '/Users/test/big',
cost: 1234.56,
calls: 10,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1_000_000, output: 1_000_000, cacheR: 2_000_000_000, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).toContain('$1,234.56')
// tokens render as full, comma-grouped numbers (not abbreviated)
expect(out).toContain('2,002,000,000')
// no-color mode must not emit ANSI escape codes
// eslint-disable-next-line no-control-regex
expect(out).not.toMatch(/\[/)
})
it('marks an estimated model row with a tilde and prints the legend once', () => {
const out = renderOverview([makeProject({
project: 'warpish',
projectPath: '/Users/test/warpish',
cost: 4.2,
calls: 2,
model: 'kiro-auto',
provider: 'kiro',
tokens: { input: 1000, output: 200, cacheR: 0, cacheW: 0 },
estimated: true,
})], { label: 'June 2026', color: false })
// The estimated cost carries the tilde marker in the Top models table...
expect(out).toContain('~$4.20')
// ...and the legend explains it exactly once.
expect(out).toContain('~ estimated cost (priced from estimated tokens)')
})
it('does not mark a measured model row', () => {
const out = renderOverview([makeProject({
project: 'metered',
projectPath: '/Users/test/metered',
cost: 4.2,
calls: 2,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 0, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).toContain('$4.20')
expect(out).not.toContain('~$4.20')
expect(out).not.toContain('estimated cost (priced from estimated tokens)')
})
it('reports no usage for an empty range', () => {
const out = renderOverview([], { label: 'June 2026', color: false })
expect(out).toContain('No usage found for June 2026')
})
it('does not render a budget line when no budget is provided', () => {
const out = renderOverview([makeProject({
project: 'myproject',
projectPath: '/Users/test/myproject',
cost: 12.5,
calls: 3,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 5000, cacheW: 100 },
})], { label: 'June 2026', color: false })
expect(out).not.toContain('budget:')
})
it('renders a configured monthly budget line with percent and projection', () => {
const out = renderOverview([makeProject({
project: 'myproject',
projectPath: '/Users/test/myproject',
cost: 80,
calls: 3,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 5000, cacheW: 100 },
})], {
label: 'June 2026',
color: false,
budget: {
tier: 'monthly',
inProgress: true,
status: {
spent: 80,
budget: 120,
pct: 66.6666666667,
projected: 160,
state: 'under',
},
},
})
expect(out).toContain('Monthly budget: $80.00 of $120.00 (66%)')
expect(out).toContain('projected $160.00 by month end')
})
it('does not display 100 percent while the raw state is warn at 99.6 percent', () => {
const out = renderOverview([makeProject({
project: 'myproject',
projectPath: '/Users/test/myproject',
cost: 99.6,
calls: 3,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 5000, cacheW: 100 },
})], {
label: 'June 2026',
color: false,
budget: {
tier: 'monthly',
inProgress: false,
status: {
spent: 99.6,
budget: 100,
pct: 99.6,
projected: 99.6,
state: 'warn',
},
},
})
expect(out).toContain('Monthly budget: $99.60 of $100.00 (99%)')
expect(out).not.toContain('(100%)')
})
it('does not split a slug-only Claude project path into fake path segments', () => {
const out = renderOverview([makeProject({
project: 'Projects-Content-OS',
projectPath: 'Projects/Content/OS',
cost: 3.25,
calls: 1,
model: 'claude-sonnet-4-5',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 0, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).toContain('Projects-Content-OS')
expect(out).not.toContain(' OS ')
})
})
describe('renderOverview unpriced models', () => {
it('warns when a model with usage has no pricing data', () => {
const out = renderOverview([makeProject({
project: 'mystery',
projectPath: '/Users/test/mystery',
cost: 0,
calls: 4,
model: 'zz-mystery-paid-model-999',
provider: 'claude',
tokens: { input: 1000, output: 200, cacheR: 0, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).toContain('Unpriced')
expect(out).toContain('1 model at $0')
expect(out).toContain('zz-mystery-paid-model-999')
expect(out).toContain('codeburn model-alias')
expect(out).toContain('model-flat-rate')
expect(out).not.toContain('Fix: codeburn model-alias')
})
it('stays silent for subscription SKUs whose $0 is correct', () => {
const out = renderOverview([makeProject({
project: 'pass',
projectPath: '/Users/test/pass',
cost: 0,
calls: 4,
model: 'auto-genius',
provider: 'cline-cli',
tokens: { input: 1000, output: 200, cacheR: 0, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).not.toContain('Unpriced')
expect(out).not.toContain('model-alias')
})
it('stays silent when every model is priced', () => {
const out = renderOverview([makeProject({
project: 'priced',
projectPath: '/Users/test/priced',
cost: 5,
calls: 2,
model: 'claude-opus-4-8',
provider: 'claude',
tokens: { input: 100, output: 50, cacheR: 0, cacheW: 0 },
})], { label: 'June 2026', color: false })
expect(out).not.toContain('Unpriced')
})
})