4228c87595
Unpriced warnings were firing on flat-rate product ids and telling users to model-alias them, which invents spend.
276 lines
8.9 KiB
TypeScript
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')
|
|
})
|
|
})
|