mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
* fix(codex): index a new account home before bridging history into it Codex indexes every rollout present when it first creates its state DB, and the TUI gives up after 30s, so large histories broke a new account's launch. Fixes #20669 * test(codex): keep the account migration test from starting the real Codex binary Selecting an account starts the history bridge, which spawned codex app-server on the fixture homes and raced the test's temp-dir cleanup. * fix(codex): index a new account's most recent bridged history first Indexing a large history takes minutes, and Codex's /resume hides unindexed threads once a directory has any indexed one, so recent conversations stayed missing until the heal reached them in directory-listing order. * test(codex): cover the history bridge's quit, no-history and failed-link guards Drop the stop check before creating the state DB: it ran in the same tick as the check at bridge start, so it could never observe a quit. * test(codex): make the account heal tests fail closed instead of reaching a real codex Fake invocations now point at a nonexistent binary and a throwaway CODEX_HOME, and cover per-home failure memory and a session that fails during quit. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import SyncDatabase from '../sqlite/sync-database'
|
|
import {
|
|
findNewestCodexStateDbPath,
|
|
isCodexStateDbBackfillPending,
|
|
readCodexStateDbBackfillStatus,
|
|
readIndexedCodexThreadIds
|
|
} from './codex-state-db'
|
|
|
|
const temporaryHomes: string[] = []
|
|
|
|
async function createHome(): Promise<string> {
|
|
const home = await mkdtemp(join(tmpdir(), 'orca-codex-state-db-'))
|
|
temporaryHomes.push(home)
|
|
return home
|
|
}
|
|
|
|
function createBackfillDb(home: string, version: number, status: string): string {
|
|
const path = join(home, `state_${version}.sqlite`)
|
|
const db = new SyncDatabase(path)
|
|
db.exec(
|
|
'CREATE TABLE backfill_state (id INTEGER PRIMARY KEY, status TEXT NOT NULL); ' +
|
|
`INSERT INTO backfill_state (id, status) VALUES (1, '${status}')`
|
|
)
|
|
db.close()
|
|
return path
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryHomes.splice(0).map((home) => rm(home, { recursive: true, force: true }))
|
|
)
|
|
})
|
|
|
|
describe('Codex state DB backfill status', () => {
|
|
it('reads the newest schema DB without mutating an incomplete row', async () => {
|
|
const home = await createHome()
|
|
createBackfillDb(home, 4, 'complete')
|
|
const newest = createBackfillDb(home, 5, 'running')
|
|
|
|
expect(findNewestCodexStateDbPath(home)).toBe(newest)
|
|
expect(readCodexStateDbBackfillStatus(home)).toEqual({
|
|
kind: 'incomplete',
|
|
stateDbPath: newest,
|
|
status: 'running'
|
|
})
|
|
|
|
const db = new SyncDatabase(newest, { readonly: true, fileMustExist: true })
|
|
expect(db.prepare('SELECT status FROM backfill_state WHERE id = 1').get()).toEqual({
|
|
status: 'running'
|
|
})
|
|
db.close()
|
|
})
|
|
|
|
it('treats a large unindexed rollout history as pending', async () => {
|
|
const home = await createHome()
|
|
const sessions = join(home, 'sessions', '2026', '08', '04')
|
|
await mkdir(sessions, { recursive: true })
|
|
await Promise.all(
|
|
Array.from({ length: 100 }, (_, index) =>
|
|
writeFile(join(sessions, `rollout-${index}.jsonl`), '{}\n')
|
|
)
|
|
)
|
|
|
|
expect(isCodexStateDbBackfillPending(home)).toBe(true)
|
|
})
|
|
|
|
it('counts compressed rollouts, which Codex also backfills', async () => {
|
|
const home = await createHome()
|
|
const sessions = join(home, 'sessions', '2026', '08', '04')
|
|
await mkdir(sessions, { recursive: true })
|
|
await Promise.all(
|
|
Array.from({ length: 100 }, (_, index) =>
|
|
writeFile(join(sessions, `rollout-${index}.jsonl.zst`), '')
|
|
)
|
|
)
|
|
|
|
expect(isCodexStateDbBackfillPending(home)).toBe(true)
|
|
})
|
|
|
|
it('does not call a complete backfill pending', async () => {
|
|
const home = await createHome()
|
|
createBackfillDb(home, 5, 'complete')
|
|
|
|
expect(isCodexStateDbBackfillPending(home)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('readIndexedCodexThreadIds', () => {
|
|
it('returns lower-cased thread ids from the newest state DB', async () => {
|
|
const home = await createHome()
|
|
const path = createBackfillDb(home, 5, 'complete')
|
|
const db = new SyncDatabase(path)
|
|
db.exec(
|
|
"CREATE TABLE threads (id TEXT PRIMARY KEY); INSERT INTO threads (id) VALUES ('ABC'), ('def')"
|
|
)
|
|
db.close()
|
|
|
|
expect(readIndexedCodexThreadIds(home)).toEqual(new Set(['abc', 'def']))
|
|
})
|
|
|
|
it('returns null when there is no state DB or no threads table', async () => {
|
|
const home = await createHome()
|
|
expect(readIndexedCodexThreadIds(home)).toBeNull()
|
|
|
|
createBackfillDb(home, 5, 'complete')
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
expect(readIndexedCodexThreadIds(home)).toBeNull()
|
|
warn.mockRestore()
|
|
})
|
|
})
|