Files
orca/src/main/native-chat/transcript-opencode-sqlite-query.test.ts
T
OrcaWinandOrcaWin 3286f3b259 [comment audit] Clean up IPC, host lifecycle and native-chat comments (#32966)
* [comment audit] Tighten watcher lifetime and file-write constraints [skip ci]

* [comment audit] Clarify IPC routing and notification comments [skip ci]

* [comment audit] Shorten watcher, preflight and PTY comments [skip ci]

* [comment audit] Clarify PTY delivery and restore ownership [skip ci]

* [comment audit] Tighten PTY environment and flow-control rationale [skip ci]

* [comment audit] Clarify IPC spawn and stable-pane guarantees [skip ci]

* [comment audit] Tighten PTY provider and filesystem ownership rationale [skip ci]

* [comment audit] Clarify clone ownership and remote runtime recovery [skip ci]

* [comment audit] Condense upload, settings and SSH lifecycle comments [skip ci]

* [comment audit] Shorten shutdown, preview and worktree watcher rationale [skip ci]

* [comment audit] Clarify worktree identity, remote ownership and copy limits [skip ci]

* [comment audit] Shorten worktree catalog, removal and WSL routing rationale [skip ci]

* [comment audit] Tighten Linear credential, pagination and export comments [skip ci]

* [comment audit] Condense managed-server activation, snapshot and launch rationale [skip ci]

* [comment audit] Trim runtime installation and managed-server lifecycle comments [skip ci]

* [comment audit] Simplify managed tunnel, memory and menu comments [skip ci]

* [comment audit] Condense integration, model catalog and attachment rationale [skip ci]

* [comment audit] Shorten journal persistence, replay and queue rationale [skip ci]

* [comment audit] Condense journal ordering and timeline lifecycle comments [skip ci]

* [comment audit] Trim timeline and subscriber delivery explanations [skip ci]

* [comment audit] Shorten Stop, child-exit and recovery contracts [skip ci]

* [comment audit] Simplify handover and host recovery comments [skip ci]

* [comment audit] Trim queue delivery and provider startup commentary [skip ci]

* [comment audit] Clarify restart and rewind safety comments [skip ci]

* [comment audit] Trim status, Stop and completion commentary [skip ci]

* [comment audit] Simplify transcript lookup and visual containment comments [skip ci]

* [comment audit] Trim transcript reader, watcher and WSL transport commentary [skip ci]

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 19:07:32 -07:00

534 lines
18 KiB
TypeScript

import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AGENT_SESSION_HOST_STATUS_COPY } from '../../shared/agent-session-host-status-rows'
import Database from '../sqlite/sync-database'
import {
readOpenCodeTranscriptPage,
readOpenCodeTranscriptSignal
} from './transcript-opencode-sqlite-query'
let tempDirs: string[] = []
let openDbs: Database.Database[] = []
afterEach(() => {
// Close before removal: Windows locks open databases.
for (const db of openDbs) {
db.close()
}
openDbs = []
for (const dir of tempDirs) {
rmSync(dir, { recursive: true, force: true })
}
tempDirs = []
})
function createTempDb(): { db: Database.Database; path: string } {
const dir = mkdtempSync(join(tmpdir(), 'orca-opencode-native-chat-'))
tempDirs.push(dir)
const path = join(dir, 'opencode.db')
const db = new Database(path)
openDbs.push(db)
return { db, path }
}
function applySchema(db: Database.Database): void {
db.exec(`
CREATE TABLE session (id TEXT PRIMARY KEY);
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_updated INTEGER NOT NULL,
data TEXT NOT NULL
);
INSERT INTO session (id) VALUES ('ses-1'), ('ses-2');
`)
}
function insertMessage(
db: Database.Database,
args: { id: string; sessionId?: string; time: number; role?: string }
): void {
db.prepare(
'INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)'
).run(
args.id,
args.sessionId ?? 'ses-1',
args.time,
args.time,
JSON.stringify({ role: args.role ?? 'user' })
)
}
function insertPart(
db: Database.Database,
args: { id: string; messageId: string; sessionId?: string; time: number; data: unknown }
): void {
db.prepare(
'INSERT INTO part (id, message_id, session_id, time_updated, data) VALUES (?, ?, ?, ?, ?)'
).run(args.id, args.messageId, args.sessionId ?? 'ses-1', args.time, JSON.stringify(args.data))
}
function messageRowid(db: Database.Database, id: string): number {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: node:sqlite rows are unknown; the SELECT projects exactly one `r` integer column.
return (db.prepare('SELECT rowid AS r FROM message WHERE id = ?').get(id) as { r: number }).r
}
describe('readOpenCodeTranscriptPage', () => {
it.each(['message', 'part'] as const)(
'omits an oversized %s without losing identity or pagination',
(table) => {
const { db, path } = createTempDb()
applySchema(db)
for (let index = 1; index <= 3; index++) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'text', text: `message ${index}` }
})
}
db.prepare(`UPDATE ${table} SET data = ? WHERE id = ?`).run(
JSON.stringify({
role: 'user',
type: 'file',
mime: 'image/png',
url: 'x'.repeat(2 * 1024 * 1024)
}),
table === 'message' ? 'msg-2' : 'prt-2'
)
const args = { dbPath: path, sessionId: 'ses-1', limit: 2 }
const page = readOpenCodeTranscriptPage(args)
expect(page?.items.map((item) => item.message.id)).toEqual(['msg-2', 'msg-3'])
expect(page?.items[0]?.message.blocks).toEqual([
{
type: 'text',
text: AGENT_SESSION_HOST_STATUS_COPY['history-item-too-large'],
presentation: 'history-item-too-large'
}
])
expect(page?.items[0]?.rowid).toBe(messageRowid(db, 'msg-2'))
expect(readOpenCodeTranscriptPage(args)?.items[0]?.fingerprint).toBe(
page?.items[0]?.fingerprint
)
db.prepare(`UPDATE ${table} SET time_updated = 10 WHERE id = ?`).run(
table === 'message' ? 'msg-2' : 'prt-2'
)
expect(readOpenCodeTranscriptPage(args)?.items[0]?.fingerprint).not.toBe(
page?.items[0]?.fingerprint
)
expect(
readOpenCodeTranscriptPage({
...args,
beforeMessageRowId: page?.beforeMessageRowId ?? undefined
})?.items.map((item) => item.message.id)
).toEqual(['msg-1'])
}
)
it('preserves ordinary text beside an oversized UTF-8 attachment part', () => {
const { db, path } = createTempDb()
applySchema(db)
insertMessage(db, { id: 'msg', time: 1 })
insertPart(db, {
id: 'text',
messageId: 'msg',
time: 1,
data: { type: 'text', text: 'see this' }
})
insertPart(db, {
id: 'large',
messageId: 'msg',
time: 1,
data: { type: 'file', mime: 'image/png', url: '😀'.repeat(600_000) }
})
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 1 })
expect(page?.items.map((item) => item.message)).toMatchObject([
{ id: 'msg', role: 'user', blocks: [{ type: 'text', text: 'see this' }] },
{
id: 'msg:omission',
role: 'system',
blocks: [{ type: 'text', presentation: 'history-item-too-large' }]
}
])
expect(page?.items[0]?.rowid).toBe(page?.items[1]?.rowid)
expect(page?.items[0]?.message.transcriptOffset).toBe(page?.items[1]?.message.transcriptOffset)
})
it('returns null when the session row does not exist', () => {
const { db, path } = createTempDb()
applySchema(db)
expect(readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'missing', limit: 10 })).toBeNull()
})
it('treats a session_v2-only (OpenCode 2) DB as not-found instead of throwing', () => {
const { db, path } = createTempDb()
// A v2-only store must fail softly in the v1 reader.
db.exec('CREATE TABLE session_v2 (id TEXT PRIMARY KEY)')
expect(readOpenCodeTranscriptSignal(path, 'ses-1')).toBeNull()
expect(readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 10 })).toBeNull()
})
it('returns the NEWEST limit messages when more history exists', () => {
const { db, path } = createTempDb()
applySchema(db)
for (let index = 1; index <= 10; index++) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'text', text: `message ${index}` }
})
}
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 3 })
expect(page).not.toBeNull()
expect(page!.items.map((item) => item.message.id)).toEqual(['msg-8', 'msg-9', 'msg-10'])
expect(page!.hasMore).toBe(true)
expect(page!.beforeMessageRowId).toBe(page!.items[0]!.rowid)
})
it('pages strictly older messages from the raw-row cursor', () => {
const { db, path } = createTempDb()
applySchema(db)
for (let index = 1; index <= 10; index++) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'text', text: `message ${index}` }
})
}
const first = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 3 })
const second = readOpenCodeTranscriptPage({
dbPath: path,
sessionId: 'ses-1',
limit: 3,
beforeMessageRowId: first!.beforeMessageRowId!
})
expect(second!.items.map((item) => item.message.id)).toEqual(['msg-5', 'msg-6', 'msg-7'])
})
it('counts the limit in renderable messages, batching past non-renderable rows', () => {
const { db, path } = createTempDb()
applySchema(db)
for (let index = 1; index <= 6; index++) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: index % 2 === 0 ? { type: 'text', text: `message ${index}` } : { type: 'step-start' }
})
}
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 2 })
expect(page!.items.map((item) => item.message.id)).toEqual(['msg-4', 'msg-6'])
expect(page!.hasMore).toBe(true)
// Cursor tracks the oldest raw row, including filtered rows, so the probe survives for the next page.
expect(page!.beforeMessageRowId).toBe(messageRowid(db, 'msg-3'))
const next = readOpenCodeTranscriptPage({
dbPath: path,
sessionId: 'ses-1',
limit: 2,
beforeMessageRowId: page!.beforeMessageRowId!
})
expect(next!.items.map((item) => item.message.id)).toEqual(['msg-2'])
expect(next!.hasMore).toBe(false)
})
it('walks a fully non-renderable history to its oldest raw row without dead-ending', () => {
const { db, path } = createTempDb()
applySchema(db)
for (let index = 1; index <= 4; index++) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'step-start' }
})
}
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 2 })
expect(page!.items).toEqual([])
expect(page!.hasMore).toBe(false)
expect(page!.beforeMessageRowId).toBe(messageRowid(db, 'msg-1'))
})
it('re-reads messages trimmed by an overshoot batch instead of skipping them', () => {
const { db, path } = createTempDb()
applySchema(db)
// Sparse first batch then dense second batch must trim without skipping overflow.
for (const index of [1, 2, 3, 5]) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'text', text: `message ${index}` }
})
}
insertMessage(db, { id: 'msg-4', time: 4 })
insertPart(db, {
id: 'prt-4',
messageId: 'msg-4',
time: 4,
data: { type: 'step-start' }
})
const seen: string[] = []
let cursor: number | null = null
for (;;) {
const page = readOpenCodeTranscriptPage({
dbPath: path,
sessionId: 'ses-1',
limit: 2,
...(cursor !== null ? { beforeMessageRowId: cursor } : {})
})
seen.push(...page!.items.map((item) => item.message.id))
if (!page!.hasMore) {
break
}
cursor = page!.beforeMessageRowId
}
// Pages arrive newest-first; preserve order within each page and include every message once.
expect(seen).toEqual(['msg-3', 'msg-5', 'msg-1', 'msg-2'])
})
it('keeps hasMore true when a trim happens on the final short batch', () => {
const { db, path } = createTempDb()
applySchema(db)
// A final batch can overshoot despite reaching table end; trimming must retain hasMore.
for (const index of [1, 2, 4]) {
insertMessage(db, { id: `msg-${index}`, time: index })
insertPart(db, {
id: `prt-${index}`,
messageId: `msg-${index}`,
time: index,
data: { type: 'text', text: `message ${index}` }
})
}
insertMessage(db, { id: 'msg-3', time: 3 })
insertPart(db, {
id: 'prt-3',
messageId: 'msg-3',
time: 3,
data: { type: 'step-start' }
})
const first = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 2 })
expect(first!.hasMore).toBe(true)
const second = readOpenCodeTranscriptPage({
dbPath: path,
sessionId: 'ses-1',
limit: 2,
beforeMessageRowId: first!.beforeMessageRowId!
})
expect(second!.items.map((item) => item.message.id)).toEqual(['msg-1'])
expect(second!.hasMore).toBe(false)
})
it('maps image file parts to image refs and skips non-image files', () => {
const { db, path } = createTempDb()
applySchema(db)
// Only native-platform file URLs decode to local paths; foreign paths stay opaque.
const localPath = process.platform === 'win32' ? 'C:\\Users\\u\\pic.jpg' : '/home/user/pic.jpg'
const fileUrl = pathToFileURL(localPath).href
insertMessage(db, { id: 'msg-1', time: 1, role: 'user' })
insertPart(db, {
id: 'prt-data',
messageId: 'msg-1',
time: 1,
data: {
type: 'file',
mime: 'image/png',
url: 'data:image/png;base64,AAA',
filename: 'paste.png'
}
})
insertPart(db, {
id: 'prt-file-url',
messageId: 'msg-1',
time: 1,
data: { type: 'file', mime: 'image/jpeg', url: fileUrl }
})
insertPart(db, {
id: 'prt-plain-path',
messageId: 'msg-1',
time: 1,
data: { type: 'file', mime: 'image/webp', url: '/tmp/opencode/paste.webp' }
})
insertPart(db, {
id: 'prt-pdf',
messageId: 'msg-1',
time: 1,
data: { type: 'file', mime: 'application/pdf', url: 'file:///tmp/doc.pdf' }
})
insertPart(db, {
id: 'prt-text',
messageId: 'msg-1',
time: 1,
data: { type: 'text', text: 'prompt body' }
})
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 10 })
expect(page!.items[0]!.message.blocks).toEqual([
{ type: 'image-ref', url: 'data:image/png;base64,AAA', alt: 'paste.png' },
{ type: 'image-ref', path: localPath },
{ type: 'image-ref', path: '/tmp/opencode/paste.webp' },
{ type: 'text', text: 'prompt body' }
])
})
it('filters synthetic text parts and maps tool parts to call/result blocks', () => {
const { db, path } = createTempDb()
applySchema(db)
insertMessage(db, { id: 'msg-1', time: 1, role: 'user' })
insertPart(db, {
id: 'prt-syn',
messageId: 'msg-1',
time: 1,
data: { type: 'text', text: 'injected context', synthetic: true }
})
insertPart(db, {
id: 'prt-text',
messageId: 'msg-1',
time: 1,
data: { type: 'text', text: 'real prompt' }
})
insertMessage(db, { id: 'msg-2', time: 2, role: 'assistant' })
insertPart(db, {
id: 'prt-tool-done',
messageId: 'msg-2',
time: 2,
data: {
type: 'tool',
tool: 'bash',
callID: 'call-1',
state: { status: 'completed', input: { command: 'ls' }, output: 'file list' }
}
})
insertPart(db, {
id: 'prt-tool-pending',
messageId: 'msg-2',
time: 2,
data: { type: 'tool', tool: 'read', callID: 'call-2', state: { status: 'running' } }
})
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 10 })
const user = page!.items.find((item) => item.message.id === 'msg-1')!
expect(user.message.blocks).toEqual([{ type: 'text', text: 'real prompt' }])
const assistant = page!.items.find((item) => item.message.id === 'msg-2')!
expect(assistant.message.blocks).toEqual([
{
type: 'tool-call',
callId: 'call-1',
name: 'bash',
state: 'completed',
input: { command: 'ls' }
},
{ type: 'tool-result', callId: 'call-1', output: 'file list' },
{ type: 'tool-call', callId: 'call-2', name: 'read', state: 'running', input: undefined }
])
})
it('surfaces a tool error string as the result output', () => {
const { db, path } = createTempDb()
applySchema(db)
insertMessage(db, { id: 'msg-1', time: 1, role: 'assistant' })
insertPart(db, {
id: 'prt-tool-error',
messageId: 'msg-1',
time: 1,
data: {
type: 'tool',
tool: 'bash',
callID: 'call-1',
state: { status: 'error', input: { command: 'ls' }, error: 'exit 1: boom' }
}
})
const page = readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 10 })
expect(page!.items[0]!.message.blocks).toEqual([
{
type: 'tool-call',
callId: 'call-1',
name: 'bash',
state: 'failed',
input: { command: 'ls' }
},
{ type: 'tool-result', callId: 'call-1', output: 'exit 1: boom', isError: true }
])
})
})
describe('readOpenCodeTranscriptSignal', () => {
it('aggregates counts and maxes per session', () => {
const { db, path } = createTempDb()
applySchema(db)
insertMessage(db, { id: 'msg-1', time: 1 })
insertMessage(db, { id: 'msg-2', time: 2 })
insertMessage(db, { id: 'other-1', time: 3, sessionId: 'ses-2' })
insertPart(db, { id: 'prt-1', messageId: 'msg-1', time: 10, data: { type: 'text', text: 'a' } })
insertPart(db, { id: 'prt-2', messageId: 'msg-2', time: 20, data: { type: 'text', text: 'b' } })
db.exec(
"ALTER TABLE session ADD COLUMN directory TEXT; UPDATE session SET directory = '/folder'"
)
const signal = readOpenCodeTranscriptSignal(path, 'ses-1')
expect(signal).toEqual({
cwd: '/folder',
messageCount: 2,
partCount: 2,
maxMessageRowId: 2,
maxPartTimeUpdated: 20
})
})
it('returns null when the session row does not exist', () => {
const { db, path } = createTempDb()
applySchema(db)
expect(readOpenCodeTranscriptSignal(path, 'missing')).toBeNull()
})
})
it('stops before fetching a third part batch when earlier batches exhaust the page byte budget', () => {
const { db, path } = createTempDb()
applySchema(db)
const text = 'x'.repeat(100_000)
db.exec('BEGIN')
{
for (let i = 0; i < 300; i++) {
insertMessage(db, { id: `budget-${i}`, time: i })
insertPart(db, {
id: `part-${i}`,
messageId: `budget-${i}`,
time: i,
data: { type: 'text', text }
})
}
}
db.exec('COMMIT')
const prepared = vi.spyOn(Database.prototype, 'prepare')
try {
expect(() =>
readOpenCodeTranscriptPage({ dbPath: path, sessionId: 'ses-1', limit: 300 })
).toThrow('read limit')
const partQueries = prepared.mock.calls.filter(
([sql]) => sql.includes('FROM part') && sql.includes('message_id IN')
)
expect(partQueries).toHaveLength(2)
} finally {
prepared.mockRestore()
}
})