mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-19 07:43:09 +00:00
Add OpenClaw, Roo Code, and KiloCode providers (#175)
- OpenClaw: JSONL parser with multi-path discovery, tool extraction (toolCall + tool_use block types), model tracking via model_change and custom model-snapshot events - Roo Code + KiloCode: shared Cline-family parser extracts model from <model> tags in api_conversation_history.json, strips provider prefixes from model names - Add cline-auto and openclaw-auto aliases and display names - Add menubar provider filters and tab colors for all three - Show cached data instantly instead of blocking on CLI refresh
This commit is contained in:
parent
ce78ac52c1
commit
ec2de6a642
13 changed files with 1034 additions and 7 deletions
|
|
@ -3,7 +3,7 @@ import { providers, getAllProviders } from '../src/providers/index.js'
|
|||
|
||||
describe('provider registry', () => {
|
||||
it('has core providers registered synchronously', () => {
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'codex', 'copilot', 'gemini', 'kiro', 'pi', 'omp'])
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'codex', 'copilot', 'gemini', 'kilo-code', 'kiro', 'openclaw', 'pi', 'omp', 'roo-code'])
|
||||
})
|
||||
|
||||
it('includes sqlite providers after async load', async () => {
|
||||
|
|
|
|||
62
tests/providers/kilo-code.test.ts
Normal file
62
tests/providers/kilo-code.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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 { kiloCode, createKiloCodeProvider } from '../../src/providers/kilo-code.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
describe('kilo-code provider - discovery path differentiation', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'kilo-code-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('discovers tasks using kilo-code extension path', async () => {
|
||||
const task = join(tmpDir, 'tasks', 'task-kilo-1')
|
||||
await mkdir(task, { recursive: true })
|
||||
await writeFile(join(task, 'ui_messages.json'), JSON.stringify([
|
||||
{ type: 'say', say: 'api_req_started', text: JSON.stringify({ tokensIn: 100, tokensOut: 50 }), ts: 1700000000000 },
|
||||
]))
|
||||
|
||||
const provider = createKiloCodeProvider(tmpDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(1)
|
||||
expect(sessions[0]!.provider).toBe('kilo-code')
|
||||
})
|
||||
|
||||
it('parses with kilo-code provider name in dedup key', async () => {
|
||||
const task = join(tmpDir, 'tasks', 'task-kilo-2')
|
||||
await mkdir(task, { recursive: true })
|
||||
await writeFile(join(task, 'ui_messages.json'), JSON.stringify([
|
||||
{ type: 'say', say: 'api_req_started', text: JSON.stringify({ tokensIn: 200, tokensOut: 100 }), ts: 1700000000000 },
|
||||
]))
|
||||
|
||||
const source = { path: task, project: 'task-kilo-2', provider: 'kilo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of kiloCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.provider).toBe('kilo-code')
|
||||
expect(calls[0]!.deduplicationKey).toMatch(/^kilo-code:/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('kilo-code provider - metadata', () => {
|
||||
it('has correct name and displayName', () => {
|
||||
expect(kiloCode.name).toBe('kilo-code')
|
||||
expect(kiloCode.displayName).toBe('KiloCode')
|
||||
})
|
||||
|
||||
it('uses different extension ID than roo-code', async () => {
|
||||
const kiloProvider = createKiloCodeProvider('/tmp/kilo-test')
|
||||
const sessions = await kiloProvider.discoverSessions()
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
192
tests/providers/openclaw.test.ts
Normal file
192
tests/providers/openclaw.test.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import { describe, it, expect, afterAll } from 'vitest'
|
||||
import { createOpenClawProvider } from '../../src/providers/openclaw.js'
|
||||
import { writeFile, mkdir, rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
const SESSION_LINES = [
|
||||
JSON.stringify({ type: 'session', version: 3, id: 'test-sess-1', timestamp: '2026-04-20T10:00:00.000Z', cwd: '/tmp' }),
|
||||
JSON.stringify({ type: 'model_change', id: 'mc1', timestamp: '2026-04-20T10:00:01.000Z', provider: 'anthropic', modelId: 'claude-sonnet-4-6' }),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'u1', timestamp: '2026-04-20T10:00:02.000Z',
|
||||
message: { role: 'user', content: [{ type: 'text', text: 'hello world' }] },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:03.000Z',
|
||||
message: {
|
||||
role: 'assistant', model: 'claude-sonnet-4-6',
|
||||
content: [{ type: 'text', text: 'Hi!' }],
|
||||
usage: { input: 500, output: 100, cacheRead: 200, cacheWrite: 50, totalTokens: 850 },
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'a2', timestamp: '2026-04-20T10:00:05.000Z',
|
||||
message: {
|
||||
role: 'assistant', model: 'claude-sonnet-4-6',
|
||||
content: [
|
||||
{ type: 'text', text: 'Running command' },
|
||||
{ type: 'toolCall', name: 'exec', arguments: { command: 'ls -la' } },
|
||||
{ type: 'toolCall', name: 'read', arguments: { path: '/tmp/x' } },
|
||||
{ type: 'tool_use', name: 'write', arguments: { path: '/tmp/y' } },
|
||||
],
|
||||
usage: { input: 600, output: 200, cacheRead: 100, cacheWrite: 0, totalTokens: 900, cost: { total: 0.05 } },
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
async function setupFixture(dir: string, agentName: string, sessionId: string, lines: string[]): Promise<string> {
|
||||
const sessionsDir = join(dir, agentName, 'sessions')
|
||||
await mkdir(sessionsDir, { recursive: true })
|
||||
const filePath = join(sessionsDir, `${sessionId}.jsonl`)
|
||||
await writeFile(filePath, lines.join('\n'))
|
||||
return filePath
|
||||
}
|
||||
|
||||
describe('openclaw provider', () => {
|
||||
const baseDir = join(tmpdir(), `codeburn-openclaw-test-${Date.now()}`)
|
||||
|
||||
it('discovers sessions in agent directories', async () => {
|
||||
const dir = join(baseDir, 'discover')
|
||||
await setupFixture(dir, 'myproject', 'sess-1', SESSION_LINES)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
expect(sources.length).toBe(1)
|
||||
expect(sources[0].provider).toBe('openclaw')
|
||||
expect(sources[0].project).toBe('myproject')
|
||||
})
|
||||
|
||||
it('parses assistant messages with usage', async () => {
|
||||
const dir = join(baseDir, 'parse')
|
||||
await setupFixture(dir, 'proj', 'test-sess-1', SESSION_LINES)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const call of parser.parse()) {
|
||||
calls.push(call)
|
||||
}
|
||||
expect(calls.length).toBe(2)
|
||||
expect(calls[0].provider).toBe('openclaw')
|
||||
expect(calls[0].model).toBe('claude-sonnet-4-6')
|
||||
expect(calls[0].inputTokens).toBe(500)
|
||||
expect(calls[0].outputTokens).toBe(100)
|
||||
expect(calls[0].cacheReadInputTokens).toBe(200)
|
||||
expect(calls[0].userMessage).toBe('hello world')
|
||||
expect(calls[0].sessionId).toBe('test-sess-1')
|
||||
})
|
||||
|
||||
it('uses cost.total from provider when available', async () => {
|
||||
const dir = join(baseDir, 'cost')
|
||||
await setupFixture(dir, 'proj', 'test-sess-1', SESSION_LINES)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const call of parser.parse()) calls.push(call)
|
||||
expect(calls[1].costUSD).toBe(0.05)
|
||||
})
|
||||
|
||||
it('extracts tools and bash commands', async () => {
|
||||
const dir = join(baseDir, 'tools')
|
||||
await setupFixture(dir, 'proj', 'test-sess-1', SESSION_LINES)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const call of parser.parse()) calls.push(call)
|
||||
expect(calls[1].tools).toContain('Bash')
|
||||
expect(calls[1].tools).toContain('Read')
|
||||
expect(calls[1].tools).toContain('Write')
|
||||
expect(calls[1].bashCommands).toContain('ls')
|
||||
})
|
||||
|
||||
it('deduplicates on re-parse', async () => {
|
||||
const dir = join(baseDir, 'dedup')
|
||||
await setupFixture(dir, 'proj', 'test-sess-1', SESSION_LINES)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const seen = new Set<string>()
|
||||
const parser1 = provider.createSessionParser(sources[0], seen)
|
||||
const calls1: any[] = []
|
||||
for await (const c of parser1.parse()) calls1.push(c)
|
||||
expect(calls1.length).toBe(2)
|
||||
const parser2 = provider.createSessionParser(sources[0], seen)
|
||||
const calls2: any[] = []
|
||||
for await (const c of parser2.parse()) calls2.push(c)
|
||||
expect(calls2.length).toBe(0)
|
||||
})
|
||||
|
||||
it('reads model from model_change event', async () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'session', id: 'mc-test', timestamp: '2026-04-20T10:00:00.000Z' }),
|
||||
JSON.stringify({ type: 'model_change', id: 'mc1', modelId: 'gpt-5.5', provider: 'openai' }),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:01.000Z',
|
||||
message: { role: 'assistant', usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0 } },
|
||||
}),
|
||||
]
|
||||
const dir = join(baseDir, 'model-change')
|
||||
await setupFixture(dir, 'proj', 'mc-test', lines)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const c of parser.parse()) calls.push(c)
|
||||
expect(calls[0].model).toBe('gpt-5.5')
|
||||
})
|
||||
|
||||
it('reads model from custom model-snapshot event', async () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'session', id: 'snap-test', timestamp: '2026-04-20T10:00:00.000Z' }),
|
||||
JSON.stringify({ type: 'custom', customType: 'model-snapshot', data: { modelId: 'glm-5.1:cloud', provider: 'ollama' }, id: 's1' }),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'a1', timestamp: '2026-04-20T10:00:01.000Z',
|
||||
message: { role: 'assistant', usage: { input: 200, output: 80, cacheRead: 0, cacheWrite: 0 } },
|
||||
}),
|
||||
]
|
||||
const dir = join(baseDir, 'snapshot')
|
||||
await setupFixture(dir, 'proj', 'snap-test', lines)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const c of parser.parse()) calls.push(c)
|
||||
expect(calls[0].model).toBe('glm-5.1:cloud')
|
||||
})
|
||||
|
||||
it('skips entries with invalid timestamps', async () => {
|
||||
const lines = [
|
||||
JSON.stringify({ type: 'session', id: 'bad-ts', timestamp: 'not-a-date' }),
|
||||
JSON.stringify({
|
||||
type: 'message', id: 'a1', timestamp: 'also-bad',
|
||||
message: { role: 'assistant', model: 'test', usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0 } },
|
||||
}),
|
||||
]
|
||||
const dir = join(baseDir, 'bad-ts')
|
||||
await setupFixture(dir, 'proj', 'bad-ts', lines)
|
||||
const provider = createOpenClawProvider(dir)
|
||||
const sources = await provider.discoverSessions()
|
||||
const parser = provider.createSessionParser(sources[0], new Set())
|
||||
const calls: any[] = []
|
||||
for await (const c of parser.parse()) calls.push(c)
|
||||
expect(calls.length).toBe(0)
|
||||
})
|
||||
|
||||
it('tool and model display names work', () => {
|
||||
const provider = createOpenClawProvider()
|
||||
expect(provider.toolDisplayName('bash')).toBe('Bash')
|
||||
expect(provider.toolDisplayName('dispatch_agent')).toBe('Agent')
|
||||
expect(provider.toolDisplayName('unknown')).toBe('unknown')
|
||||
expect(provider.modelDisplayName('claude-sonnet-4-6')).toBe('claude-sonnet-4-6')
|
||||
})
|
||||
|
||||
it('returns empty for nonexistent directory', async () => {
|
||||
const provider = createOpenClawProvider('/tmp/nonexistent-openclaw-test')
|
||||
const sources = await provider.discoverSessions()
|
||||
expect(sources.length).toBe(0)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(baseDir, { recursive: true, force: true })
|
||||
})
|
||||
})
|
||||
247
tests/providers/roo-code.test.ts
Normal file
247
tests/providers/roo-code.test.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
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 { rooCode, createRooCodeProvider } from '../../src/providers/roo-code.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
function makeUiMessages(opts: {
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
cacheReads?: number
|
||||
cacheWrites?: number
|
||||
cost?: number
|
||||
userMessage?: string
|
||||
ts?: number
|
||||
}): string {
|
||||
const messages: unknown[] = []
|
||||
|
||||
if (opts.userMessage) {
|
||||
messages.push({ type: 'say', say: 'user_feedback', text: opts.userMessage, ts: 1700000000000 })
|
||||
}
|
||||
|
||||
const apiData: Record<string, unknown> = {
|
||||
tokensIn: opts.tokensIn ?? 100,
|
||||
tokensOut: opts.tokensOut ?? 50,
|
||||
cacheReads: opts.cacheReads ?? 0,
|
||||
cacheWrites: opts.cacheWrites ?? 0,
|
||||
}
|
||||
if (opts.cost !== undefined) apiData.cost = opts.cost
|
||||
|
||||
messages.push({
|
||||
type: 'say',
|
||||
say: 'api_req_started',
|
||||
text: JSON.stringify(apiData),
|
||||
ts: opts.ts ?? 1700000001000,
|
||||
})
|
||||
|
||||
return JSON.stringify(messages)
|
||||
}
|
||||
|
||||
function makeApiHistory(opts?: { model?: string }): string {
|
||||
const modelTag = opts?.model ? `<model>${opts.model}</model>` : ''
|
||||
const messages = [
|
||||
{ role: 'user', content: [{ type: 'text', text: `hello\n<environment_details>\n${modelTag}\n</environment_details>` }] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'response' }] },
|
||||
]
|
||||
return JSON.stringify(messages)
|
||||
}
|
||||
|
||||
describe('roo-code provider - parsing', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'roo-code-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('parses tokens and cost from ui_messages.json', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-001')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({
|
||||
tokensIn: 200,
|
||||
tokensOut: 100,
|
||||
cacheReads: 50,
|
||||
cacheWrites: 30,
|
||||
cost: 0.05,
|
||||
userMessage: 'fix the bug',
|
||||
}))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory())
|
||||
|
||||
const source = { path: taskDir, project: 'task-001', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
const call = calls[0]!
|
||||
expect(call.provider).toBe('roo-code')
|
||||
expect(call.inputTokens).toBe(200)
|
||||
expect(call.outputTokens).toBe(100)
|
||||
expect(call.cacheReadInputTokens).toBe(50)
|
||||
expect(call.cacheCreationInputTokens).toBe(30)
|
||||
expect(call.costUSD).toBe(0.05)
|
||||
expect(call.userMessage).toBe('fix the bug')
|
||||
expect(call.sessionId).toBe('task-001')
|
||||
})
|
||||
|
||||
it('extracts model from api_conversation_history.json', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-002')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory({ model: 'claude-sonnet-4-5' }))
|
||||
|
||||
const source = { path: taskDir, project: 'task-002', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.model).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('falls back to cline-auto when no model indicators', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-003')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), JSON.stringify([
|
||||
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'hi' }] },
|
||||
]))
|
||||
|
||||
const source = { path: taskDir, project: 'task-003', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.model).toBe('cline-auto')
|
||||
})
|
||||
|
||||
it('deduplicates across parser runs', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-004')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
|
||||
const source = { path: taskDir, project: 'task-004', provider: 'roo-code' }
|
||||
const seenKeys = new Set<string>()
|
||||
|
||||
const calls1: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, seenKeys).parse()) calls1.push(call)
|
||||
|
||||
const calls2: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, seenKeys).parse()) calls2.push(call)
|
||||
|
||||
expect(calls1).toHaveLength(1)
|
||||
expect(calls2).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles missing ui_messages.json gracefully', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-005')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
|
||||
const source = { path: taskDir, project: 'task-005', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles invalid JSON gracefully', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-006')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), 'not valid json')
|
||||
|
||||
const source = { path: taskDir, project: 'task-006', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('skips entries with zero tokens', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-007')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), JSON.stringify([
|
||||
{ type: 'say', say: 'api_req_started', text: JSON.stringify({ tokensIn: 0, tokensOut: 0 }), ts: 1700000000000 },
|
||||
]))
|
||||
|
||||
const source = { path: taskDir, project: 'task-007', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('calculates cost from model when cost field missing', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-008')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 1000, tokensOut: 500 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory())
|
||||
|
||||
const source = { path: taskDir, project: 'task-008', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.costUSD).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('roo-code provider - discovery', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'roo-code-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('discovers task directories with ui_messages.json', async () => {
|
||||
const task1 = join(tmpDir, 'tasks', 'task-a')
|
||||
const task2 = join(tmpDir, 'tasks', 'task-b')
|
||||
await mkdir(task1, { recursive: true })
|
||||
await mkdir(task2, { recursive: true })
|
||||
await writeFile(join(task1, 'ui_messages.json'), '[]')
|
||||
await writeFile(join(task2, 'ui_messages.json'), '[]')
|
||||
|
||||
const provider = createRooCodeProvider(tmpDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(2)
|
||||
expect(sessions.every(s => s.provider === 'roo-code')).toBe(true)
|
||||
})
|
||||
|
||||
it('skips tasks without ui_messages.json', async () => {
|
||||
const task = join(tmpDir, 'tasks', 'task-no-ui')
|
||||
await mkdir(task, { recursive: true })
|
||||
await writeFile(join(task, 'api_conversation_history.json'), '[]')
|
||||
|
||||
const provider = createRooCodeProvider(tmpDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty for nonexistent directory', async () => {
|
||||
const provider = createRooCodeProvider('/nonexistent/path')
|
||||
const sessions = await provider.discoverSessions()
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('roo-code provider - metadata', () => {
|
||||
it('has correct name and displayName', () => {
|
||||
expect(rooCode.name).toBe('roo-code')
|
||||
expect(rooCode.displayName).toBe('Roo Code')
|
||||
})
|
||||
|
||||
it('passes through model display names', () => {
|
||||
expect(rooCode.modelDisplayName('claude-sonnet-4-5')).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('passes through tool display names', () => {
|
||||
expect(rooCode.toolDisplayName('read_file')).toBe('read_file')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue