Files
2026-07-13 12:26:02 +08:00

156 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(`${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')
})
})