codeburn/tests/providers/open-design.test.ts
iamtoruk 62cb4f8630 test: isolate the real home on Windows and make the CLI suite pass there
os.homedir() reads USERPROFILE on Windows and never HOME, so the
isolation setup and every test that moved home were still resolving the
developer's real home directory. Fixtures came back empty, real sessions
leaked into counts, and the code under test wrote plans, receipts and a
sync ledger into the real ~/.config/codeburn during a run.

USERPROFILE now joins the redirected variables and a shared setHome()
helper moves HOME and USERPROFILE together; every spawn env carries both.
The remaining Windows-only failures were POSIX-shaped fixtures: chmod
tricks that only toggle a read-only attribute on NTFS, pid 1 as an
always-alive lock holder, splitting paths on '/', bare tar, mode-bit
expectations, and per-file timeouts too short for the cost of a tsx spawn.
Each gets a platform-aware equivalent so nothing is skipped on Windows and
the POSIX branches stay byte-for-byte what they were.

Full run on Windows: 267 files passed, 2 skipped, 3592 tests passed.
2026-09-02 13:43:57 -07:00

155 lines
5.9 KiB
TypeScript

import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { calculateCost } from '../../src/models.js'
import { clearSessionCache, filterProjectsByDateRange, parseAllSessions } from '../../src/parser.js'
import { allProviderNames } from '../../src/providers/index.js'
import { createOpenDesignProvider } from '../../src/providers/open-design.js'
import type { ParsedProviderCall, SessionSource } from '../../src/providers/types.js'
const fixtureRoot = join(import.meta.dirname, '../fixtures/open-design')
const dataDir = join(fixtureRoot, 'namespaces', 'release-stable', 'data')
let previousOverride: string | undefined
let previousCacheDir: string | undefined
let cacheDir: string | undefined
async function collect(source: SessionSource, seenKeys = new Set<string>()): Promise<ParsedProviderCall[]> {
const provider = createOpenDesignProvider()
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, seenKeys).parse()) calls.push(call)
return calls
}
async function fixtureSource(runId: string): Promise<SessionSource> {
const provider = createOpenDesignProvider()
const sources = await provider.discoverSessions()
const source = sources.find(s => s.path.includes(join(runId, 'events.jsonl')))
expect(source).toBeDefined()
return source!
}
describe('open-design provider', () => {
beforeEach(async () => {
previousOverride = process.env['CODEBURN_OPEN_DESIGN_DIR']
previousCacheDir = process.env['CODEBURN_CACHE_DIR']
cacheDir = await mkdtemp(join(tmpdir(), 'codeburn-open-design-cache-'))
process.env['CODEBURN_OPEN_DESIGN_DIR'] = dataDir
process.env['CODEBURN_CACHE_DIR'] = cacheDir
clearSessionCache()
})
afterEach(async () => {
clearSessionCache()
if (previousOverride === undefined) {
delete process.env['CODEBURN_OPEN_DESIGN_DIR']
} else {
process.env['CODEBURN_OPEN_DESIGN_DIR'] = previousOverride
}
if (previousCacheDir === undefined) {
delete process.env['CODEBURN_CACHE_DIR']
} else {
process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
}
if (cacheDir) await rm(cacheDir, { recursive: true, force: true })
cacheDir = undefined
})
it('discovers per-run events.jsonl files from the env override data dir', async () => {
const provider = createOpenDesignProvider()
const sources = await provider.discoverSessions()
expect(sources.map(s => s.provider).every(p => p === 'open-design')).toBe(true)
expect(sources.map(s => s.project)).toEqual(['release-stable', 'release-stable', 'release-stable'])
expect(sources.map(s => s.path).sort()).toEqual([
join(dataDir, 'runs', 'run-mixed', 'events.jsonl'),
join(dataDir, 'runs', 'run-no-usage', 'events.jsonl'),
join(dataDir, 'runs', 'run-start-seeded', 'events.jsonl'),
].sort())
})
it('parses a mixed-model run into separate per-model usage calls', async () => {
const calls = await collect(await fixtureSource('run-mixed'))
expect(calls).toHaveLength(2)
expect(calls.map(c => c.model)).toEqual(['openai-codex:gpt-5.5', 'glm-5.2'])
const codex = calls[0]!
expect(codex.provider).toBe('open-design')
expect(codex.sessionId).toBe('run-mixed')
expect(codex.inputTokens).toBe(950)
expect(codex.outputTokens).toBe(200)
expect(codex.cacheCreationInputTokens).toBe(0)
expect(codex.cacheReadInputTokens).toBe(50)
expect(codex.cachedInputTokens).toBe(50)
expect(codex.reasoningTokens).toBe(25)
expect(codex.timestamp).toBe('2026-06-22T10:00:05.000Z')
expect(new Date(codex.timestamp).toISOString()).toBe(codex.timestamp)
expect(codex.costUSD).toBeCloseTo(
calculateCost(codex.model, 950, 225, 0, 50, 0),
12,
)
const glm = calls[1]!
expect(glm.inputTokens).toBe(2900)
expect(glm.outputTokens).toBe(400)
expect(glm.cacheCreationInputTokens).toBe(0)
expect(glm.cacheReadInputTokens).toBe(100)
expect(glm.cachedInputTokens).toBe(100)
expect(glm.reasoningTokens).toBe(60)
expect(glm.timestamp).toBe('2026-06-22T10:00:15.000Z')
expect(glm.costUSD).toBeGreaterThan(0)
})
it('does not emit calls for a run with no usage events', async () => {
const calls = await collect(await fixtureSource('run-no-usage'))
expect(calls).toHaveLength(0)
})
it('uses the start-seeded model before any status transition', async () => {
const calls = await collect(await fixtureSource('run-start-seeded'))
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('glm-5.2')
expect(calls[0]!.inputTokens).toBe(770)
expect(calls[0]!.outputTokens).toBe(33)
expect(calls[0]!.cacheReadInputTokens).toBe(7)
expect(calls[0]!.reasoningTokens).toBe(3)
expect(calls[0]!.costUSD).toBeGreaterThan(0)
})
it('keeps numeric epoch timestamp usage in date-scoped aggregation', async () => {
const projects = await parseAllSessions(undefined, 'open-design')
const filtered = filterProjectsByDateRange(projects, {
start: new Date('2026-06-22T00:00:00.000Z'),
end: new Date('2026-06-22T23:59:59.999Z'),
})
const calls = filtered.flatMap(project =>
project.sessions.flatMap(session =>
session.turns.flatMap(turn => turn.assistantCalls),
),
)
const numericTimestampCall = calls.find(call =>
call.deduplicationKey === 'open-design:run-mixed:evt-codex-usage',
)
expect(numericTimestampCall?.timestamp).toBe('2026-06-22T10:00:05.000Z')
})
it('deduplicates usage events per run and event id across parser runs', async () => {
const source = await fixtureSource('run-mixed')
const seenKeys = new Set<string>()
const first = await collect(source, seenKeys)
const second = await collect(source, seenKeys)
expect(first).toHaveLength(2)
expect(second).toHaveLength(0)
})
it('registers open-design as a core provider', () => {
expect(allProviderNames()).toContain('open-design')
})
})