codeburn/packages/cli/tests/overview.test.ts

258 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')
})
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')
})
})