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 { 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()): Promise { 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: 'x' }, { 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() 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) }) })