codeburn/tests/menubar-json.test.ts
Resham Joshi c826f1a479
fix(models): price kimi-for-coding by the model it served on the call date (#1676)
* fix(models): price kimi-for-coding by the model it served on the call date

kimi-for-coding is a moving alias. It priced as retired K2 Thinking for
every call. It now prices as K2.5 from 27 Jan 2026, K2.6 from 13 Apr and
K2.7 Code from 12 Jun (K2.8 Preview from 11 Sep has no published price,
so it stays on K2.7 Code). kimi-for-coding-highspeed prices at Moonshot's
K2.7 Code HighSpeed rate instead of $0. Daily cache v64.

* fix(models): mark K2.8 Preview kimi-for-coding calls as estimated

From 11 Sep 2026 kimi-for-coding served K2.8 Preview, which has no
published rate and prices as K2.7 Code. Those calls now carry the
estimated flag. The legacy kimi provider prices by the dated model too.

* fix(models): flag the K2.8 Preview stand-in where every call is read

cachedCallToApiCall now marks kimi-for-coding calls from 11 Sep 2026
estimated, so the flag reaches every provider and cached reads without a
parse bump. Calls with a reported cost are left alone. The kimi provider's
own flag and its parse version entry go, as they only duplicated this.
2026-10-08 00:02:38 +02:00

656 lines
29 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { buildMenubarPayload, type CombinedUsage, type LocalModelSavings, type PeriodData, type ProviderCost } from '../src/menubar-json.js'
import { getShortModelName } from '../src/models.js'
import type { OptimizeResult } from '../src/optimize.js'
function emptyPeriod(label: string): PeriodData {
return {
label,
cost: 0,
calls: 0,
sessions: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
categories: [],
models: [],
}
}
describe('buildMenubarPayload', () => {
it('emits the full schema with current-period metrics and iso timestamp', () => { const period: PeriodData = {
label: '7 Days',
cost: 1248.01,
calls: 11231,
sessions: 97,
inputTokens: 19100,
outputTokens: 675600,
cacheReadTokens: 0,
cacheWriteTokens: 0,
categories: [],
models: [],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.generated).toMatch(/^\d{4}-\d{2}-\d{2}T/)
expect(payload.current.label).toBe('7 Days')
expect(payload.current.cost).toBe(1248.01)
expect(payload.current.calls).toBe(11231)
// An absent pricingCoverage is UNKNOWN and must render as null, never as
// a fabricated 100% coverage (post-#756 review finding).
expect(payload.current.pricingCoverage).toBeNull()
expect(payload.current.sessions).toBe(97)
expect(payload.current.inputTokens).toBe(19100)
expect(payload.current.outputTokens).toBe(675600)
})
it('carries drill-through session identity (sessionId, provider) into topSessions and project sessionDetails', () => {
const period: PeriodData = {
...emptyPeriod('7 Days'),
projects: [{
id: '/abs/project-a',
name: 'project-a',
cost: 5,
savingsUSD: 0,
sessions: 1,
sessionDetails: [{
cost: 5, savingsUSD: 0, calls: 3, inputTokens: 100, outputTokens: 20,
date: '2026-09-10',
models: [{ name: 'Sonnet 4.5', cost: 5, savingsUSD: 0 }],
sessionId: 'session-a1',
provider: 'claude',
}],
}],
topSessions: [{
project: 'project-a', cost: 5, savingsUSD: 0, calls: 3, date: '2026-09-10',
sessionId: 'session-a1', provider: 'claude',
}],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.topSessions[0]).toMatchObject({ sessionId: 'session-a1', provider: 'claude', project: 'project-a' })
expect(payload.current.topProjects[0]!.sessionDetails[0]).toMatchObject({ sessionId: 'session-a1', provider: 'claude' })
})
it('passes the pull-requests payload (models, categories, cap remainder) through verbatim', () => {
const period: PeriodData = {
...emptyPeriod('7 Days'),
pullRequests: {
rows: [
{ url: 'https://github.com/o/r/pull/1', label: 'o/r#1', cost: 40, savingsUSD: 0, sessions: 1, calls: 12, firstStarted: '2026-07-20T10:00:00Z', lastEnded: '2026-07-20T11:00:00Z', approx: false, models: ['fable', 'opus'], categories: [{ name: 'Coding', cost: 30 }, { name: 'Debugging', cost: 10 }] },
],
distinctCost: 45,
distinctSessions: 1,
attributedCost: 40,
unattributedCost: 5,
},
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.pullRequests).toEqual(period.pullRequests)
expect(payload.current.pullRequests!.rows[0]!.models).toEqual(['fable', 'opus'])
expect(payload.current.pullRequests!.rows[0]!.categories).toEqual([{ name: 'Coding', cost: 30 }, { name: 'Debugging', cost: 10 }])
})
it('exposes period-scoped cache tokens on current, decoupled from the 365-day history backfill (#583)', () => {
const period: PeriodData = {
label: '30 Days',
cost: 5, calls: 10, sessions: 2,
inputTokens: 1000, outputTokens: 2000,
// What `report -p 30days` shows in the terminal for the same window.
cacheReadTokens: 1_391_100_000,
cacheWriteTokens: 42_000_000,
categories: [], models: [],
}
// history.daily is the full BACKFILL_DAYS (365) trend, whose cache totals are
// far larger than the selected period. The web cards used to sum these, which
// is the bug in #583. current must mirror the period, not the backfill.
const dailyHistory = [
{ date: '2025-07-15', cost: 0, savingsUSD: 0, calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 1_800_000_000, cacheWriteTokens: 60_000_000, topModels: [] },
{ date: '2026-06-30', cost: 0, savingsUSD: 0, calls: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 270_000_000, cacheWriteTokens: 12_000_000, topModels: [] },
]
const payload = buildMenubarPayload(period, [], null, dailyHistory)
// current carries the period totals verbatim ...
expect(payload.current.cacheReadTokens).toBe(1_391_100_000)
expect(payload.current.cacheWriteTokens).toBe(42_000_000)
// ... and is independent of the larger history-backfill sum the cards used before.
const historyReadSum = dailyHistory.reduce((s, d) => s + d.cacheReadTokens, 0)
const historyWriteSum = dailyHistory.reduce((s, d) => s + d.cacheWriteTokens, 0)
expect(historyReadSum).toBeGreaterThan(payload.current.cacheReadTokens)
expect(historyWriteSum).toBeGreaterThan(payload.current.cacheWriteTokens)
})
it('computes per-category oneShotRate from editTurns and skips categories without edits', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [
{ name: 'Coding', cost: 15.83, turns: 7, editTurns: 7, oneShotTurns: 6 },
{ name: 'Conversation', cost: 16.69, turns: 47, editTurns: 0, oneShotTurns: 0 },
],
models: [],
}
const payload = buildMenubarPayload(period, [], null)
const coding = payload.current.topActivities.find(a => a.name === 'Coding')!
expect(coding.oneShotRate).toBeCloseTo(6 / 7)
const conv = payload.current.topActivities.find(a => a.name === 'Conversation')!
expect(conv.oneShotRate).toBeNull()
})
it('computes aggregate oneShotRate across categories with edits', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [
{ name: 'Coding', cost: 1, turns: 7, editTurns: 10, oneShotTurns: 8 },
{ name: 'Debugging', cost: 1, turns: 5, editTurns: 10, oneShotTurns: 6 },
{ name: 'Conversation', cost: 1, turns: 40, editTurns: 0, oneShotTurns: 0 },
],
models: [],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.oneShotRate).toBeCloseTo((8 + 6) / (10 + 10))
})
it('returns null aggregate oneShotRate when no categories have editTurns', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [{ name: 'Conversation', cost: 1, turns: 5, editTurns: 0, oneShotTurns: 0 }],
models: [],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.oneShotRate).toBeNull()
})
it('filters out the synthetic model and keeps every model row uncapped (#1318)', () => {
const models = Array.from({ length: 30 }, (_, i) => ({
name: `Model${i}`, cost: 30 - i, calls: 100,
}))
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [{ name: '<synthetic>', cost: 99, calls: 0 }, ...models],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.topModels.find(m => m.name === '<synthetic>')).toBeUndefined()
// The Overview model table renders this list whole, so a cap would drop
// exactly the local/free models at its tail.
expect(payload.current.topModels).toHaveLength(30)
expect(payload.current.topModels[0].name).toBe('Model0')
expect(payload.current.topModels[29].name).toBe('Model29')
})
it('resolves raw model ids to display names in topModels and merges rows that collapse', () => {
// Day entries key models by the raw wire id (day-aggregator); the menubar
// must show friendly names and merge ids that share one (k3 + kimi-k3).
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [
{ name: 'k3', cost: 2.5, calls: 78 },
{ name: 'kimi-k3', cost: 0.5, calls: 2 },
{ name: 'kimi-for-coding', cost: 0.06, calls: 13 },
{ name: 'k3-agent', cost: 1.2, calls: 40 },
],
}
const payload = buildMenubarPayload(period, [], null)
const kimiK3 = payload.current.topModels.find(m => m.name === 'Kimi K3')!
expect(kimiK3.cost).toBeCloseTo(2.5 + 0.5 + 1.2)
expect(kimiK3.calls).toBe(78 + 2 + 40)
const k2 = payload.current.topModels.find(m => m.name === 'Kimi for Coding')!
expect(k2.cost).toBeCloseTo(0.06)
expect(payload.current.topModels.find(m => m.name === 'k3')).toBeUndefined()
expect(payload.current.topModels.find(m => m.name === 'k3-agent')).toBeUndefined()
})
it('merges per-model token counts under the same display-name grouping as cost', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [
{ name: 'k3', cost: 2.5, calls: 78, inputTokens: 1000, outputTokens: 200, cacheReadTokens: 3000, cacheWriteTokens: 400 },
{ name: 'kimi-k3', cost: 0.5, calls: 2, inputTokens: 10, outputTokens: 20, cacheReadTokens: 30, cacheWriteTokens: 40 },
],
}
const payload = buildMenubarPayload(period, [], null)
const kimiK3 = payload.current.topModels.find(m => m.name === 'Kimi K3')!
expect(kimiK3.inputTokens).toBe(1010)
expect(kimiK3.outputTokens).toBe(220)
expect(kimiK3.cacheReadTokens).toBe(3030)
expect(kimiK3.cacheWriteTokens).toBe(440)
})
it('keeps known-zero per-model counts as zeros instead of dashes or drops', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [
{ name: 'k3', cost: 0, calls: 3, inputTokens: 0, outputTokens: 0, cacheReadTokens: 500, cacheWriteTokens: 0 },
],
}
const payload = buildMenubarPayload(period, [], null)
const row = payload.current.topModels[0]!
expect(row.inputTokens).toBe(0)
expect(row.outputTokens).toBe(0)
expect(row.cacheReadTokens).toBe(500)
expect(row.cacheWriteTokens).toBe(0)
})
it('omits per-model counts for a row any legacy contributor without counts folded into', () => {
// A period assembled from older rows that never carried counts must not
// grow a plausible-looking partial sum: unknown stays absent.
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [
{ name: 'k3', cost: 2.5, calls: 78, inputTokens: 1000, outputTokens: 200, cacheReadTokens: 3000, cacheWriteTokens: 400 },
{ name: 'kimi-k3', cost: 0.5, calls: 2 },
{ name: 'kimi-for-coding', cost: 0.06, calls: 13, inputTokens: 5, outputTokens: 6, cacheReadTokens: 7, cacheWriteTokens: 8 },
],
}
const payload = buildMenubarPayload(period, [], null)
const merged = payload.current.topModels.find(m => m.name === 'Kimi K3')!
expect(merged.inputTokens).toBeUndefined()
expect(merged.outputTokens).toBeUndefined()
expect(merged.cacheReadTokens).toBeUndefined()
expect(merged.cacheWriteTokens).toBeUndefined()
// A row whose every contributor carried counts keeps them.
const intact = payload.current.topModels.find(m => m.name === getShortModelName('kimi-for-coding'))!
expect(intact.inputTokens).toBe(5)
expect(intact.outputTokens).toBe(6)
expect(intact.cacheReadTokens).toBe(7)
expect(intact.cacheWriteTokens).toBe(8)
})
it('keeps merged counts unknown regardless of the order contributors arrive in', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: [],
models: [
{ name: 'kimi-k3', cost: 0.5, calls: 2 },
{ name: 'k3', cost: 2.5, calls: 78, inputTokens: 1000, outputTokens: 200, cacheReadTokens: 3000, cacheWriteTokens: 400 },
],
}
const payload = buildMenubarPayload(period, [], null)
const merged = payload.current.topModels.find(m => m.name === 'Kimi K3')!
expect(merged.inputTokens).toBeUndefined()
expect(merged.cacheWriteTokens).toBeUndefined()
})
it('caps topActivities at 20 so all task categories can surface', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
categories: Array.from({ length: 25 }, (_, i) => ({
name: `Cat${i}`, cost: 1, turns: 1, editTurns: 1, oneShotTurns: 1,
})),
models: [],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.topActivities).toHaveLength(20)
})
it('computes cacheHitPercent from cache reads over input plus cache reads', () => {
const period: PeriodData = {
label: 'Today',
cost: 0, calls: 0, sessions: 0,
inputTokens: 100,
outputTokens: 200,
cacheReadTokens: 900,
cacheWriteTokens: 0,
categories: [],
models: [],
}
const payload = buildMenubarPayload(period, [], null)
expect(payload.current.cacheHitPercent).toBeCloseTo(90)
})
it('returns zero cacheHitPercent when there is no input or cache traffic', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.current.cacheHitPercent).toBe(0)
})
it('handles null optimize as empty findings block', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.optimize).toEqual({ findingCount: 0, savingsUSD: 0, topFindings: [] })
})
it('converts tokensSaved to savingsUSD via costRate and caps topFindings at 10', () => {
const findings = Array.from({ length: 15 }, (_, i) => ({
title: `F${i}`, explanation: '', impact: 'low' as const, tokensSaved: 1000,
fix: { type: 'paste' as const, label: '', text: '' },
}))
const optimize: OptimizeResult = {
findings,
costRate: 0.00002,
healthScore: 60,
healthGrade: 'C',
}
const payload = buildMenubarPayload(emptyPeriod('Today'), [], optimize)
expect(payload.optimize.findingCount).toBe(15)
expect(payload.optimize.topFindings).toHaveLength(10)
expect(payload.optimize.topFindings[0].title).toBe('F0')
expect(payload.optimize.topFindings[0].savingsUSD).toBeCloseTo(1000 * 0.00002)
expect(payload.optimize.savingsUSD).toBeCloseTo(15 * 1000 * 0.00002)
})
it('maps providers into a lowercased display-name dict inside the current-period block', () => {
const providers: ProviderCost[] = [
{ name: 'cursor-agent', displayName: 'Cursor Agent', cost: 76.45 },
{ name: 'cursor', displayName: 'Cursor', cost: 2.18 },
{ name: 'codex', displayName: 'Codex', cost: 1.5 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
// Keys stay lowercased DISPLAY names (byte-compatible with the Swift menubar).
expect(payload.current.providers).toEqual({ 'cursor agent': 76.45, cursor: 2.18, codex: 1.5 })
})
it('emits providerDetails with the internal id and display label alongside the providers map', () => {
const providers: ProviderCost[] = [
{ name: 'grok', displayName: 'Grok Build', cost: 12.5 },
{ name: 'cursor-agent', displayName: 'Cursor Agent', cost: 3.4 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
// providerDetails carries the internal id (round-trips as --provider) + label.
expect(payload.current.providerDetails).toEqual([
{ id: 'grok', label: 'Grok Build', cost: 12.5, calls: 0, hasUsage: true },
{ id: 'cursor-agent', label: 'Cursor Agent', cost: 3.4, calls: 0, hasUsage: true },
])
// ... while the existing providers map keys stay the lowercased display names.
expect(payload.current.providers).toEqual({ 'grok build': 12.5, 'cursor agent': 3.4 })
})
it('carries per-provider tokens and sessions into providerDetails', () => {
const providers: ProviderCost[] = [
{ name: 'claude', displayName: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12 },
{ name: 'codex', displayName: 'Codex', cost: 88.84, calls: 642, hasUsage: true, inputTokens: 3_000_000, outputTokens: 150_000, sessions: 4 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providerDetails).toEqual([
{ id: 'claude', label: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12 },
{ id: 'codex', label: 'Codex', cost: 88.84, calls: 642, hasUsage: true, inputTokens: 3_000_000, outputTokens: 150_000, sessions: 4 },
])
})
it('carries per-provider cache read into providerDetails', () => {
const providers: ProviderCost[] = [
{ name: 'claude', displayName: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12, cacheReadTokens: 4_200_000 },
{ name: 'codex', displayName: 'Codex', cost: 88.84, calls: 642, hasUsage: true, inputTokens: 3_000_000, outputTokens: 150_000, sessions: 4, cacheReadTokens: 0 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providerDetails).toEqual([
{ id: 'claude', label: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12, cacheReadTokens: 4_200_000 },
{ id: 'codex', label: 'Codex', cost: 88.84, calls: 642, hasUsage: true, inputTokens: 3_000_000, outputTokens: 150_000, sessions: 4, cacheReadTokens: 0 },
])
})
it('omits the cache-read key entirely when no day reported it', () => {
// Add-only contract: absent means unknown, not zero, so a legacy row must
// stay absent rather than being emitted as 0.
const payload = buildMenubarPayload(
emptyPeriod('Today'),
[{ name: 'claude', displayName: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12 }],
null,
)
expect(payload.current.providerDetails).toEqual([
{ id: 'claude', label: 'Claude', cost: 190.1, calls: 900, hasUsage: true, inputTokens: 6_000_000, outputTokens: 250_000, sessions: 12 },
])
expect(Object.keys(payload.current.providerDetails[0]!)).not.toContain('cacheReadTokens')
})
it('drops a partial cache-read sum when an active day lacked counts', () => {
// A fold that summed some days but missed a legacy active day must not be
// labelled complete: the emitter omits the key rather than publish a
// partial number.
const payload = buildMenubarPayload(
emptyPeriod('Today'),
[{ name: 'claude', displayName: 'Claude', cost: 190.1, calls: 900, hasUsage: true, cacheReadTokens: 4_200_000, cacheReadIncomplete: true }],
null,
)
expect(Object.keys(payload.current.providerDetails[0]!)).not.toContain('cacheReadTokens')
})
it('omits the token and session keys entirely when the period has no breakdown', () => {
// Add-only contract: a consumer must be able to tell "no breakdown" from
// zero, so absent stays absent rather than being emitted as 0.
const payload = buildMenubarPayload(
emptyPeriod('Today'),
[{ name: 'claude', displayName: 'Claude', cost: 190.1, calls: 900, hasUsage: true }],
null,
)
expect(payload.current.providerDetails).toEqual([
{ id: 'claude', label: 'Claude', cost: 190.1, calls: 900, hasUsage: true },
])
expect(Object.keys(payload.current.providerDetails[0]!)).not.toContain('inputTokens')
})
it('preserves zero-cost provider usage counts for menubar provider visibility', () => {
const providers = [
{ name: 'hermes', displayName: 'Hermes Agent', cost: 0, calls: 7 },
{ name: 'claude', displayName: 'Claude', cost: 0, calls: 0 },
] satisfies Array<ProviderCost & { calls: number }>
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providerDetails).toEqual([
{ id: 'hermes', label: 'Hermes Agent', cost: 0, calls: 7, hasUsage: true },
{ id: 'claude', label: 'Claude', cost: 0, calls: 0, hasUsage: false },
])
})
it('preserves token-only provider activity even when cost and behavioral calls are zero', () => {
const providers = [
{ name: 'hermes', displayName: 'Hermes Agent', cost: 0, calls: 0, hasUsage: true },
] satisfies Array<ProviderCost & { calls: number; hasUsage: boolean }>
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providerDetails).toEqual([
{ id: 'hermes', label: 'Hermes Agent', cost: 0, calls: 0, hasUsage: true },
])
})
it('keeps the legacy provider dict additive while providerDetails remains the activity authority', () => {
const providers: ProviderCost[] = [
{ name: 'claude', displayName: 'Claude', cost: 76.45 },
{ name: 'codex', displayName: 'Codex', cost: 0 },
{ name: 'cursor', displayName: 'Cursor', cost: 2.18 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providers).toEqual({ claude: 76.45, codex: 0, cursor: 2.18 })
expect(payload.current.providerDetails).toEqual([
{ id: 'claude', label: 'Claude', cost: 76.45, calls: 0, hasUsage: true },
{ id: 'codex', label: 'Codex', cost: 0, calls: 0, hasUsage: false },
{ id: 'cursor', label: 'Cursor', cost: 2.18, calls: 0, hasUsage: true },
])
})
it('includes up to 365 daily history entries sorted ascending by date', () => {
const history = Array.from({ length: 400 }, (_, i) => {
const d = new Date(2025, 0, 1)
d.setDate(d.getDate() + i)
return {
date: d.toISOString().slice(0, 10),
cost: i,
calls: i * 10,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: [],
}
})
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, history)
expect(payload.history.daily).toHaveLength(365)
expect(payload.history.daily[0]!.date < payload.history.daily[364]!.date).toBe(true)
expect(payload.history.daily[364]!.date).toBe(history[399]!.date)
})
it('preserves token fields in dailyHistory entries', () => {
const history = [
{ date: '2026-04-15', cost: 10, calls: 50, inputTokens: 100, outputTokens: 200, cacheReadTokens: 5000, cacheWriteTokens: 800, topModels: [{ name: 'Opus 4.7', cost: 8, calls: 40, inputTokens: 80, outputTokens: 160 }] },
{ date: '2026-04-16', cost: 20, calls: 75, inputTokens: 150, outputTokens: 350, cacheReadTokens: 8000, cacheWriteTokens: 1200, topModels: [] },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, history)
expect(payload.history.daily[0]).toEqual(history[0])
expect(payload.history.daily[1]).toEqual(history[1])
})
it('returns empty history when none supplied', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.history.daily).toEqual([])
})
it('emits the active display currency (USD by default) so the client can convert raw-USD costs', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.currency).toEqual({ code: 'USD', symbol: '$', rate: 1 })
})
it('preserves the optional selected-period timeline alongside daily history', () => {
const timeline = {
bucketMinutes: 15,
modelSeries: [{ id: 'model_0', label: 'claude-opus-4-6' }],
sessionSeries: [{ id: 'session_0', label: 'codeburn · abc123 (claude)' }],
points: [{
timestamp: '2026-07-15T10:00:00.000Z',
cost: 1.5,
tokens: 200,
models: [{ seriesId: 'model_0', cost: 1.5, tokens: 200 }],
sessions: [{ seriesId: 'session_0', cost: 1.5, tokens: 200 }],
}],
}
const payload = buildMenubarPayload(
emptyPeriod('Today'), [], null,
undefined, undefined, undefined, undefined, undefined,
timeline,
)
expect(payload.history).toEqual({ daily: [], timeline })
})
it('drops providers with negative cost defensively', () => {
const providers: ProviderCost[] = [
{ name: 'claude', displayName: 'Claude', cost: 76.45 },
{ name: 'broken', displayName: 'Broken', cost: -1 },
]
const payload = buildMenubarPayload(emptyPeriod('Today'), providers, null)
expect(payload.current.providers).toEqual({ claude: 76.45 })
expect(payload.current.providerDetails).toEqual([{ id: 'claude', label: 'Claude', cost: 76.45, calls: 0, hasUsage: true }])
})
it('omits combined usage by default and accepts the documented combined shape when attached', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload).not.toHaveProperty('combined')
const combined: CombinedUsage = {
perDevice: [
{
id: 'local',
name: 'Mac Studio',
local: true,
cost: 1,
calls: 2,
sessions: 1,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180,
},
],
combined: {
cost: 1,
calls: 2,
sessions: 1,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180,
deviceCount: 1,
reachableCount: 1,
},
}
payload.combined = combined
expect(payload.combined).toEqual(combined)
})
it('emits Claude config selector metadata only when multiple configs are available', () => {
const oneConfig = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, {
selectedId: null,
options: [{ id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' }],
})
expect(oneConfig).not.toHaveProperty('claudeConfigs')
const twoConfigs = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, {
selectedId: 'claude-config:b',
options: [
{ id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' },
{ id: 'claude-config:b', label: 'claude-personal', path: '/tmp/claude-personal' },
],
})
expect(twoConfigs.claudeConfigs).toEqual({
selectedId: 'claude-config:b',
options: [
{ id: 'claude-config:a', label: 'claude-work', path: '/tmp/claude-work' },
{ id: 'claude-config:b', label: 'claude-personal', path: '/tmp/claude-personal' },
],
})
})
it('sets stale:true when the caller reports an incomplete hydration', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null, undefined, undefined, undefined, undefined, undefined, undefined, true)
expect(payload.stale).toBe(true)
})
it('omits stale on a normal fresh build', () => {
const payload = buildMenubarPayload(emptyPeriod('Today'), [], null)
expect(payload.stale).toBeUndefined()
})
it('passes localModelSavings.byModel.outputTokens through as billable output', () => {
// usage-aggregator writes billableOutputTokens into this field while the
// provider is still known. Exclusive 10+3 → 13; inclusive 10+3 → 10.
const localModelSavings: LocalModelSavings = {
totalUSD: 2,
calls: 2,
byModel: [
{ name: 'Grok 4', calls: 1, actualUSD: 0, savingsUSD: 1, baselineModel: 'gpt-4o', inputTokens: 0, outputTokens: 13 },
{ name: 'GPT-5.4', calls: 1, actualUSD: 0, savingsUSD: 1, baselineModel: 'gpt-4o', inputTokens: 0, outputTokens: 10 },
],
byProvider: [
{ name: 'grok', calls: 1, savingsUSD: 1 },
{ name: 'codex', calls: 1, savingsUSD: 1 },
],
}
const period: PeriodData = { ...emptyPeriod('Today'), outputTokens: 23 }
const payload = buildMenubarPayload(period, [], null, undefined, undefined, undefined, { localModelSavings })
expect(payload.current.outputTokens).toBe(23)
expect(payload.current.localModelSavings.byModel).toEqual(localModelSavings.byModel)
expect(payload.current.localModelSavings.byModel[0]!.outputTokens).toBe(13)
expect(payload.current.localModelSavings.byModel[1]!.outputTokens).toBe(10)
})
})