chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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 }
|
||||
}): 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, 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('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 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user