Files
orca/src/main/daemon/history-reader-memory.test.ts
T
Jinjing e1f0e7689c fix(history): quarantine unreadable recovery generations. (#10713)
* fix(history): quarantine unreadable recovery files

* fix(history): preserve mixed recovery generations

* fix(history): reanchor reconciled live sessions

* fix(history): serialize final checkpoint queue

* fix(history): drain sleep shutdowns before disconnect

* fix(history): restore legacy wide sessions

* fix(history): preserve malformed mixed logs

* fix(history): preserve malformed log tails

* refactor tests to reduce file size

* refactor(history-recovery): extract freeze helper and improve test robus

- Extract takeRecoveryFreeze to eliminate duplicated freeze-and-clear pattern across five call sites
- Skip permission-mode tests on root CI containers (chmod 0o500 doesn't block root writes)
- Replace fixed sleep with deterministic wait for queued exclusive checkpoints
- Distinguish ENOENT (missing) from corrupt in history metadata reads
- Add ceiling-dimension restore test and torn-tail exclusion assertion
- Wrap chmod operations in try/finally to prevent leaked permissions from masking test failures
- Add .catch() to checkpoint promise to prevent unhandled rejections from finally re-throws

* test(history-recovery): consolidate checkpoint assertions

Wait for both the checkpoint call and set clear atomically to avoid
a timing race where the spy fires before the set is cleared.
2026-07-26 01:13:00 -07:00

192 lines
7.4 KiB
TypeScript

import {
closeSync,
ftruncateSync,
mkdirSync,
mkdtempSync,
openSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { HistoryReader } from './history-reader'
import { getHistorySessionDirName } from './history-paths'
import {
TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES,
TERMINAL_HISTORY_LEGACY_SCROLLBACK_MAX_BYTES,
TERMINAL_HISTORY_LOG_MAX_BYTES,
TERMINAL_HISTORY_META_MAX_BYTES
} from './terminal-history-file-limits'
import { readTerminalHistoryJson } from './terminal-history-file-reader'
const RETIRED_CHECKPOINT_READ_CAP_BYTES = 16 * 1024 * 1024
const directories: string[] = []
function createSession(sessionId: string): { basePath: string; sessionPath: string } {
const basePath = mkdtempSync(join(tmpdir(), 'orca-history-memory-'))
directories.push(basePath)
const sessionPath = join(basePath, getHistorySessionDirName(sessionId))
mkdirSync(sessionPath, { recursive: true })
writeFileSync(
join(sessionPath, 'meta.json'),
JSON.stringify({
cwd: '/workspace',
cols: 80,
rows: 24,
startedAt: '2026-01-01T00:00:00.000Z',
endedAt: null,
exitCode: null
})
)
return { basePath, sessionPath }
}
function createSparseFile(path: string, bytes: number): void {
const descriptor = openSync(path, 'w')
ftruncateSync(descriptor, bytes)
closeSync(descriptor)
}
function checkpoint(overrides?: Record<string, unknown>): string {
return JSON.stringify({
snapshotAnsi: 'safe checkpoint',
scrollbackAnsi: '',
rehydrateSequences: '',
cwd: '/workspace',
cols: 80,
rows: 24,
modes: {
bracketedPaste: false,
mouseTracking: false,
applicationCursor: false,
alternateScreen: false
},
scrollbackLines: 0,
checkpointedAt: '2026-01-01T00:00:00.000Z',
...overrides
})
}
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true })
}
})
describe('terminal history restore memory limits', () => {
it('falls back to a valid checkpoint when the incremental log is oversized', async () => {
const { basePath, sessionPath } = createSession('oversized-log')
writeFileSync(join(sessionPath, 'checkpoint.json'), checkpoint())
createSparseFile(join(sessionPath, 'output.log'), TERMINAL_HISTORY_LOG_MAX_BYTES + 1)
const restore = await new HistoryReader(basePath).detectColdRestore('oversized-log')
expect(restore?.snapshotAnsi).toBe('safe checkpoint')
})
// Why: the read cap once sat at 16MiB while the writer stayed unbounded, so a big-scrollback
// session cold-restored to an empty terminal — checkpoint nulled, and every fallback collapsed.
it('restores a checkpoint larger than the retired 16MiB read cap', async () => {
const { basePath, sessionPath } = createSession('large-checkpoint')
const scrollbackAnsi = 'x'.repeat(RETIRED_CHECKPOINT_READ_CAP_BYTES + 1)
writeFileSync(join(sessionPath, 'checkpoint.json'), checkpoint({ scrollbackAnsi }))
const restore = await new HistoryReader(basePath).detectColdRestore('large-checkpoint')
expect(restore?.scrollbackAnsi).toBe(scrollbackAnsi)
})
// Why the cap survives at all: it bounds a corrupt/runaway file, well above legitimate output.
it('ignores oversized checkpoint and metadata files before parsing', async () => {
const checkpointSession = createSession('oversized-checkpoint')
const oversizedCheckpoint = join(checkpointSession.sessionPath, 'checkpoint.json')
createSparseFile(oversizedCheckpoint, TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES + 1)
// Why assert the reader directly too: detectColdRestore returns null for any unparseable
// checkpoint, so it would stay green with the byte cap removed entirely.
expect(() =>
readTerminalHistoryJson(oversizedCheckpoint, TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES)
).toThrow(/File too large/)
const checkpointReader = new HistoryReader(checkpointSession.basePath)
expect(await checkpointReader.detectColdRestoreState('oversized-checkpoint')).toEqual({
status: 'unreadable',
sessionId: 'oversized-checkpoint'
})
expect(await checkpointReader.detectColdRestore('oversized-checkpoint')).toBeNull()
const metadataSession = createSession('oversized-metadata')
createSparseFile(
join(metadataSession.sessionPath, 'meta.json'),
TERMINAL_HISTORY_META_MAX_BYTES + 1
)
expect(
new HistoryReader(metadataSession.basePath).hasRestorableHistory('oversized-metadata')
).toBe(true)
})
it('reports an oversized legacy scrollback as unreadable recovery', async () => {
const { basePath, sessionPath } = createSession('oversized-legacy-scrollback')
createSparseFile(
join(sessionPath, 'scrollback.bin'),
TERMINAL_HISTORY_LEGACY_SCROLLBACK_MAX_BYTES + 1
)
expect(
await new HistoryReader(basePath).detectColdRestoreState('oversized-legacy-scrollback')
).toEqual({
status: 'unreadable',
sessionId: 'oversized-legacy-scrollback'
})
})
// Why: the retired 1M-structural-token pre-scan was reachable by ordinary content —
// ~100k OSC-8 hyperlinks cross it, the assert threw, detectColdRestore swallowed it,
// and the terminal restored blank. Same user-visible loss as the retired byte cap.
it('restores a checkpoint carrying more OSC links than the retired structural-token cap', async () => {
const { basePath, sessionPath } = createSession('many-osc-links')
const oscLinks = Array.from({ length: 150_000 }, (_, index) => ({
row: index,
startCol: 0,
endCol: 40,
uri: `https://example.com/build/${index}`
}))
writeFileSync(join(sessionPath, 'checkpoint.json'), checkpoint({ oscLinks }))
const restore = await new HistoryReader(basePath).detectColdRestore('many-osc-links')
expect(restore?.snapshotAnsi).toBe('safe checkpoint')
expect(restore?.oscLinks).toHaveLength(oscLinks.length)
})
// Why assert the reader directly too: detectColdRestore hides any reader throw as a
// null checkpoint, so a reinstated pre-scan would stay invisible above.
it('parses link-dense checkpoints without a structural pre-scan budget', () => {
const { sessionPath } = createSession('link-dense-checkpoint')
const checkpointPath = join(sessionPath, 'checkpoint.json')
writeFileSync(checkpointPath, `{"snapshotAnsi":"","values":[${'0,'.repeat(2_000_000)}0]}`)
expect(
readTerminalHistoryJson<{ values: number[] }>(
checkpointPath,
TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES
).values
).toHaveLength(2_000_001)
})
// Why: the pre-scan also carried a 128-level nesting cap, and dropping it is only safe
// because V8 parses JSON iteratively — depth costs heap, not stack. A future engine that
// recursed would abort the daemon rather than throw into the callers' catch, so pin it.
it('parses deeply nested checkpoints without the retired nesting-depth cap', () => {
const { sessionPath } = createSession('deeply-nested-checkpoint')
const checkpointPath = join(sessionPath, 'checkpoint.json')
const depth = 50_000
writeFileSync(
checkpointPath,
`{"snapshotAnsi":"","nested":${'['.repeat(depth)}${']'.repeat(depth)}}`
)
expect(
readTerminalHistoryJson<{ nested: unknown[] }>(
checkpointPath,
TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES
).nested
).toHaveLength(1)
})
})