Files
orca/src/main/opencode-usage/store.test.ts
T
Neil 97b71c2285 refactor(usage): split AI-usage scanners and stores under the max-lines budget (#14668)
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).

Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.

Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
2026-08-15 18:33:33 -07:00

356 lines
9.6 KiB
TypeScript

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 {
OpenCodeUsageDailyAggregate,
OpenCodeUsagePersistedState,
OpenCodeUsageSession
} from './types'
const { getPathMock } = vi.hoisted(() => ({
getPathMock: vi.fn(() => '/tmp/orca-test-userdata')
}))
vi.mock('electron', () => ({
app: {
getPath: getPathMock
}
}))
vi.mock('./scanner', () => ({
scanOpenCodeUsageDatabases: vi.fn()
}))
import { OpenCodeUsageStore, initOpenCodeUsagePath } from './store'
import { normalizePersistedState } from './persisted-state-normalization'
import { scanOpenCodeUsageDatabases } from './scanner'
function createEmptyScanResult() {
return {
processedDatabases: [],
sessions: [],
dailyAggregates: []
}
}
function getDefaultState(): OpenCodeUsagePersistedState {
return {
schemaVersion: 2,
worktreeFingerprint: null,
processedDatabases: [],
sessions: [],
dailyAggregates: [],
scanState: {
enabled: false,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
}
}
function createStoreWithState(state: Partial<OpenCodeUsagePersistedState>): OpenCodeUsageStore {
const store = new OpenCodeUsageStore({
getRepos: () => [],
getAllWorktreeMeta: () => ({})
})
;(store as unknown as { state: OpenCodeUsagePersistedState }).state = {
...getDefaultState(),
...state
}
return store
}
function makeSession(overrides: Partial<OpenCodeUsageSession> = {}): OpenCodeUsageSession {
const worktreeId = overrides.primaryWorktreeId ?? 'repo-1::/workspace/repo'
const repoId = overrides.primaryRepoId ?? 'repo-1'
const projectLabel = overrides.primaryProjectLabel ?? 'Repo'
const model = overrides.primaryModel ?? 'anthropic/claude-sonnet-4-5'
return {
sessionId: 'session-1',
firstTimestamp: '2026-04-09T10:00:00.000Z',
lastTimestamp: '2026-04-09T10:10:00.000Z',
primaryModel: model,
hasMixedModels: false,
primaryProjectLabel: projectLabel,
hasMixedLocations: false,
primaryWorktreeId: worktreeId,
primaryRepoId: repoId,
eventCount: 1,
totalInputTokens: 1000,
totalCachedInputTokens: 400,
totalOutputTokens: 250,
totalReasoningOutputTokens: 100,
totalTokens: 1350,
estimatedCostUsd: 0.05,
locationBreakdown: [
{
locationKey: worktreeId ? `worktree:${worktreeId}` : 'cwd:/outside/repo',
projectLabel,
repoId,
worktreeId,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1350,
estimatedCostUsd: 0.05
}
],
modelBreakdown: [
{
modelKey: model ?? 'unknown',
modelLabel: model ?? 'Unknown model',
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1350,
estimatedCostUsd: 0.05
}
],
locationModelBreakdown: [
{
locationKey: worktreeId ? `worktree:${worktreeId}` : 'cwd:/outside/repo',
modelKey: model ?? 'unknown',
modelLabel: model ?? 'Unknown model',
repoId,
worktreeId,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1350,
estimatedCostUsd: 0.05
}
],
...overrides
}
}
function makeDaily(
overrides: Partial<OpenCodeUsageDailyAggregate> = {}
): OpenCodeUsageDailyAggregate {
const worktreeId = overrides.worktreeId ?? 'repo-1::/workspace/repo'
return {
day: '2026-04-09',
model: 'anthropic/claude-sonnet-4-5',
projectKey: worktreeId ? `worktree:${worktreeId}` : 'cwd:/outside/repo',
projectLabel: worktreeId ? 'Repo' : 'outside/repo',
repoId: worktreeId ? 'repo-1' : null,
worktreeId,
eventCount: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1350,
estimatedCostUsd: 0.05,
...overrides
}
}
describe('OpenCodeUsageStore', () => {
let tempUserData: string
beforeEach(() => {
tempUserData = mkdtempSync(join(tmpdir(), 'orca-opencode-usage-store-'))
getPathMock.mockReturnValue(tempUserData)
initOpenCodeUsagePath()
vi.mocked(scanOpenCodeUsageDatabases).mockReset()
vi.mocked(scanOpenCodeUsageDatabases).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 OpenCode scans to pretty-printed cache persistence', async () => {
const store = createStoreWithState({
scanState: {
enabled: true,
lastScanStartedAt: null,
lastScanCompletedAt: null,
lastScanError: null
}
})
await store.refresh(true)
const persistedJson = readFileSync(join(tempUserData, 'orca-opencode-usage.json'), 'utf-8')
expect(scanOpenCodeUsageDatabases).toHaveBeenCalledWith([], [])
expect(persistedJson).toContain('\n')
})
it('reports no data for Orca scope when only non-Orca OpenCode usage exists', async () => {
const store = createStoreWithState({
sessions: [
makeSession({
primaryProjectLabel: 'outside/repo',
primaryWorktreeId: null,
primaryRepoId: null
})
],
dailyAggregates: [
makeDaily({
projectKey: 'cwd:/outside/repo',
projectLabel: 'outside/repo',
repoId: null,
worktreeId: null
})
]
})
const summary = await store.getSummary('orca', '30d')
expect(summary.hasAnyOpenCodeData).toBe(false)
expect(summary.sessions).toBe(0)
expect(summary.events).toBe(0)
})
it('uses recorded OpenCode costs and token totals without model pricing inference', async () => {
const store = createStoreWithState({
sessions: [
makeSession({ sessionId: 'session-1' }),
makeSession({
sessionId: 'session-2',
primaryModel: 'openai/gpt-5.5',
totalTokens: 2000,
estimatedCostUsd: null,
modelBreakdown: [
{
modelKey: 'openai/gpt-5.5',
modelLabel: 'openai/gpt-5.5',
eventCount: 1,
inputTokens: 1500,
cachedInputTokens: 200,
outputTokens: 500,
reasoningOutputTokens: 0,
totalTokens: 2000,
estimatedCostUsd: null
}
]
})
],
dailyAggregates: [
makeDaily(),
makeDaily({
model: 'openai/gpt-5.5',
eventCount: 2,
inputTokens: 1500,
cachedInputTokens: 200,
outputTokens: 500,
reasoningOutputTokens: 0,
totalTokens: 2000,
estimatedCostUsd: null
})
]
})
const summary = await store.getSummary('orca', '30d')
const daily = await store.getDaily('orca', '30d')
const breakdown = await store.getBreakdown('orca', '30d', 'model')
expect(summary).toMatchObject({
sessions: 2,
events: 3,
inputTokens: 2500,
cachedInputTokens: 600,
outputTokens: 750,
reasoningOutputTokens: 100,
totalTokens: 3350,
estimatedCostUsd: 0.05,
topModel: 'openai/gpt-5.5',
topProject: 'Repo',
hasAnyOpenCodeData: true
})
expect(daily).toEqual([
{
day: '2026-04-09',
inputTokens: 2500,
cachedInputTokens: 600,
outputTokens: 750,
reasoningOutputTokens: 100,
totalTokens: 3350
}
])
expect(breakdown.find((row) => row.key === 'openai/gpt-5.5')).toMatchObject({
sessions: 1,
estimatedCostUsd: null
})
})
it('returns recent sessions with OpenCode event and token fields', async () => {
const store = createStoreWithState({
sessions: [makeSession()],
dailyAggregates: [makeDaily()]
})
const sessions = await store.getRecentSessions('orca', '30d', 5)
expect(sessions).toEqual([
{
sessionId: 'session-1',
lastActiveAt: '2026-04-09T10:10:00.000Z',
durationMinutes: 10,
projectLabel: 'Repo',
model: 'anthropic/claude-sonnet-4-5',
events: 1,
inputTokens: 1000,
cachedInputTokens: 400,
outputTokens: 250,
reasoningOutputTokens: 100,
totalTokens: 1350
}
])
})
it('normalizes persisted OpenCode state by schema version', () => {
expect(
normalizePersistedState({
...getDefaultState(),
schemaVersion: 0,
processedDatabases: [
{
path: '/tmp/opencode.db',
mtimeMs: 1,
size: 2,
sessions: [makeSession()],
dailyAggregates: [makeDaily()],
ownedSessionIds: ['session-1'],
hasDeferredClaims: false
}
],
sessions: [makeSession()],
dailyAggregates: [makeDaily()]
})
).toEqual(getDefaultState())
expect(
normalizePersistedState({
...getDefaultState(),
processedDatabases: [
{
path: '/tmp/opencode.db',
mtimeMs: 1,
size: 2,
sessions: [],
dailyAggregates: [],
ownedSessionIds: [],
hasDeferredClaims: false
}
]
}).processedDatabases
).toHaveLength(1)
})
})