Files
orca/src/main/codex-usage/store.test.ts
T
2026-05-15 13:16:55 -07:00

665 lines
20 KiB
TypeScript

/* eslint-disable max-lines */
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { CodexUsagePersistedState } from './types'
const { getPathMock } = vi.hoisted(() => ({
getPathMock: vi.fn(() => '/tmp/orca-test-userdata')
}))
vi.mock('electron', () => ({
app: {
getPath: getPathMock
}
}))
import { CodexUsageStore, normalizePersistedState } from './store'
function createStoreWithState(state: Partial<CodexUsagePersistedState>): CodexUsageStore {
const store = new CodexUsageStore({
getRepos: () => [],
getWorktreeMeta: () => undefined
} as never)
;(store as unknown as { state: CodexUsagePersistedState }).state = {
schemaVersion: 1,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: false,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
},
...state
}
return store
}
describe('CodexUsageStore', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-10T12:00:00.000-04:00'))
})
it('reports no data for Orca scope when only non-Orca Codex usage exists', async () => {
const store = createStoreWithState({
sessions: [
{
sessionId: 'session-1',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: 'gpt-5',
hasMixedModels: false,
primaryProjectLabel: 'outside/repo',
hasMixedLocations: false,
primaryWorktreeId: null,
primaryRepoId: null,
eventCount: 1,
totalInputTokens: 1000,
totalCachedInputTokens: 400,
totalOutputTokens: 250,
totalReasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false,
locationBreakdown: [
{
locationKey: 'cwd:/outside/repo',
projectLabel: 'outside/repo',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
],
modelBreakdown: [
{
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
],
locationModelBreakdown: [
{
locationKey: 'cwd:/outside/repo',
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
]
}
],
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5',
projectKey: 'cwd:/outside/repo',
projectLabel: 'outside/repo',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
]
})
const summary = await store.getSummary('orca', '30d')
expect(summary.hasAnyCodexData).toBe(false)
expect(summary.sessions).toBe(0)
expect(summary.events).toBe(0)
})
it('calculates cost from uncached input plus cached input without double billing', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 2,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
]
})
const summary = await store.getSummary('orca', '30d')
expect(summary.estimatedCostUsd).toBeCloseTo(0.0014)
expect(summary.totalTokens).toBe(1250)
expect(summary.reasoningOutputTokens).toBe(100)
})
it('prices current Codex models with current model rates', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5.2-codex',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.3-codex',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.4',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.5',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
}
]
})
const summary = await store.getSummary('orca', '30d')
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(summary.estimatedCostUsd).toBeCloseTo(107.486)
expect(breakdown.find((row) => row.key === 'gpt-5.2-codex')?.estimatedCostUsd).toBeCloseTo(
15.925
)
expect(breakdown.find((row) => row.key === 'gpt-5.3-codex')?.estimatedCostUsd).toBeCloseTo(
15.925
)
expect(breakdown.find((row) => row.key === 'gpt-5.4')?.estimatedCostUsd).toBeCloseTo(25.212)
expect(breakdown.find((row) => row.key === 'gpt-5.5')?.estimatedCostUsd).toBeCloseTo(50.424)
})
it('normalizes Codex model variants and reasoning suffixes before pricing', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5.4-mini-high',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 1_000_000,
cachedInputTokens: 500_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 2_000_000,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.3-codex-spark-xhigh',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 2_000_000,
cachedInputTokens: 1_000_000,
outputTokens: 1_000_000,
reasoningOutputTokens: 100_000,
totalTokens: 3_000_000,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.5(xhigh)',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100_000,
cachedInputTokens: 50_000,
outputTokens: 25_000,
reasoningOutputTokens: 5_000,
totalTokens: 125_000,
hasInferredPricing: false
}
]
})
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(breakdown.find((row) => row.key === 'gpt-5.4-mini-high')?.estimatedCostUsd).toBeCloseTo(
4.9125
)
expect(
breakdown.find((row) => row.key === 'gpt-5.3-codex-spark-xhigh')?.estimatedCostUsd
).toBeCloseTo(15.925)
expect(breakdown.find((row) => row.key === 'gpt-5.5(xhigh)')?.estimatedCostUsd).toBeCloseTo(
1.025
)
})
it('keeps cached input out of the full-price input bucket for GPT-5.5 totals', async () => {
const store = createStoreWithState({
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5.5',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 491_053_514,
cachedInputTokens: 459_283_584,
outputTokens: 1_944_952,
reasoningOutputTokens: 551_764,
totalTokens: 492_998_466,
hasInferredPricing: false
}
]
})
const summary = await store.getSummary('orca', '30d')
expect(summary.estimatedCostUsd).toBeCloseTo(858.929724)
})
it('counts mixed-model sessions once for each real model row in the breakdown', async () => {
const store = createStoreWithState({
sessions: [
{
sessionId: 'session-1',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: 'Mixed models',
hasMixedModels: true,
primaryProjectLabel: 'Repo',
hasMixedLocations: false,
primaryWorktreeId: 'repo-1::/workspace/repo',
primaryRepoId: 'repo-1',
eventCount: 2,
totalInputTokens: 300,
totalCachedInputTokens: 100,
totalOutputTokens: 90,
totalReasoningOutputTokens: 10,
totalTokens: 390,
hasInferredPricing: false,
locationBreakdown: [
{
locationKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 2,
inputTokens: 300,
cachedInputTokens: 100,
outputTokens: 90,
reasoningOutputTokens: 10,
totalTokens: 390,
hasInferredPricing: false
}
],
modelBreakdown: [
{
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
modelKey: 'gpt-5.2-codex',
modelLabel: 'gpt-5.2-codex',
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 80,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
],
locationModelBreakdown: [
{
locationKey: 'worktree:repo-1::/workspace/repo',
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
locationKey: 'worktree:repo-1::/workspace/repo',
modelKey: 'gpt-5.2-codex',
modelLabel: 'gpt-5.2-codex',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 80,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
]
}
],
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.2-codex',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 80,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
]
})
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(breakdown.find((row) => row.key === 'gpt-5')?.sessions).toBe(1)
expect(breakdown.find((row) => row.key === 'gpt-5.2-codex')?.sessions).toBe(1)
})
it('uses only Orca-scoped models when projecting mixed-scope sessions', async () => {
const store = createStoreWithState({
sessions: [
{
sessionId: 'session-1',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: 'Mixed models',
hasMixedModels: true,
primaryProjectLabel: 'Multiple locations',
hasMixedLocations: true,
primaryWorktreeId: 'repo-1::/workspace/repo',
primaryRepoId: 'repo-1',
eventCount: 2,
totalInputTokens: 300,
totalCachedInputTokens: 60,
totalOutputTokens: 90,
totalReasoningOutputTokens: 10,
totalTokens: 390,
hasInferredPricing: false,
locationBreakdown: [
{
locationKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
locationKey: 'cwd:/outside/repo',
projectLabel: 'outside/repo',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 40,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
],
modelBreakdown: [
{
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
modelKey: 'gpt-5.2-codex',
modelLabel: 'gpt-5.2-codex',
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 40,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
],
locationModelBreakdown: [
{
locationKey: 'worktree:repo-1::/workspace/repo',
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
locationKey: 'cwd:/outside/repo',
modelKey: 'gpt-5.2-codex',
modelLabel: 'gpt-5.2-codex',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 40,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
]
}
],
dailyAggregates: [
{
day: '2026-04-09',
model: 'gpt-5',
projectKey: 'worktree:repo-1::/workspace/repo',
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId: 'repo-1::/workspace/repo',
eventCount: 1,
inputTokens: 100,
cachedInputTokens: 20,
outputTokens: 30,
reasoningOutputTokens: 5,
totalTokens: 130,
hasInferredPricing: false
},
{
day: '2026-04-09',
model: 'gpt-5.2-codex',
projectKey: 'cwd:/outside/repo',
projectLabel: 'outside/repo',
repoId: null,
worktreeId: null,
eventCount: 1,
inputTokens: 200,
cachedInputTokens: 40,
outputTokens: 60,
reasoningOutputTokens: 5,
totalTokens: 260,
hasInferredPricing: false
}
]
})
const breakdown = await store.getBreakdown('orca', '30d', 'model')
const recentSessions = await store.getRecentSessions('orca', '30d', 10)
expect(breakdown.find((row) => row.key === 'gpt-5')?.sessions).toBe(1)
expect(breakdown.find((row) => row.key === 'gpt-5.2-codex')).toBeUndefined()
expect(recentSessions[0]?.projectLabel).toBe('Repo')
expect(recentSessions[0]?.model).toBe('gpt-5')
})
it('drops persisted caches from older schemas that lack scoped model breakdown data', () => {
const normalized = normalizePersistedState({
schemaVersion: 1,
processedFiles: [],
sessions: [
{
sessionId: 'legacy',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: 'gpt-5',
hasMixedModels: false,
primaryProjectLabel: 'Repo',
hasMixedLocations: false,
primaryWorktreeId: 'repo-1::/workspace/repo',
primaryRepoId: 'repo-1',
eventCount: 1,
totalInputTokens: 1,
totalCachedInputTokens: 0,
totalOutputTokens: 1,
totalReasoningOutputTokens: 0,
totalTokens: 2,
hasInferredPricing: false,
locationBreakdown: [],
modelBreakdown: []
}
],
dailyAggregates: [],
scanState: {
enabled: true,
lastScanStartedAt: 1,
lastScanCompletedAt: 2,
lastScanError: null
}
} as unknown as CodexUsagePersistedState)
expect(normalized).toEqual({
schemaVersion: 3,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: false,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
})
})