mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
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.
356 lines
9.6 KiB
TypeScript
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)
|
|
})
|
|
})
|