Files
orca/src/main/codex-usage/store.test.ts
T

905 lines
28 KiB
TypeScript

/* eslint-disable max-lines */
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, 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
}
}))
vi.mock('./scanner', () => ({
scanCodexUsageFiles: vi.fn()
}))
import { CodexUsageStore, initCodexUsagePath, normalizePersistedState } from './store'
import { scanCodexUsageFiles } from './scanner'
function createEmptyScanResult() {
return {
processedFiles: [],
sessions: [],
dailyAggregates: []
}
}
function createStoreWithState(state: Partial<CodexUsagePersistedState>): CodexUsageStore {
const store = new CodexUsageStore({
getRepos: () => [],
getAllWorktreeMeta: () => ({})
})
;(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', () => {
let tempUserData: string
beforeEach(() => {
tempUserData = mkdtempSync(join(tmpdir(), 'orca-codex-usage-store-'))
getPathMock.mockReturnValue(tempUserData)
initCodexUsagePath()
vi.mocked(scanCodexUsageFiles).mockReset()
vi.mocked(scanCodexUsageFiles).mockResolvedValue(createEmptyScanResult())
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-10T12:00:00.000-04:00'))
})
afterEach(() => {
vi.useRealTimers()
rmSync(tempUserData, { recursive: true, force: true })
})
it('adapts Codex scans to compact cache persistence', async () => {
const store = createStoreWithState({
schemaVersion: 5,
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
await store.refresh(true)
const persistedJson = readFileSync(join(tempUserData, 'orca-codex-usage.json'), 'utf-8')
expect(scanCodexUsageFiles).toHaveBeenCalledWith([], [])
expect(persistedJson).toBe(JSON.stringify(JSON.parse(persistedJson)))
})
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('prices GPT-5.6 sol, terra, and luna with current OpenAI rates', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'].map((model) => ({
day: '2026-04-09',
model,
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(85.7208)
expect(breakdown.find((row) => row.key === 'gpt-5.6-sol')?.estimatedCostUsd).toBeCloseTo(50.424)
expect(breakdown.find((row) => row.key === 'gpt-5.6-terra')?.estimatedCostUsd).toBeCloseTo(
25.212
)
expect(breakdown.find((row) => row.key === 'gpt-5.6-luna')?.estimatedCostUsd).toBeCloseTo(
10.0848
)
})
it('normalizes GPT-5.6 reasoning suffixes before pricing', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6-terra-high', 'gpt-5.6-luna(medium)'].map((model) => ({
day: '2026-04-09',
model,
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.6-terra-high')?.estimatedCostUsd).toBeCloseTo(
0.5125
)
expect(
breakdown.find((row) => row.key === 'gpt-5.6-luna(medium)')?.estimatedCostUsd
).toBeCloseTo(0.205)
})
it('prices the bare gpt-5.6 alias at Sol rates without shadowing the tier IDs', async () => {
const store = createStoreWithState({
dailyAggregates: ['gpt-5.6', 'gpt-5.6-luna'].map((model) => ({
day: '2026-04-09',
model,
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.6')?.estimatedCostUsd).toBeCloseTo(1.025)
expect(breakdown.find((row) => row.key === 'gpt-5.6-luna')?.estimatedCostUsd).toBeCloseTo(0.205)
})
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: 5,
worktreeFingerprint: null,
processedFiles: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
})
it('returns automation usage for a single matching worktree session', async () => {
const worktreeId = 'repo-1::/workspace/repo'
const store = createStoreWithState({
scanState: {
enabled: true,
lastScanStartedAt: 1,
lastScanCompletedAt: 2,
lastScanError: null
},
sessions: [
{
sessionId: 'session-1',
firstTimestamp: '2026-04-10T15:00:00.000Z',
lastTimestamp: '2026-04-10T15:05:00.000Z',
primaryModel: 'gpt-5',
hasMixedModels: false,
primaryProjectLabel: 'Repo',
hasMixedLocations: false,
primaryWorktreeId: worktreeId,
primaryRepoId: 'repo-1',
eventCount: 1,
totalInputTokens: 1000,
totalCachedInputTokens: 400,
totalOutputTokens: 250,
totalReasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false,
locationBreakdown: [
{
locationKey: `worktree:${worktreeId}`,
projectLabel: 'Repo',
repoId: 'repo-1',
worktreeId,
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: `worktree:${worktreeId}`,
modelKey: 'gpt-5',
modelLabel: 'gpt-5',
repoId: 'repo-1',
worktreeId,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1250,
hasInferredPricing: false
}
]
}
]
})
const refreshMock = vi.fn().mockResolvedValue({
enabled: true,
isScanning: false,
lastScanStartedAt: 1,
lastScanCompletedAt: 2,
lastScanError: null,
hasAnyCodexData: true
})
;(store as unknown as { refresh: typeof store.refresh }).refresh = refreshMock
const completedAt = new Date('2026-04-10T15:06:00.000Z').getTime()
const request = {
worktreeId,
terminalSessionId: 'tab-1',
startedAt: new Date('2026-04-10T14:59:00.000Z').getTime(),
completedAt
}
const usage = await store.getAutomationRunUsage(request)
expect(usage.status).toBe('known')
expect(usage.providerSessionId).toBe('session-1')
expect(usage.cacheReadTokens).toBe(400)
expect(usage.reasoningOutputTokens).toBe(100)
expect(usage.estimatedCostUsd).toBeCloseTo(0.0033)
expect(refreshMock).toHaveBeenCalledWith(true)
;(store as unknown as { state: CodexUsagePersistedState }).state.scanState.lastScanCompletedAt =
completedAt + 1000
refreshMock.mockClear()
await store.getAutomationRunUsage(request)
expect(refreshMock).toHaveBeenCalledWith(false)
})
})