From 64d886916f4e12ad80bcb0cfe08e31ee81b9701a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 2 Sep 2026 03:29:19 -0400 Subject: [PATCH] test(ai-vault-search): cover OpenCode SQLite session freshness Share the OpenCode DB fixture builders instead of keeping them inline in the SQLite scanner suite, then drive discovery + a required-mode parse over a session that gains a message: the synthetic candidate's mtime follows time_updated, so the cache re-parses and the sink replaces the indexed rows. An unchanged session still reuses the cached parse and leaves the index alone. --- .../session-search-opencode-freshness.test.ts | 176 ++++++++++++ ...ession-scanner-opencode-sqlite-fixtures.ts | 180 +++++++++++++ .../session-scanner-opencode-sqlite.test.ts | 253 ++++-------------- 3 files changed, 404 insertions(+), 205 deletions(-) create mode 100644 src/main/ai-vault-search/session-search-opencode-freshness.test.ts create mode 100644 src/main/ai-vault/session-scanner-opencode-sqlite-fixtures.ts diff --git a/src/main/ai-vault-search/session-search-opencode-freshness.test.ts b/src/main/ai-vault-search/session-search-opencode-freshness.test.ts new file mode 100644 index 00000000000..18b4194b9a1 --- /dev/null +++ b/src/main/ai-vault-search/session-search-opencode-freshness.test.ts @@ -0,0 +1,176 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { AiVaultScanIssue } from '../../shared/ai-vault-types' +import { sessionCandidatesFromDiscoveries } from '../ai-vault/session-scanner-candidates' +import { + applyOpenCodeSqliteSchema, + insertOpenCodeMessage, + insertOpenCodePart, + insertOpenCodeSession, + touchOpenCodeSession +} from '../ai-vault/session-scanner-opencode-sqlite-fixtures' +import { + createSessionParseStats, + parseAgentSessionFileCached, + resetSessionParseCacheForTests, + type SessionParseStats +} from '../ai-vault/session-scanner-parse-cache' +import { discoverAiVaultSessionSources } from '../ai-vault/session-scanner-source-discovery' +import { isolatedScanRoots } from '../ai-vault/session-scanner-test-fixtures' +import type { AiVaultScanOptions } from '../ai-vault/session-scanner-types' +import { + registerSessionSearchIndexSink, + withSessionSearchIndexRequired +} from '../ai-vault/session-search-capture' +import Database from '../sqlite/sync-database' +import { SessionSearchStore } from './session-search-store' + +// Why: a source-level suite has no built worker bundle, so the SQLite reads run +// inline here; production fails closed when the bundle is absent. +vi.mock('../ai-vault/session-scanner-opencode-sqlite-worker-spawn', async () => { + const [{ listOpenCodeSqliteSessions }, { parseOpenCodeSqliteSession }] = await Promise.all([ + import('../ai-vault/session-scanner-opencode-sqlite-list'), + import('../ai-vault/session-scanner-opencode-sqlite') + ]) + return { + listOpenCodeSqliteSessionsViaWorker: listOpenCodeSqliteSessions, + parseOpenCodeSqliteSessionViaWorker: parseOpenCodeSqliteSession + } +}) + +const SESSION_ID = 'ses_fresh0000000000000000000000' +const CREATED_MS = 1_777_634_000_000 + +let tempRoots: string[] = [] +let store: SessionSearchStore +let apply: ReturnType + +beforeEach(async () => { + resetSessionParseCacheForTests() + store = new SessionSearchStore(join(await makeTempDir(), 'index.sqlite'), (error) => { + throw error + }) + apply = vi.spyOn(store, 'apply') + registerSessionSearchIndexSink(store) +}) + +afterEach(async () => { + registerSessionSearchIndexSink(null) + store.close() + await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true }))) + tempRoots = [] +}) + +async function makeTempDir(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-opencode-freshness-')) + tempRoots.push(root) + return root +} + +/** One opencode.db holding a single session with its first user turn. */ +async function createOpenCodeDb(): Promise { + const dbPath = join(await makeTempDir(), 'opencode.db') + const db = new Database(dbPath) + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { + id: SESSION_ID, + title: 'Ballast planning', + timeCreated: CREATED_MS, + timeUpdated: CREATED_MS + 1_000 + }) + insertOpenCodeMessage(db, { + id: 'msg_1', + sessionId: SESSION_ID, + role: 'user', + timeCreated: CREATED_MS + 500 + }) + insertOpenCodePart(db, { + id: 'prt_1', + messageId: 'msg_1', + sessionId: SESSION_ID, + timeCreated: CREATED_MS + 500, + text: 'how do we raise the vacuum quota for the intake pump' + }) + db.close() + return dbPath +} + +/** Appends an assistant turn and bumps the session row, as OpenCode does. */ +async function appendAssistantTurn(dbPath: string): Promise { + const db = new Database(dbPath) + insertOpenCodeMessage(db, { + id: 'msg_2', + sessionId: SESSION_ID, + role: 'assistant', + timeCreated: CREATED_MS + 1_500 + }) + insertOpenCodePart(db, { + id: 'prt_2', + messageId: 'msg_2', + sessionId: SESSION_ID, + timeCreated: CREATED_MS + 1_500, + text: 'drain the ballast tanks before the next calibration run' + }) + touchOpenCodeSession(db, { id: SESSION_ID, timeUpdated: CREATED_MS + 2_000 }) + db.close() +} + +/** Mirrors SessionSearchService.refreshRecent: discover, then parse in required mode. */ +async function refreshRecent(dbPath: string): Promise { + const options: AiVaultScanOptions = { + ...isolatedScanRoots(await makeTempDir()), + opencodeDbPaths: [dbPath] + } + const issues: AiVaultScanIssue[] = [] + const discoveries = await discoverAiVaultSessionSources({ options, limitPerAgent: 12, issues }) + const candidates = await sessionCandidatesFromDiscoveries(discoveries, options) + const stats = createSessionParseStats() + await withSessionSearchIndexRequired(async () => { + for (const candidate of candidates) { + await parseAgentSessionFileCached(candidate, process.platform, stats) + } + }) + return stats +} + +describe('OpenCode SQLite session freshness', () => { + it('re-indexes a session that gained a message since the last parse', async () => { + const dbPath = await createOpenCodeDb() + + const first = await refreshRecent(dbPath) + expect(first.fullParses).toBe(1) + expect(store.search({ query: 'vacuum quota' }).hits).toMatchObject([ + { agent: 'opencode', sessionId: SESSION_ID } + ]) + expect(store.coverage().messagesIndexed).toBe(1) + + await appendAssistantTurn(dbPath) + const second = await refreshRecent(dbPath) + + expect(second.reused).toBe(0) + expect(second.fullParses).toBe(1) + expect(apply).toHaveBeenCalledTimes(2) + expect(apply.mock.calls[1]?.[0]).toMatchObject({ mode: 'replace' }) + expect(store.search({ query: 'ballast tanks' }).hits).toMatchObject([ + { agent: 'opencode', sessionId: SESSION_ID } + ]) + // The replace re-emits the whole session, so the first turn stays searchable. + expect(store.search({ query: 'vacuum quota' }).hits).toHaveLength(1) + expect(store.coverage().messagesIndexed).toBe(2) + }) + + it('reuses the cached parse and leaves the index alone when nothing changed', async () => { + const dbPath = await createOpenCodeDb() + await refreshRecent(dbPath) + expect(apply).toHaveBeenCalledTimes(1) + + const second = await refreshRecent(dbPath) + + expect(second.reused).toBe(1) + expect(second.fullParses).toBe(0) + expect(apply).toHaveBeenCalledTimes(1) + expect(store.coverage().messagesIndexed).toBe(1) + }) +}) diff --git a/src/main/ai-vault/session-scanner-opencode-sqlite-fixtures.ts b/src/main/ai-vault/session-scanner-opencode-sqlite-fixtures.ts new file mode 100644 index 00000000000..186d0817f34 --- /dev/null +++ b/src/main/ai-vault/session-scanner-opencode-sqlite-fixtures.ts @@ -0,0 +1,180 @@ +import type Database from '../sqlite/sync-database' + +// OpenCode 1.17.x DB shape used by the SQLite scanner suites: the schema plus +// row builders for `session`, `message` and `part`. Shared so the discovery, +// parser and search-freshness tests all exercise one fixture definition. + +/** Full 1.17.x schema, including the columns the scanner treats as optional. */ +export function applyOpenCodeSqliteSchema(db: Database.Database): void { + 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, + 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, + cost REAL DEFAULT 0 NOT NULL, + tokens_input INTEGER DEFAULT 0 NOT NULL, + tokens_output INTEGER DEFAULT 0 NOT NULL, + tokens_reasoning INTEGER DEFAULT 0 NOT NULL, + tokens_cache_read INTEGER DEFAULT 0 NOT NULL, + tokens_cache_write INTEGER DEFAULT 0 NOT NULL, + metadata TEXT + ); + 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 + ); + 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 + ); + 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 + ); + `) +} + +/** Only the columns the list reader requires, for the older-schema paths. */ +export function applyMinimalOpenCodeSqliteSchema(db: Database.Database): void { + db.exec(`CREATE TABLE session ( + id TEXT PRIMARY KEY, + time_created INTEGER NOT NULL, + time_updated INTEGER NOT NULL + );`) +} + +export function insertOpenCodeSession( + db: Database.Database, + args: { + id: string + title?: string + directory?: string + timeCreated: number + timeUpdated: number + parentId?: string | null + timeArchived?: number | null + model?: string | null + agent?: string | null + tokensInput?: number + tokensOutput?: number + tokensReasoning?: number + tokensCacheRead?: number + cost?: number + } +): void { + db.prepare( + `INSERT INTO session (id, project_id, parent_id, slug, directory, title, version, + time_created, time_updated, time_archived, model, agent, cost, + tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write) + VALUES (?, 'proj-1', ?, ?, ?, ?, '1.0.0', + ?, ?, ?, ?, ?, ?, + ?, ?, ?, ?, 0)` + ).run( + args.id, + args.parentId ?? null, + `slug-${args.id}`, + args.directory ?? '/tmp/opencode', + args.title ?? 'OpenCode title', + args.timeCreated, + args.timeUpdated, + args.timeArchived ?? null, + args.model ?? JSON.stringify({ id: 'glm-5.2', providerID: 'zai-coding-plan' }), + args.agent ?? 'build', + args.cost ?? 0, + args.tokensInput ?? 100, + args.tokensOutput ?? 40, + args.tokensReasoning ?? 10, + args.tokensCacheRead ?? 5 + ) +} + +export function insertOpenCodeMessage( + db: Database.Database, + args: { + id: string + sessionId: string + role: 'user' | 'assistant' + timeCreated: number + summaryTitle?: string | null + summaryBody?: string | null + } +): void { + const data = JSON.stringify({ + role: args.role, + time: { created: args.timeCreated }, + agent: 'build', + summary: + args.summaryTitle || args.summaryBody + ? { title: args.summaryTitle ?? null, body: args.summaryBody ?? null } + : undefined + }) + db.prepare( + `INSERT INTO message (id, session_id, time_created, time_updated, data) + VALUES (?, ?, ?, ?, ?)` + ).run(args.id, args.sessionId, args.timeCreated, args.timeCreated, data) +} + +export function insertOpenCodePart( + db: Database.Database, + args: { + id: string + messageId: string + sessionId: string + timeCreated: number + type?: 'text' | 'tool' | 'reasoning' + text?: string + } +): void { + const data = JSON.stringify({ + type: args.type ?? 'text', + text: args.text ?? 'hello world' + }) + db.prepare( + `INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) + VALUES (?, ?, ?, ?, ?, ?)` + ).run(args.id, args.messageId, args.sessionId, args.timeCreated, args.timeCreated, data) +} + +/** Mirrors OpenCode bumping the session row when a message lands. */ +export function touchOpenCodeSession( + db: Database.Database, + args: { id: string; timeUpdated: number } +): void { + db.prepare('UPDATE session SET time_updated = ? WHERE id = ?').run(args.timeUpdated, args.id) +} diff --git a/src/main/ai-vault/session-scanner-opencode-sqlite.test.ts b/src/main/ai-vault/session-scanner-opencode-sqlite.test.ts index f7e042d663b..2c4a075cc8c 100644 --- a/src/main/ai-vault/session-scanner-opencode-sqlite.test.ts +++ b/src/main/ai-vault/session-scanner-opencode-sqlite.test.ts @@ -3,6 +3,13 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import Database from '../sqlite/sync-database' +import { + applyMinimalOpenCodeSqliteSchema, + applyOpenCodeSqliteSchema, + insertOpenCodeMessage, + insertOpenCodePart, + insertOpenCodeSession +} from './session-scanner-opencode-sqlite-fixtures' import { buildOpenCodeSqliteCandidatePath } from './session-scanner-opencode-sqlite-paths' import { listOpenCodeSqliteSessions } from './session-scanner-opencode-sqlite-discovery' import { parseOpenCodeSqliteSession } from './session-scanner-opencode-sqlite' @@ -25,181 +32,17 @@ function createTempDb(): { db: Database.Database; path: string } { return { db: new Database(path), path } } -function applyOpenCodeSchema(db: Database.Database): void { - 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, - 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, - cost REAL DEFAULT 0 NOT NULL, - tokens_input INTEGER DEFAULT 0 NOT NULL, - tokens_output INTEGER DEFAULT 0 NOT NULL, - tokens_reasoning INTEGER DEFAULT 0 NOT NULL, - tokens_cache_read INTEGER DEFAULT 0 NOT NULL, - tokens_cache_write INTEGER DEFAULT 0 NOT NULL, - metadata TEXT - ); - 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 - ); - 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 - ); - 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 - ); - `) -} - -function applyMinimalOpenCodeSchema(db: Database.Database): void { - db.exec(`CREATE TABLE session ( - id TEXT PRIMARY KEY, - time_created INTEGER NOT NULL, - time_updated INTEGER NOT NULL - );`) -} - -function insertSession( - db: Database.Database, - args: { - id: string - title?: string - directory?: string - timeCreated: number - timeUpdated: number - parentId?: string | null - timeArchived?: number | null - model?: string | null - agent?: string | null - tokensInput?: number - tokensOutput?: number - tokensReasoning?: number - tokensCacheRead?: number - cost?: number - } -): void { - db.prepare( - `INSERT INTO session (id, project_id, parent_id, slug, directory, title, version, - time_created, time_updated, time_archived, model, agent, cost, - tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write) - VALUES (?, 'proj-1', ?, ?, ?, ?, '1.0.0', - ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, 0)` - ).run( - args.id, - args.parentId ?? null, - `slug-${args.id}`, - args.directory ?? '/tmp/opencode', - args.title ?? 'OpenCode title', - args.timeCreated, - args.timeUpdated, - args.timeArchived ?? null, - args.model ?? JSON.stringify({ id: 'glm-5.2', providerID: 'zai-coding-plan' }), - args.agent ?? 'build', - args.cost ?? 0, - args.tokensInput ?? 100, - args.tokensOutput ?? 40, - args.tokensReasoning ?? 10, - args.tokensCacheRead ?? 5 - ) -} - -function insertMessage( - db: Database.Database, - args: { - id: string - sessionId: string - role: 'user' | 'assistant' - timeCreated: number - summaryTitle?: string | null - summaryBody?: string | null - } -): void { - const data = JSON.stringify({ - role: args.role, - time: { created: args.timeCreated }, - agent: 'build', - summary: - args.summaryTitle || args.summaryBody - ? { title: args.summaryTitle ?? null, body: args.summaryBody ?? null } - : undefined - }) - db.prepare( - `INSERT INTO message (id, session_id, time_created, time_updated, data) - VALUES (?, ?, ?, ?, ?)` - ).run(args.id, args.sessionId, args.timeCreated, args.timeCreated, data) -} - -function insertPart( - db: Database.Database, - args: { - id: string - messageId: string - sessionId: string - timeCreated: number - type?: 'text' | 'tool' | 'reasoning' - text?: string - } -): void { - const data = JSON.stringify({ - type: args.type ?? 'text', - text: args.text ?? 'hello world' - }) - db.prepare( - `INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) - VALUES (?, ?, ?, ?, ?, ?)` - ).run(args.id, args.messageId, args.sessionId, args.timeCreated, args.timeCreated, data) -} describe('listOpenCodeSqliteSessions', () => { it('returns candidates sorted by time_updated desc via the synthesized mtimeMs', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_old', title: 'Old', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_001_000 }) - insertSession(db, { + insertOpenCodeSession(db, { id: 'ses_new', title: 'New', timeCreated: 1_777_634_002_000, @@ -223,8 +66,8 @@ describe('listOpenCodeSqliteSessions', () => { it('dedups matching session ids across databases and keeps the newest row', async () => { const { db: oldDb, path: oldPath } = createTempDb() - applyOpenCodeSchema(oldDb) - insertSession(oldDb, { + applyOpenCodeSqliteSchema(oldDb) + insertOpenCodeSession(oldDb, { id: 'ses_duplicate', title: 'Old duplicate', timeCreated: 1_777_634_000_000, @@ -233,8 +76,8 @@ describe('listOpenCodeSqliteSessions', () => { oldDb.close() const { db: newDb, path: newPath } = createTempDb() - applyOpenCodeSchema(newDb) - insertSession(newDb, { + applyOpenCodeSqliteSchema(newDb) + insertOpenCodeSession(newDb, { id: 'ses_duplicate', title: 'New duplicate', timeCreated: 1_777_634_002_000, @@ -252,19 +95,19 @@ describe('listOpenCodeSqliteSessions', () => { }) it('excludes archived and child sessions', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_normal', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_001_000 }) - insertSession(db, { + insertOpenCodeSession(db, { id: 'ses_archived', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_002_000, timeArchived: 1_777_634_002_500 }) - insertSession(db, { + insertOpenCodeSession(db, { id: 'ses_child', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_003_000, @@ -309,7 +152,7 @@ describe('listOpenCodeSqliteSessions', () => { it('lists sessions from a minimal readable session table', async () => { const { db, path } = createTempDb() - applyMinimalOpenCodeSchema(db) + applyMinimalOpenCodeSqliteSchema(db) db.prepare(`INSERT INTO session VALUES ('ses_minimal', 1777634000000, 1777634001000)`).run() db.close() @@ -328,8 +171,8 @@ describe('listOpenCodeSqliteSessions', () => { describe('parseOpenCodeSqliteSession', () => { it('builds an AiVaultSession with title, cwd, model, tokens, and resume command', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_1', title: 'OpenCode title', directory: '/tmp/opencode', @@ -341,27 +184,27 @@ describe('parseOpenCodeSqliteSession', () => { tokensCacheRead: 5, cost: 0.01 }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_1', sessionId: 'ses_1', role: 'user', timeCreated: 1_777_634_000_500, summaryTitle: 'OpenCode title' }) - insertPart(db, { + insertOpenCodePart(db, { id: 'prt_1', messageId: 'msg_1', sessionId: 'ses_1', timeCreated: 1_777_634_000_500, text: 'Plan the work' }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_2', sessionId: 'ses_1', role: 'assistant', timeCreated: 1_777_634_000_900 }) - insertPart(db, { + insertOpenCodePart(db, { id: 'prt_2', messageId: 'msg_2', sessionId: 'ses_1', @@ -396,21 +239,21 @@ describe('parseOpenCodeSqliteSession', () => { it('falls back to summary.body for title when session.title is empty', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_2', title: '', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_001_000 }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_1', sessionId: 'ses_2', role: 'user', timeCreated: 1_777_634_000_500, summaryBody: 'fallback title from summary' }) - insertPart(db, { + insertOpenCodePart(db, { id: 'prt_1', messageId: 'msg_1', sessionId: 'ses_2', @@ -430,8 +273,8 @@ describe('parseOpenCodeSqliteSession', () => { it('returns null when the session id is not found', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_real', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_001_000 @@ -459,7 +302,7 @@ describe('parseOpenCodeSqliteSession', () => { it('parses a minimal readable session table without optional columns or messages', async () => { const { db, path } = createTempDb() - applyMinimalOpenCodeSchema(db) + applyMinimalOpenCodeSqliteSchema(db) db.prepare(`INSERT INTO session VALUES ('ses_minimal', 1777634000000, 1777634001000)`).run() db.close() @@ -481,8 +324,8 @@ describe('parseOpenCodeSqliteSession', () => { it('extracts model from older modelID schema', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_3', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_001_000, @@ -500,26 +343,26 @@ describe('parseOpenCodeSqliteSession', () => { it('captures every text part of the earliest user message and no later turn', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_fp', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_900_000 }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_1', sessionId: 'ses_fp', role: 'user', timeCreated: 1_777_634_000_000 }) - insertPart(db, { + insertOpenCodePart(db, { id: 'part_1a', messageId: 'msg_1', sessionId: 'ses_fp', timeCreated: 10, text: 'first ask line one' }) - insertPart(db, { + insertOpenCodePart(db, { id: 'part_1b', messageId: 'msg_1', sessionId: 'ses_fp', @@ -527,7 +370,7 @@ describe('parseOpenCodeSqliteSession', () => { text: 'first ask line two' }) // Non-text parts of the same message must not leak into the copied prompt. - insertPart(db, { + insertOpenCodePart(db, { id: 'part_1c', messageId: 'msg_1', sessionId: 'ses_fp', @@ -535,13 +378,13 @@ describe('parseOpenCodeSqliteSession', () => { type: 'tool', text: 'tool output blob' }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_2', sessionId: 'ses_fp', role: 'user', timeCreated: 1_777_634_500_000 }) - insertPart(db, { + insertOpenCodePart(db, { id: 'part_2a', messageId: 'msg_2', sessionId: 'ses_fp', @@ -559,19 +402,19 @@ describe('parseOpenCodeSqliteSession', () => { it('skips an earliest user message that has no text parts', async () => { const { db, path } = createTempDb() - applyOpenCodeSchema(db) - insertSession(db, { + applyOpenCodeSqliteSchema(db) + insertOpenCodeSession(db, { id: 'ses_fp2', timeCreated: 1_777_634_000_000, timeUpdated: 1_777_634_900_000 }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_1', sessionId: 'ses_fp2', role: 'user', timeCreated: 1_777_634_000_000 }) - insertPart(db, { + insertOpenCodePart(db, { id: 'part_1a', messageId: 'msg_1', sessionId: 'ses_fp2', @@ -579,13 +422,13 @@ describe('parseOpenCodeSqliteSession', () => { type: 'tool', text: 'tool only' }) - insertMessage(db, { + insertOpenCodeMessage(db, { id: 'msg_2', sessionId: 'ses_fp2', role: 'user', timeCreated: 1_777_634_500_000 }) - insertPart(db, { + insertOpenCodePart(db, { id: 'part_2a', messageId: 'msg_2', sessionId: 'ses_fp2',