import assert from 'node:assert/strict'; import { mkdir, mkdtemp, rm } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import Database from 'better-sqlite3'; import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js'; import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js'; import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js'; const patchHomeDir = (nextHomeDir: string) => { const original = os.homedir; (os as any).homedir = () => nextHomeDir; return () => { (os as any).homedir = original; }; }; async function withIsolatedDatabase(runTest: () => void | Promise): Promise { const previousDatabasePath = process.env.DATABASE_PATH; const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'opencode-provider-db-')); const databasePath = path.join(tempDirectory, 'auth.db'); closeConnection(); process.env.DATABASE_PATH = databasePath; await initializeDatabase(); try { await runTest(); } finally { closeConnection(); if (previousDatabasePath === undefined) { delete process.env.DATABASE_PATH; } else { process.env.DATABASE_PATH = previousDatabasePath; } await rm(tempDirectory, { recursive: true, force: true }); } } const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): Promise => { const dataDir = path.join(homeDir, '.local', 'share', 'opencode'); await mkdir(dataDir, { recursive: true }); const db = new Database(path.join(dataDir, 'opencode.db')); try { db.exec(` CREATE TABLE project ( id TEXT PRIMARY KEY, worktree TEXT NOT NULL, vcs TEXT, name TEXT, icon_url TEXT, icon_color TEXT, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, time_initialized INTEGER, sandboxes TEXT NOT NULL, commands TEXT, icon_url_override TEXT ); 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, share_url TEXT, summary_additions INTEGER, summary_deletions INTEGER, summary_files INTEGER, summary_diffs TEXT, revert TEXT, permission TEXT, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, time_compacting INTEGER, time_archived INTEGER, workspace_id TEXT, path TEXT, agent TEXT, model TEXT, FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE ); CREATE TABLE message ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL, FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE ); CREATE TABLE part ( id TEXT PRIMARY KEY, message_id TEXT NOT NULL, session_id TEXT NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL, data TEXT NOT NULL, FOREIGN KEY (message_id) REFERENCES message(id) ON DELETE CASCADE ); CREATE INDEX part_session_idx ON part (session_id); CREATE INDEX session_project_idx ON session (project_id); CREATE INDEX message_session_time_created_id_idx ON message (session_id, time_created, id); CREATE INDEX part_message_id_id_idx ON part (message_id, id); `); db.prepare( 'INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?)', ).run( 'project-1', workspacePath, 1_700_000_000_000, 1_700_000_001_000, '[]', ); db.prepare(` INSERT INTO session ( id, project_id, slug, directory, title, version, time_created, time_updated, time_archived ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( 'open-session-1', 'project-1', 'open-session-1', workspacePath, 'OpenCode indexed title', '0.0.0', 1_700_000_000_000, 1_700_000_004_000, null, ); const userMessageData = JSON.stringify({ role: 'user', time: { created: 1_700_000_001_000 }, agent: 'test', model: { providerID: 'anthropic', modelID: 'claude' }, }); const assistantMessageData = JSON.stringify({ role: 'assistant', time: { created: 1_700_000_002_000, completed: 1_700_000_003_000 }, parentID: 'message-user', modelID: 'anthropic/claude-sonnet-4-5', providerID: 'anthropic', mode: 'default', agent: 'test', path: { cwd: '.', root: '.' }, cost: 0.01, tokens: { input: 10, output: 20, reasoning: 0, cache: { read: 3, write: 2 }, }, }); db.prepare( 'INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)', ).run('message-user', 'open-session-1', 1_700_000_001_000, 1_700_000_001_500, userMessageData); db.prepare( 'INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)', ).run('message-assistant', 'open-session-1', 1_700_000_002_000, 1_700_000_003_000, assistantMessageData); const insertPart = db.prepare(` INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?) `); insertPart.run( 'part-user-text', 'message-user', 'open-session-1', 1_700_000_001_000, 1_700_000_001_000, JSON.stringify({ type: 'text', text: JSON.stringify('Build the OpenCode integration.'), }), ); insertPart.run( 'part-reasoning', 'message-assistant', 'open-session-1', 1_700_000_002_000, 1_700_000_002_000, JSON.stringify({ type: 'reasoning', text: 'I will inspect the provider shape first.', time: { start: 0, end: 1 }, }), ); insertPart.run( 'part-assistant-text', 'message-assistant', 'open-session-1', 1_700_000_002_500, 1_700_000_002_500, JSON.stringify({ type: 'text', text: 'The provider is wired.', }), ); insertPart.run( 'part-tool', 'message-assistant', 'open-session-1', 1_700_000_003_000, 1_700_000_003_000, JSON.stringify({ type: 'tool', tool: 'bash', callID: 'tool-call-1', state: { status: 'completed', input: { command: 'npm test' }, output: 'ok', title: 'bash', metadata: {}, time: { start: 0, end: 1 }, }, }), ); } finally { db.close(); } }; test('OpenCode session synchronizer indexes sqlite sessions without deletable transcript paths', { concurrency: false }, async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-')); const workspacePath = path.join(tempRoot, 'workspace'); await mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { await createOpenCodeDatabase(tempRoot, workspacePath); await withIsolatedDatabase(() => { const synchronizer = new OpenCodeSessionSynchronizer(); const processed = synchronizer.synchronize(); return Promise.resolve(processed).then((count) => { assert.equal(count, 1); const indexed = sessionsDb.getSessionById('open-session-1'); assert.equal(indexed?.provider, 'opencode'); assert.equal(indexed?.project_path, workspacePath); assert.equal(indexed?.custom_name, 'OpenCode indexed title'); assert.equal(indexed?.jsonl_path, null); }); }); } finally { restoreHomeDir(); await rm(tempRoot, { recursive: true, force: true }); } }); test('OpenCode sessions provider normalizes quoted live text and skips user echoes', () => { const provider = new OpenCodeSessionsProvider(); const normalized = provider.normalizeMessage({ type: 'text', sessionID: 'open-session-live', text: JSON.stringify('hello bro'), }, null); assert.equal(normalized.length, 1); assert.equal(normalized[0]?.kind, 'stream_delta'); assert.equal(normalized[0]?.content, 'hello bro'); const userEcho = provider.normalizeMessage({ type: 'text', sessionID: 'open-session-live', role: 'user', text: 'hello bro', }, null); assert.deepEqual(userEcho, []); }); test('OpenCode sessions provider reads sqlite history and token usage', { concurrency: false }, async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-history-')); const workspacePath = path.join(tempRoot, 'workspace'); await mkdir(workspacePath, { recursive: true }); const restoreHomeDir = patchHomeDir(tempRoot); try { await createOpenCodeDatabase(tempRoot, workspacePath); const provider = new OpenCodeSessionsProvider(); const history = await provider.fetchHistory('open-session-1'); assert.equal(history.total, 4); assert.equal(history.messages[0]?.kind, 'text'); assert.equal(history.messages[0]?.role, 'user'); assert.equal(history.messages[0]?.content, 'Build the OpenCode integration.'); assert.equal(history.messages[1]?.kind, 'thinking'); assert.equal(history.messages[2]?.content, 'The provider is wired.'); assert.equal(history.messages[3]?.kind, 'tool_use'); assert.deepEqual(history.messages[3]?.toolResult, { content: 'ok', isError: false }); assert.deepEqual(history.tokenUsage, { used: 35, total: 35, inputTokens: 10, outputTokens: 20, cacheReadTokens: 3, cacheCreationTokens: 2, }); const paged = await provider.fetchHistory('open-session-1', { limit: 2, offset: 0 }); assert.equal(paged.messages.length, 2); assert.equal(paged.hasMore, true); assert.equal(paged.messages[0]?.content, 'The provider is wired.'); } finally { restoreHomeDir(); await rm(tempRoot, { recursive: true, force: true }); } });