mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
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.
This commit is contained in:
@@ -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<typeof vi.spyOn>
|
||||
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<void> {
|
||||
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<SessionParseStats> {
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user