Files
orca/src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.test.ts
T
Jinwoo Hong b06f40f3ba fix(opencode): read the binder's session store off the main thread; ship the reader worker in orcad (#24638)
* fix(opencode): read the binder's session store on the foreign SQLite reader worker (STA-9122)

Before: the OpenCode session binder listed new sessions from opencode.db with
node:sqlite on the main thread every 60 s (and on SessionStart kicks), so a
large or contended store could stall the app the same way Cursor's did.

After: the read is a pure openCodeBinderSessions reader in
foreign-sqlite-readers/readers/, run only on the worker. The binder's
correlation, pane snapshot and process sweep stay where they were.

- The binder round awaits listSessions and re-checks its generation right
  after, so a stop() during the read discards the round before it touches the
  unbound map or the watermark.
- The client's in-flight dedupe key now includes the cursor, so a stale round
  from before a restart cannot hand its rows to the restarted round.
- Idle teardown is per reader. The binder lane keeps its thread for 120 s,
  longer than its 60 s poll, so the thread is not respawned every round.
- A timeout, crash, malformed reply or unstartable worker resolves to [] (no
  sessions), the value the old read already returned on failure.
- An absent store still reads as [] without a log line, and a permission or
  corrupt-file failure still logs (kept from #24577, now in the reader: it
  stats the path and throws anything but ENOENT/ENOTDIR to the client's log).
- The binder lane inherits #24572's limits from the shared lane: no respawn
  until a timed-out worker has exited, 2 consecutive deaths, a queue cap of
  8. Its timeout stays 60 s, matching its poll.
- dispatch switches on the destructured kind, so a new kind without a case
  still fails to compile.

orcad: the hook server runs there too, so orcad now ships
foreign-sqlite-reader-entry.js beside orcad.js (ORCAD_ARTIFACTS, built as an
orcad child). build-orcad runs a smoke check that starts the built worker
under the build's Node and under the pinned runtime, and does a real binder
read on a fixture DB, a Cursor read of a missing file and an OpenCode history
list. The OpenCode history scanner uses the same entry and was bundled into
orcad without it, so on orcad it always failed closed; it can now run.

Tests: reader (cursor, same-ms ids, OpenCode 2 rows, missing then created,
corrupt, inaccessible directory), retirement gate for the binder lane, dispatch
routing, client lane (rows, failure -> [], dedupe per cursor, own thread, idle
teardown default and override), binder loop with an async listSessions
(failure -> [], stop during the read), orcad path resolution through orcad's
host adapters, artifact list, and the smoke check against good, missing and
non-reading entries.

* test(opencode): cover the binder read deadline with fake timers and name the failure test accurately (STA-9122)
2026-10-02 19:58:29 -04:00

50 lines
1.7 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { handleForeignSqliteReaderRequest } from './foreign-sqlite-reader-dispatch'
describe('handleForeignSqliteReaderRequest', () => {
it('routes cursorProfile to its reader', () => {
expect(
handleForeignSqliteReaderRequest({
id: 4,
kind: 'cursorProfile',
dbPath: '/definitely/missing/state.vscdb'
})
).toEqual({ id: 4, ok: true, value: { status: 'missing' } })
})
it('routes openCodeBinderSessions to its reader and reports a failed open as an error', () => {
const request = { kind: 'openCodeBinderSessions' as const, cursor: { ms: 0, id: '' } }
expect(
handleForeignSqliteReaderRequest({
...request,
id: 5,
dbPath: '/definitely/missing/opencode.db'
})
).toEqual({ id: 5, ok: true, value: [] })
const dir = mkdtempSync(join(tmpdir(), 'orca-foreign-sqlite-dispatch-'))
try {
const dbPath = join(dir, 'opencode.db')
writeFileSync(dbPath, 'not a sqlite database'.repeat(100))
expect(handleForeignSqliteReaderRequest({ ...request, id: 6, dbPath })).toMatchObject({
id: 6,
ok: false
})
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('rejects a kind no reader owns instead of running one', () => {
// Parsed, as a structured clone arrives: untyped, with a kind outside the union.
const request = JSON.parse('{"id":9,"kind":"list","dbPaths":[]}')
expect(handleForeignSqliteReaderRequest(request)).toEqual({
id: 9,
ok: false,
error: 'Unknown foreign SQLite reader kind: list'
})
})
})