Files
orca/src/main/sqlite/sync-database-concurrent-backup.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

85 lines
3.4 KiB
TypeScript

import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Worker } from 'node:worker_threads'
import { expect, it } from 'vitest'
import SyncDatabase from './sync-database'
const WRITER_SOURCE = `
const { parentPort, workerData } = require('node:worker_threads')
const Database = require('node:sqlite').DatabaseSync
const db = new Database(workerData.path)
db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL')
const count = new Int32Array(workerData.count)
let revision = 0
function commit() {
revision += 1
db.exec('BEGIN IMMEDIATE; UPDATE marker SET revision=' + revision + '; COMMIT')
Atomics.store(count, 0, revision)
if (revision === 1) {
parentPort.once('message', commit)
parentPort.postMessage('writing')
return
}
if (revision < 40) setTimeout(commit, 2)
else { db.close(true); parentPort.close() }
}
commit()
`
it('backs up one complete revision while another thread commits to the WAL', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-sqlite-concurrent-backup-'))
const path = join(directory, 'source.db')
const target = join(directory, 'snapshot.db')
const source = new SyncDatabase(path)
let writer: Worker | undefined
let snapshot: SyncDatabase | undefined
try {
source.exec(`
PRAGMA journal_mode=WAL;
PRAGMA synchronous=FULL;
CREATE TABLE marker(name TEXT PRIMARY KEY, revision INTEGER NOT NULL);
INSERT INTO marker VALUES('first',0),('second',0);
CREATE TABLE payload(id INTEGER PRIMARY KEY, value BLOB NOT NULL);
WITH RECURSIVE rows(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM rows WHERE id<1024)
INSERT INTO payload SELECT id, zeroblob(65536) FROM rows;
`)
const sharedCount = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
const count = new Int32Array(sharedCount)
writer = new Worker(WRITER_SOURCE, { eval: true, workerData: { path, count: sharedCount } })
const firstCommit = new Promise<void>((resolve, reject) => {
writer?.once('message', () => resolve())
writer?.once('error', reject)
})
const writerExit = new Promise<void>((resolve, reject) => {
writer?.once('error', reject)
writer?.once('exit', (code) =>
code === 0 ? resolve() : reject(new Error(`Writer exited ${code}`))
)
})
// Attach failure handling before awaiting either event.
void writerExit.catch(() => {})
await firstCommit
const before = Atomics.load(count, 0)
// Keep the writer alive until this thread is ready to start the backup.
writer.postMessage('continue')
await source.backup(target)
const after = Atomics.load(count, 0)
expect(after).toBeGreaterThan(before)
await writerExit
snapshot = new SyncDatabase(target, { readonly: true, fileMustExist: true })
const rows = snapshot.prepare('SELECT revision FROM marker ORDER BY name').all()
expect(rows).toHaveLength(2)
expect(rows[0]).toEqual(rows[1])
expect(rows[0]?.revision).toBeGreaterThanOrEqual(before)
expect(rows[0]?.revision).toBeLessThanOrEqual(Atomics.load(count, 0))
expect(snapshot.prepare('SELECT count(*) AS count FROM payload').get()).toEqual({ count: 1024 })
expect(snapshot.pragma('integrity_check', { simple: true })).toBe('ok')
} finally {
await writer?.terminate()
snapshot?.close()
source.close()
rmSync(directory, { recursive: true, force: true })
}
}, 20_000)