mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 06:59:37 +00:00
npm was warning on every install that prebuild-install@7.1.3 is no longer maintained. prebuild-install ships as a transitive dependency of better-sqlite3 and upstream PR #1446 to replace it is still open, so we switch to Node's built-in node:sqlite module (stable in Node 24, experimental in Node 22/23) and remove the better-sqlite3 dep entirely. - src/sqlite.ts: uses DatabaseSync from node:sqlite. The one-shot ExperimentalWarning about SQLite on Node 22/23 is silenced for that specific warning; other warnings pass through unchanged. - package.json: engines.node bumped to >=22 (Node 20 EOL 2026-04-30), better-sqlite3 and @types/better-sqlite3 removed, @types/node added (it was coming in transitively via @types/better-sqlite3). - tests/providers/opencode.test.ts: fixture DB creation switched to node:sqlite (API parity for the CREATE TABLE + INSERT + prepare path we use). End-user install footprint shrinks from 167 to 40 packages and prints zero deprecation warnings. Credit: @primeminister for the report.
558 lines
21 KiB
TypeScript
558 lines
21 KiB
TypeScript
import { mkdtemp, rm, mkdir, writeFile } from 'fs/promises'
|
|
import { mkdirSync } from 'fs'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { isSqliteAvailable } from '../../src/sqlite.js'
|
|
import { createOpenCodeProvider } from '../../src/providers/opencode.js'
|
|
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
|
|
|
type TestDb = {
|
|
exec(sql: string): void
|
|
prepare(sql: string): { run(...params: unknown[]): void }
|
|
close(): void
|
|
}
|
|
|
|
let tmpDir: string
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await mkdtemp(join(tmpdir(), 'opencode-test-'))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
function createTestDb(dir: string): string {
|
|
const ocDir = join(dir, 'opencode')
|
|
mkdirSync(ocDir, { recursive: true })
|
|
const dbPath = join(ocDir, 'opencode.db')
|
|
|
|
const { DatabaseSync: Database } = require('node:sqlite')
|
|
const db = new Database(dbPath)
|
|
db.exec(`
|
|
CREATE TABLE session (
|
|
id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT,
|
|
slug TEXT NOT NULL, directory TEXT NOT NULL, title TEXT NOT NULL,
|
|
version TEXT NOT NULL, time_created INTEGER, time_updated INTEGER,
|
|
time_archived INTEGER
|
|
)
|
|
`)
|
|
db.exec(`
|
|
CREATE TABLE message (
|
|
id TEXT PRIMARY KEY, session_id TEXT NOT NULL,
|
|
time_created INTEGER, time_updated INTEGER, data TEXT NOT NULL
|
|
)
|
|
`)
|
|
db.exec(`
|
|
CREATE TABLE part (
|
|
id TEXT PRIMARY KEY, message_id TEXT NOT NULL,
|
|
session_id TEXT NOT NULL, time_created INTEGER,
|
|
time_updated INTEGER, data TEXT NOT NULL
|
|
)
|
|
`)
|
|
db.close()
|
|
return dbPath
|
|
}
|
|
|
|
function withTestDb(dbPath: string, fn: (db: TestDb) => void): void {
|
|
const { DatabaseSync: Database } = require('node:sqlite')
|
|
const db = new Database(dbPath)
|
|
fn(db)
|
|
db.close()
|
|
}
|
|
|
|
function insertSession(
|
|
db: TestDb,
|
|
id: string,
|
|
opts: { directory?: string; title?: string; parentId?: string | null; archived?: number | null } = {},
|
|
): void {
|
|
db.prepare(`
|
|
INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_archived, parent_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(id, 'proj-1', 'slug-1', opts.directory ?? '/home/user/myproject', opts.title ?? 'My Project', '1.0', 1700000000000, opts.archived ?? null, opts.parentId ?? null)
|
|
}
|
|
|
|
type MessageFixture = {
|
|
role: string
|
|
modelID?: string
|
|
cost?: number
|
|
tokens?: {
|
|
input: number
|
|
output: number
|
|
reasoning: number
|
|
cache: { read: number; write: number }
|
|
}
|
|
}
|
|
|
|
type PartFixture = {
|
|
type: string
|
|
text?: string
|
|
tool?: string
|
|
state?: { status: string; input: { command?: string } }
|
|
}
|
|
|
|
function insertMessage(db: TestDb, id: string, sessionId: string, timeCreated: number, data: MessageFixture): void {
|
|
db.prepare(`INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)`)
|
|
.run(id, sessionId, timeCreated, JSON.stringify(data))
|
|
}
|
|
|
|
function insertPart(db: TestDb, id: string, messageId: string, sessionId: string, data: PartFixture): void {
|
|
db.prepare(`INSERT INTO part (id, message_id, session_id, data) VALUES (?, ?, ?, ?)`)
|
|
.run(id, messageId, sessionId, JSON.stringify(data))
|
|
}
|
|
|
|
async function collectCalls(provider: ReturnType<typeof createOpenCodeProvider>, dbPath: string, sessionId: string, seenKeys?: Set<string>): Promise<ParsedProviderCall[]> {
|
|
const source = { path: `${dbPath}:${sessionId}`, project: 'myproject', provider: 'opencode' }
|
|
const calls: ParsedProviderCall[] = []
|
|
for await (const call of provider.createSessionParser(source, seenKeys ?? new Set()).parse()) {
|
|
calls.push(call)
|
|
}
|
|
return calls
|
|
}
|
|
|
|
const skipUnlessSqlite = isSqliteAvailable() ? describe : describe.skip
|
|
|
|
skipUnlessSqlite('opencode provider - model display names', () => {
|
|
it('strips provider prefix and delegates to shared lookup', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.modelDisplayName('claude-opus-4-6-20260205')).toBe('Opus 4.6')
|
|
})
|
|
|
|
it('strips google provider prefix', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.modelDisplayName('google/gemini-2.5-pro')).toBe('Gemini 2.5 Pro')
|
|
})
|
|
|
|
it('strips openai provider prefix', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.modelDisplayName('openai/gpt-4o')).toBe('GPT-4o')
|
|
})
|
|
|
|
it('passes through models without prefix unchanged', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.modelDisplayName('gpt-4o')).toBe('GPT-4o')
|
|
expect(provider.modelDisplayName('gpt-4o-mini')).toBe('GPT-4o Mini')
|
|
})
|
|
|
|
it('returns unknown models as-is', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.modelDisplayName('big-pickle')).toBe('big-pickle')
|
|
})
|
|
|
|
it('has correct displayName', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.displayName).toBe('OpenCode')
|
|
expect(provider.name).toBe('opencode')
|
|
})
|
|
})
|
|
|
|
skipUnlessSqlite('opencode provider - tool display names', () => {
|
|
it('maps opencode builtins', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.toolDisplayName('bash')).toBe('Bash')
|
|
expect(provider.toolDisplayName('edit')).toBe('Edit')
|
|
expect(provider.toolDisplayName('task')).toBe('Agent')
|
|
expect(provider.toolDisplayName('fetch')).toBe('WebFetch')
|
|
expect(provider.toolDisplayName('grep')).toBe('Grep')
|
|
expect(provider.toolDisplayName('write')).toBe('Write')
|
|
expect(provider.toolDisplayName('skill')).toBe('Skill')
|
|
})
|
|
|
|
it('returns unknown tools as-is', () => {
|
|
const provider = createOpenCodeProvider()
|
|
expect(provider.toolDisplayName('github_search_code')).toBe('github_search_code')
|
|
})
|
|
})
|
|
|
|
skipUnlessSqlite('opencode provider - session discovery', () => {
|
|
it('discovers sessions with correct path format', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
|
|
expect(sessions).toHaveLength(1)
|
|
expect(sessions[0]!.provider).toBe('opencode')
|
|
expect(sessions[0]!.project).toBe('home-user-myproject')
|
|
expect(sessions[0]!.path).toBe(`${dbPath}:sess-1`)
|
|
})
|
|
|
|
it('excludes archived sessions', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-archived', { archived: 1700000001000 })
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toHaveLength(0)
|
|
})
|
|
|
|
it('excludes child sessions', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-child', { parentId: 'parent-id' })
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toHaveLength(0)
|
|
})
|
|
|
|
it('returns empty for non-existent path', async () => {
|
|
const provider = createOpenCodeProvider('/nonexistent/path')
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toEqual([])
|
|
})
|
|
|
|
it('returns empty for empty database', async () => {
|
|
createTestDb(tmpDir)
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toEqual([])
|
|
})
|
|
|
|
it('discovers sessions across multiple channel databases', async () => {
|
|
const ocDir = join(tmpDir, 'opencode')
|
|
await mkdir(ocDir, { recursive: true })
|
|
|
|
const { DatabaseSync: Database } = require('node:sqlite')
|
|
for (const file of ['opencode.db', 'opencode-dev.db']) {
|
|
const dbPath = join(ocDir, file)
|
|
const db = new Database(dbPath)
|
|
db.exec(`
|
|
CREATE TABLE session (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, parent_id TEXT,
|
|
slug TEXT NOT NULL, directory TEXT NOT NULL, title TEXT NOT NULL,
|
|
version TEXT NOT NULL, time_created INTEGER, time_updated INTEGER, time_archived INTEGER)
|
|
`)
|
|
db.exec(`CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT NOT NULL,
|
|
time_created INTEGER, time_updated INTEGER, data TEXT NOT NULL)`)
|
|
db.exec(`CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT NOT NULL,
|
|
session_id TEXT NOT NULL, time_created INTEGER, time_updated INTEGER, data TEXT NOT NULL)`)
|
|
db.prepare(`INSERT INTO session (id, project_id, slug, directory, title, version, time_created)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(`sess-${file}`, 'proj-1', 'slug-1', '/home/user/myproject', 'My Project', '1.0', 1700000000000)
|
|
db.close()
|
|
}
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
|
|
expect(sessions).toHaveLength(2)
|
|
expect(sessions.map(s => s.path)).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('opencode.db:sess-opencode.db'),
|
|
expect.stringContaining('opencode-dev.db:sess-opencode-dev.db'),
|
|
]),
|
|
)
|
|
expect(sessions.every(s => s.provider === 'opencode')).toBe(true)
|
|
})
|
|
|
|
it('ignores non-opencode db files in the directory', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
})
|
|
await writeFile(join(tmpDir, 'opencode', 'other.db'), '')
|
|
await writeFile(join(tmpDir, 'opencode', 'opencode.txt'), '')
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toHaveLength(1)
|
|
})
|
|
|
|
it('sanitizes title when directory is empty', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1', { directory: '', title: 'My Session Title' })
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions[0]!.project).toBe('My Session Title')
|
|
})
|
|
|
|
it('discovers multiple sessions in one database', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1', { directory: '/home/user/project-a', title: 'A' })
|
|
insertSession(db, 'sess-2', { directory: '/home/user/project-b', title: 'B' })
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const sessions = await provider.discoverSessions()
|
|
expect(sessions).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
skipUnlessSqlite('opencode provider - session parsing', () => {
|
|
it('parses assistant messages with all fields', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000000000, { role: 'user' })
|
|
insertPart(db, 'part-1', 'msg-1', 'sess-1', { type: 'text', text: 'fix the login bug' })
|
|
|
|
insertMessage(db, 'msg-2', 'sess-1', 1700000001000, {
|
|
role: 'assistant',
|
|
modelID: 'claude-opus-4-6',
|
|
cost: 0.05,
|
|
tokens: { input: 100, output: 200, reasoning: 50, cache: { read: 500, write: 300 } },
|
|
})
|
|
insertPart(db, 'part-2', 'msg-2', 'sess-1', {
|
|
type: 'tool', tool: 'bash',
|
|
state: { status: 'completed', input: { command: 'npm test && git push' } },
|
|
})
|
|
insertPart(db, 'part-3', 'msg-2', 'sess-1', {
|
|
type: 'tool', tool: 'edit', state: { status: 'completed', input: {} },
|
|
})
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const calls = await collectCalls(provider, dbPath, 'sess-1')
|
|
|
|
expect(calls).toHaveLength(1)
|
|
const call = calls[0]!
|
|
expect(call.provider).toBe('opencode')
|
|
expect(call.model).toBe('claude-opus-4-6')
|
|
expect(call.inputTokens).toBe(100)
|
|
expect(call.outputTokens).toBe(200)
|
|
expect(call.reasoningTokens).toBe(50)
|
|
expect(call.cacheReadInputTokens).toBe(500)
|
|
expect(call.cacheCreationInputTokens).toBe(300)
|
|
expect(call.cachedInputTokens).toBe(500)
|
|
expect(call.webSearchRequests).toBe(0)
|
|
expect(call.speed).toBe('standard')
|
|
expect(call.costUSD).toBeGreaterThan(0)
|
|
expect(call.tools).toEqual(['Bash', 'Edit'])
|
|
expect(call.bashCommands).toEqual(['npm', 'git'])
|
|
expect(call.userMessage).toBe('fix the login bug')
|
|
expect(call.sessionId).toBe('sess-1')
|
|
expect(call.timestamp).toBe(new Date(1700000001000).toISOString())
|
|
expect(call.deduplicationKey).toBe('opencode:sess-1:msg-2')
|
|
})
|
|
|
|
it('skips zero-token messages with zero cost', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0,
|
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(0)
|
|
})
|
|
|
|
it('deduplicates messages across parses', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.05,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const seenKeys = new Set<string>()
|
|
const calls1 = await collectCalls(provider, dbPath, 'sess-1', seenKeys)
|
|
const calls2 = await collectCalls(provider, dbPath, 'sess-1', seenKeys)
|
|
|
|
expect(calls1).toHaveLength(1)
|
|
expect(calls2).toHaveLength(0)
|
|
expect(seenKeys.has('opencode:sess-1:msg-1')).toBe(true)
|
|
})
|
|
|
|
it('falls back to pre-calculated cost for unknown models', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'totally-unknown-model-xyz', cost: 0.42,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.costUSD).toBe(0.42)
|
|
})
|
|
|
|
it('uses calculated cost over pre-calculated for known models', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 999.99,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.costUSD).toBeGreaterThan(0)
|
|
expect(calls[0]!.costUSD).not.toBe(999.99)
|
|
})
|
|
|
|
it('handles missing tokens field gracefully', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.10,
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.inputTokens).toBe(0)
|
|
expect(calls[0]!.outputTokens).toBe(0)
|
|
expect(calls[0]!.costUSD).toBe(0.10)
|
|
})
|
|
|
|
it('uses "unknown" for missing modelID', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', cost: 0.05,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.model).toBe('unknown')
|
|
})
|
|
|
|
it('handles corrupt JSON in message and part data', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
|
|
db.prepare(`INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)`)
|
|
.run('msg-corrupt', 'sess-1', 1700000000500, 'not valid json {]')
|
|
|
|
insertMessage(db, 'msg-valid', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.05,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
|
|
db.prepare(`INSERT INTO part (id, message_id, session_id, data) VALUES (?, ?, ?, ?)`)
|
|
.run('part-corrupt', 'msg-valid', 'sess-1', 'corrupt {[}')
|
|
|
|
insertPart(db, 'part-valid', 'msg-valid', 'sess-1', {
|
|
type: 'tool', tool: 'bash', state: { status: 'completed', input: {} },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.model).toBe('claude-opus-4-6')
|
|
expect(calls[0]!.tools).toEqual(['Bash'])
|
|
})
|
|
|
|
it('converts seconds-epoch timestamps to milliseconds', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.05,
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(1)
|
|
expect(calls[0]!.timestamp).toBe(new Date(1700000001 * 1000).toISOString())
|
|
})
|
|
|
|
it('skips non-user non-assistant roles', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-1', 'sess-1', 1700000001000, {
|
|
role: 'system', modelID: 'claude-opus-4-6',
|
|
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(0)
|
|
})
|
|
|
|
it('returns empty for invalid db path', async () => {
|
|
const provider = createOpenCodeProvider(tmpDir)
|
|
const source = { path: '/nonexistent/db.db:sess-1', project: 'test', provider: 'opencode' }
|
|
const calls: ParsedProviderCall[] = []
|
|
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(call)
|
|
expect(calls).toHaveLength(0)
|
|
})
|
|
|
|
it('tracks user messages per assistant response', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
|
|
insertMessage(db, 'msg-u1', 'sess-1', 1700000000000, { role: 'user' })
|
|
insertPart(db, 'part-u1', 'msg-u1', 'sess-1', { type: 'text', text: 'first question' })
|
|
|
|
insertMessage(db, 'msg-a1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.01,
|
|
tokens: { input: 50, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
|
|
insertMessage(db, 'msg-u2', 'sess-1', 1700000002000, { role: 'user' })
|
|
insertPart(db, 'part-u2', 'msg-u2', 'sess-1', { type: 'text', text: 'second question' })
|
|
|
|
insertMessage(db, 'msg-a2', 'sess-1', 1700000003000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.02,
|
|
tokens: { input: 80, output: 80, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(2)
|
|
expect(calls[0]!.userMessage).toBe('first question')
|
|
expect(calls[1]!.userMessage).toBe('second question')
|
|
})
|
|
|
|
it('joins multiple text parts in user messages', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
|
|
insertMessage(db, 'msg-u1', 'sess-1', 1700000000000, { role: 'user' })
|
|
insertPart(db, 'part-a', 'msg-u1', 'sess-1', { type: 'text', text: 'hello' })
|
|
insertPart(db, 'part-b', 'msg-u1', 'sess-1', { type: 'text', text: 'world' })
|
|
|
|
insertMessage(db, 'msg-a1', 'sess-1', 1700000001000, {
|
|
role: 'assistant', modelID: 'claude-opus-4-6', cost: 0.01,
|
|
tokens: { input: 50, output: 50, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
})
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls[0]!.userMessage).toBe('hello world')
|
|
})
|
|
|
|
it('yields nothing for session with only user messages', async () => {
|
|
const dbPath = createTestDb(tmpDir)
|
|
withTestDb(dbPath, (db) => {
|
|
insertSession(db, 'sess-1')
|
|
insertMessage(db, 'msg-u1', 'sess-1', 1700000000000, { role: 'user' })
|
|
insertPart(db, 'part-u1', 'msg-u1', 'sess-1', { type: 'text', text: 'hello?' })
|
|
})
|
|
|
|
const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1')
|
|
expect(calls).toHaveLength(0)
|
|
})
|
|
})
|