Files
orca/src/main/sqlite/sqlite-read-failure.test.ts
T
OrcaWinandm4air 8c2cd7d331 feat(ai-vault): read remote OpenCode history with the pinned Node; remove Bun (#24128)
* feat(ai-vault): read OpenCode history with the pinned Node instead of Bun

SSH hosts whose Node lacks node:sqlite (or its backup(), which 22.13-22.15
omit) now get the pinned Node in the shared ~/.orca-remote/runtimes/node-<sha>
store orcad uses: POSIX hosts receive the official archive and extract and
hash-verify it on the host; Windows hosts receive the verified node.exe the
client extracted, promoted by host Node with the same hash check. WSL distros
use the same layout and checks under ~/.cache/orca/runtimes/.

The Bun release pin table and its materializer are deleted. Old relays keep
reading their vault-sqlite/<sha>/bun references; nothing deletes those files.
An unconfirmed runtime upload now keeps its stage instead of removing it.

* refactor(sqlite): drop the Bun SQLite adapter; node:sqlite is the only backend

Nothing outside Electron runs on Bun any more (design D4), so SyncDatabase
loses its Bun branch, and bun-sqlite-database, bun-sqlite-statement and
bun-readonly-wal go, with the relay's bun:sqlite external. The profile-state
backup worker admits Electron or an entry that exists, and startup errors
name the pinned Node. The D7 cross-runtime gate still runs Bun 1.4.2, now
reaching Bun's SQLite through its node:sqlite.

* test(native-chat): drop the Bun SQLite driver case now that node:sqlite is the only backend

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 01:10:31 -07:00

117 lines
4.0 KiB
TypeScript

import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import SyncDatabase from './sync-database'
import { classifySqliteReadFailure, isTransientSqliteContention } from './sqlite-read-failure'
// Error codes here are the ones a real node:sqlite open produces: a contended
// database reports errcode 5 ("database is locked"), while a read-only WAL open
// with no usable -shm reports errcode 14 ("unable to open database file"). The
// two need opposite responses, so the classifier must never conflate them.
const tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function contendedDatabase(): { path: string; release: () => void } {
const dir = mkdtempSync(join(tmpdir(), 'orca-sqlite-failure-'))
tempDirs.push(dir)
const path = join(dir, 'contended.db')
const writer = new SyncDatabase(path)
writer.exec('PRAGMA journal_mode=DELETE')
writer.exec('CREATE TABLE session (id TEXT PRIMARY KEY)')
writer.exec('BEGIN EXCLUSIVE')
writer.exec("INSERT INTO session VALUES ('a')")
return {
path,
release: () => {
writer.exec('COMMIT')
writer.close()
}
}
}
describe('isTransientSqliteContention', () => {
it('recognizes a real SQLITE_BUSY thrown by a read-only open', () => {
const contended = contendedDatabase()
let reader: SyncDatabase | undefined
let thrown: unknown
try {
reader = new SyncDatabase(contended.path, { readonly: true, timeout: 0 })
reader.prepare('SELECT id FROM session').all()
} catch (error) {
thrown = error
} finally {
try {
reader?.close()
} finally {
contended.release()
}
}
expect(thrown).toMatchObject({ errcode: 5 })
expect(isTransientSqliteContention(thrown)).toBe(true)
})
it('recognizes a relayed message with no errcode, as the Codex heal pass sees it', () => {
expect(
isTransientSqliteContention('codex app-server thread/read failed: database is locked')
).toBe(true)
expect(isTransientSqliteContention(new Error('SQLITE_LOCKED: table is locked'))).toBe(true)
})
it('reads extended result codes through their primary code', () => {
// SQLITE_BUSY_SNAPSHOT (517) and SQLITE_BUSY_RECOVERY (261) both pack 5.
expect(isTransientSqliteContention({ errcode: 517, message: 'busy snapshot' })).toBe(true)
expect(isTransientSqliteContention({ errcode: 261, message: 'recovery' })).toBe(true)
})
it('does not treat an unreadable or absent database as contention', () => {
expect(isTransientSqliteContention(new Error('file is not a database'))).toBe(false)
expect(
isTransientSqliteContention({ errcode: 14, message: 'unable to open database file' })
).toBe(false)
})
})
describe('classifySqliteReadFailure', () => {
it('classifies contention as retryable regardless of the file evidence', () => {
const error = { errcode: 5, message: 'database is locked' }
expect(classifySqliteReadFailure({ error, databaseFileExists: true })).toBe('contended')
expect(classifySqliteReadFailure({ error, databaseFileExists: false })).toBe('contended')
})
it('classifies SQLITE_CANTOPEN against a present database as an unreachable wal-index', () => {
expect(
classifySqliteReadFailure({
error: { errcode: 14, message: 'unable to open database file' },
databaseFileExists: true
})
).toBe('wal-index-unavailable')
})
it('does not blame the wal-index when the database file itself is gone', () => {
expect(
classifySqliteReadFailure({
error: { errcode: 14, message: 'unable to open database file' },
databaseFileExists: false
})
).toBe('unreadable')
})
it('falls back to unreadable for anything else', () => {
expect(
classifySqliteReadFailure({
error: { errcode: 11, message: 'database disk image is malformed' },
databaseFileExists: true
})
).toBe('unreadable')
})
})