mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 12:20:43 +00:00
Discovers and parses sessions from ~/.factory/sessions/, reading JSONL message logs and companion settings.json files for token usage tracking. - Discovers sessions by scanning per-cwd subdirectories - Skips internal .factory housekeeping sessions - Extracts tools, bash commands, and user messages from JSONL - Distributes session-level cumulative token counts across calls - Normalizes Droid model wrappers before existing pricing lookup - Derives clean project names from cwd paths - Adds menubar provider filtering for Droid
148 lines
6.6 KiB
TypeScript
148 lines
6.6 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
import { createDroidProvider } from '../../src/providers/droid.js'
|
|
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
|
|
|
let factoryDir: string
|
|
|
|
async function writeSession(opts: {
|
|
projectDir?: string
|
|
sessionId?: string
|
|
lines?: unknown[]
|
|
settings?: unknown
|
|
subdir?: string
|
|
}): Promise<string> {
|
|
const sessionId = opts.sessionId ?? 'session-1'
|
|
const projectDir = opts.projectDir ?? '/tmp/my-project'
|
|
const subdir = opts.subdir ?? '-tmp-my-project'
|
|
const dir = join(factoryDir, 'sessions', subdir)
|
|
await mkdir(dir, { recursive: true })
|
|
const jsonlPath = join(dir, `${sessionId}.jsonl`)
|
|
const lines = opts.lines ?? [
|
|
{ type: 'session_start', id: sessionId, cwd: projectDir, title: 'Test session' },
|
|
{ type: 'message', id: 'u1', timestamp: '2026-04-20T10:00:00.000Z', message: { role: 'user', content: [{ type: 'text', text: 'build this' }] } },
|
|
{ type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:01.000Z', message: { role: 'assistant', content: [{ type: 'text', text: 'done' }] } },
|
|
]
|
|
await writeFile(jsonlPath, lines.map(line => JSON.stringify(line)).join('\n'))
|
|
|
|
if (opts.settings !== undefined) {
|
|
await writeFile(join(dir, `${sessionId}.settings.json`), JSON.stringify(opts.settings))
|
|
}
|
|
|
|
return jsonlPath
|
|
}
|
|
|
|
async function parseAll(filePath: string, seen = new Set<string>()): Promise<ParsedProviderCall[]> {
|
|
const provider = createDroidProvider(factoryDir)
|
|
const parser = provider.createSessionParser({ path: filePath, project: 'proj', provider: 'droid' }, seen)
|
|
const calls: ParsedProviderCall[] = []
|
|
for await (const call of parser.parse()) calls.push(call)
|
|
return calls
|
|
}
|
|
|
|
describe('droid provider', () => {
|
|
beforeEach(async () => {
|
|
factoryDir = await mkdtemp(join(tmpdir(), 'codeburn-droid-test-'))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(factoryDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('discovers Droid JSONL sessions', async () => {
|
|
await writeSession({ settings: { model: 'gpt-5', tokenUsage: { inputTokens: 10, outputTokens: 5, cacheCreationTokens: 0, cacheReadTokens: 0, thinkingTokens: 0 } } })
|
|
|
|
const provider = createDroidProvider(factoryDir)
|
|
const sessions = await provider.discoverSessions()
|
|
|
|
expect(sessions).toHaveLength(1)
|
|
expect(sessions[0]!.provider).toBe('droid')
|
|
expect(sessions[0]!.path.endsWith('session-1.jsonl')).toBe(true)
|
|
})
|
|
|
|
it('parses calls and distributes session-level token usage', async () => {
|
|
const path = await writeSession({
|
|
lines: [
|
|
{ type: 'session_start', id: 'session-1', cwd: '/tmp/my-project' },
|
|
{ type: 'message', id: 'u1', timestamp: '2026-04-20T10:00:00.000Z', message: { role: 'user', content: [{ type: 'text', text: '<system-reminder>x</system-reminder>' }, { type: 'text', text: 'build this' }] } },
|
|
{ type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:01.000Z', message: { role: 'assistant', content: [{ type: 'text', text: 'first' }] } },
|
|
{ type: 'message', id: 'a2', timestamp: '2026-04-20T10:00:02.000Z', message: { role: 'assistant', content: [{ type: 'text', text: 'second' }] } },
|
|
],
|
|
settings: { model: 'custom:gpt-5-[Proxy]-0', tokenUsage: { inputTokens: 101, outputTokens: 51, cacheCreationTokens: 7, cacheReadTokens: 11, thinkingTokens: 5 } },
|
|
})
|
|
|
|
const calls = await parseAll(path)
|
|
|
|
expect(calls).toHaveLength(2)
|
|
expect(calls[0]!.provider).toBe('droid')
|
|
expect(calls[0]!.model).toBe('gpt-5')
|
|
expect(calls[0]!.inputTokens).toBe(50)
|
|
expect(calls[1]!.inputTokens).toBe(51)
|
|
expect(calls[0]!.outputTokens).toBe(25)
|
|
expect(calls[1]!.outputTokens).toBe(26)
|
|
expect(calls[0]!.cacheReadInputTokens).toBe(5)
|
|
expect(calls[1]!.cacheReadInputTokens).toBe(6)
|
|
expect(calls[0]!.userMessage).toBe('build this')
|
|
expect(calls[0]!.sessionId).toBe('session-1')
|
|
})
|
|
|
|
it('extracts tools and meaningful bash command names', async () => {
|
|
const path = await writeSession({
|
|
lines: [
|
|
{ type: 'session_start', id: 'session-1', cwd: '/tmp/my-project' },
|
|
{ type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:01.000Z', message: { role: 'assistant', content: [
|
|
{ type: 'tool_use', name: 'Execute', input: { command: "python3 - <<'PY'\nimport os\n}\nPY" } },
|
|
{ type: 'tool_use', name: 'Read', input: { file_path: '/tmp/a' } },
|
|
{ type: 'tool_use', name: 'Task', input: { prompt: 'do work' } },
|
|
] } },
|
|
],
|
|
settings: { model: 'gpt-5', tokenUsage: { inputTokens: 10, outputTokens: 5, cacheCreationTokens: 0, cacheReadTokens: 0, thinkingTokens: 0 } },
|
|
})
|
|
|
|
const calls = await parseAll(path)
|
|
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.tools).toEqual(['Bash', 'Read', 'Agent'])
|
|
expect(calls[0]!.bashCommands).toContain('python3')
|
|
expect(calls[0]!.bashCommands).not.toContain('import')
|
|
expect(calls[0]!.bashCommands).not.toContain('}')
|
|
})
|
|
|
|
it('deduplicates calls by session and message id', async () => {
|
|
const path = await writeSession({ settings: { model: 'gpt-5', tokenUsage: { inputTokens: 10, outputTokens: 5, cacheCreationTokens: 0, cacheReadTokens: 0, thinkingTokens: 0 } } })
|
|
const seen = new Set<string>()
|
|
|
|
expect(await parseAll(path, seen)).toHaveLength(1)
|
|
expect(await parseAll(path, seen)).toHaveLength(0)
|
|
})
|
|
|
|
it('strips Droid model wrappers for display', () => {
|
|
const provider = createDroidProvider(factoryDir)
|
|
expect(provider.modelDisplayName('custom:GLM-5.1-[Proxy]-0')).toBe('GLM-5.1')
|
|
expect(provider.modelDisplayName('custom:claude-sonnet-4-6-1')).toBe('Sonnet 4.6')
|
|
})
|
|
|
|
it('returns no calls when settings are missing', async () => {
|
|
const path = await writeSession({})
|
|
expect(await parseAll(path)).toHaveLength(0)
|
|
})
|
|
|
|
it('skips internal .factory sessions during discovery', async () => {
|
|
await writeSession({ projectDir: factoryDir, subdir: '-internal', settings: { model: 'gpt-5', tokenUsage: { inputTokens: 10, outputTokens: 5, cacheCreationTokens: 0, cacheReadTokens: 0, thinkingTokens: 0 } } })
|
|
|
|
const provider = createDroidProvider(factoryDir)
|
|
expect(await provider.discoverSessions()).toHaveLength(0)
|
|
})
|
|
|
|
it('returns no calls for empty sessions', async () => {
|
|
const path = await writeSession({
|
|
lines: [{ type: 'session_start', id: 'empty', cwd: '/tmp/my-project' }],
|
|
settings: { model: 'gpt-5', tokenUsage: { inputTokens: 10, outputTokens: 5, cacheCreationTokens: 0, cacheReadTokens: 0, thinkingTokens: 0 } },
|
|
})
|
|
|
|
expect(await parseAll(path)).toHaveLength(0)
|
|
})
|
|
})
|