diff --git a/docs/reference/ci-runner-efficiency.md b/docs/reference/ci-runner-efficiency.md index 19d928f3c82..1efd247e7bb 100644 --- a/docs/reference/ci-runner-efficiency.md +++ b/docs/reference/ci-runner-efficiency.md @@ -1941,6 +1941,49 @@ remained red. Its review recognized all five shards as a complete reference (10,608 files, 8,965,977 worker-ms). This was again a full fallback with `selectionEvaluated: false`, not evidence for enabling selected tests. +## October 4 runtime imports and recovery fixtures + +Three helper-only tests now import the existing terminal modules directly rather +than initializing the runtime service. Ten copied-loop cases never exercised +runtime memoization: they passed with its cache, timestamp update or prune +invalidation disabled. Two actual helper checks remain. The existing runtime +prune suite now exercises real leaf cache reuse, split prompt timestamps, +ordinary output, fresh prompts and detection after retained-history eviction. +Each of those three production faults fails a real runtime assertion. + +Recovery tests now seed three exact fixture variants once, after the seed child +has closed. Each crash still receives an independent byte-for-byte copy of the +entire database/WAL family and remapped paths. Buffer.equals retains exact byte +comparison without recursive matcher overhead. All 46 original crash boundaries +and retries remain. Four additional copy-isolation/WAL checks run, and teardown +requires that all seed bytes remain unchanged after the full suite. + +Three alternating one-worker hosted ARM pairs in +[37182181976](https://github.com/stablyai/orca/actions/runs/37182181976) +measured these complete invocations: + +| Cohort | Baseline seconds | Candidate seconds | Median saving | +| --- | --- | --- | --- | +| Three imports only, same 15 tests | 19.257 / 19.167 / 19.363 | 1.769 / 1.768 / 1.768 | 90.8% | +| Final four-file runtime cohort | 22.312 / 22.122 / 21.969 | 13.494 / 13.793 / 13.601 | 38.5% | +| Recovery crash boundaries | 24.082 / 24.075 / 24.814 | 8.061 / 8.105 / 9.074 | 66.3% | + +The final runtime cohort has seven real cases versus 16 including the copied +loops; its new runtime case is included in candidate timing. Recovery has 50 +passes versus the original 46. Hosted Node typecheck passed. Recovery faults for +last-byte database/WAL corruption, shared database paths, missing WAL copies and +accepted/unaccepted seed collision failed the intended assertions. These are +focused workload savings, not measured whole-shard or queue-delay gains. + +An independent local cache screen left both caches disabled. Across 14 unchanged +files and 92 cases, a warm Vitest transform cache reduced median invocation time +3.090 to 1.948 seconds, excluding archive costs; its cold arm increased time to +3.281 seconds. Node compilation caching showed no gain. Controls reproduced stale +transforms after TypeScript configuration or plugin-option changes, so persisted +reuse requires a complete transform-input stamp and hosted net-cost evidence. +A separate 130,000-pane leaf-collection optimization was restored: its complete +migration-file timing stayed within noise. The regression fixture remains. + ## October 3 removal fixture cleanup ordering [37105566358](https://github.com/stablyai/orca/actions/runs/37105566358) diff --git a/src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts b/src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts index 75e498b8d70..857e4f1c7ef 100644 --- a/src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts +++ b/src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts @@ -1,4 +1,5 @@ import { + cpSync, existsSync, mkdirSync, mkdtempSync, @@ -8,7 +9,7 @@ import { writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { basename, dirname, join } from 'node:path' +import { basename, dirname, join, relative } from 'node:path' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { setSecretStore } from '../../../shared/secret-store' import { profileStateStorage } from '../../orca-profiles/profile-project-state-file' @@ -103,13 +104,25 @@ afterEach(() => { rmSync(root, { recursive: true, force: true }) } }) -afterAll(() => rmSync(suiteRoot, { recursive: true, force: true })) +afterAll(() => { + try { + for (const seed of seededFixtures.values()) { + for (const [suffix, bytes] of seed.originalFamily) { + expect(readFileSync(`${seed.databasePath}${suffix}`).equals(bytes)).toBe(true) + } + expect(readFileSync(seed.backupPath).equals(seed.backupBytes)).toBe(true) + expect(readFileSync(seed.exportPath, 'utf8')).toBe(selectedJson) + } + } finally { + rmSync(suiteRoot, { recursive: true, force: true }) + } +}) type Fixture = RecoveryCrashOptions & { backupBytes: Buffer; originalFamily: Map } +const seededFixtures = new Map() -async function fixture(kind: 'json' | 'sqlite', accepted: boolean): Promise { - const root = mkdtempSync(join(suiteRoot, 'profile-')) - fixtureRoots.push(root) +async function seedFixture(kind: 'json' | 'sqlite', accepted: boolean): Promise { + const root = mkdtempSync(join(suiteRoot, 'seed-')) const directory = join(root, 'profiles', profileId) mkdirSync(directory, { recursive: true }) writeFileSync( @@ -173,6 +186,39 @@ async function fixture(kind: 'json' | 'sqlite', accepted: boolean): Promise [ + suffix, + readFileSync(`${options.databasePath}${suffix}`) + ]) + ) + return { ...options, backupBytes: readFileSync(options.backupPath), originalFamily } +} + +async function fixture(kind: 'json' | 'sqlite', accepted: boolean): Promise { + const key = `${kind}/${accepted}` + let seed = seededFixtures.get(key) + if (!seed) { + // The seed child's close event has fired before its WAL family is copied. + seed = await seedFixture(kind, accepted) + seededFixtures.set(key, seed) + } + return cloneFixture(seed) +} + function readSqlite(path: string): unknown { const opened = openProfileStateDatabaseReadOnly(path, profileId) try { @@ -192,10 +238,12 @@ function assertQuarantine(profile: Fixture): void { const directory = join(dirname(profile.databasePath), quarantine) // Check exact family bytes before opening the copied WAL snapshot. for (const [suffix, bytes] of profile.originalFamily) { - expect(readFileSync(join(directory, `profile-state.db${suffix}`))).toEqual(bytes) + expect(readFileSync(join(directory, `profile-state.db${suffix}`)).equals(bytes)).toBe(true) } expect(readFileSync(join(directory, basename(profile.exportPath)), 'utf8')).toBe(selectedJson) - expect(readFileSync(join(directory, basename(profile.backupPath)))).toEqual(profile.backupBytes) + expect( + readFileSync(join(directory, basename(profile.backupPath))).equals(profile.backupBytes) + ).toBe(true) expect(readSqlite(join(directory, 'profile-state.db'))).toEqual(oldState) } @@ -346,7 +394,7 @@ describe('SQLite recovery process death', () => { const profile = await fixture('sqlite', true) await killRecoveryAt(bundle, profile, stage(profile, boundary)) assertQuarantine(profile) - expect(readFileSync(profile.backupPath)).toEqual(profile.backupBytes) + expect(readFileSync(profile.backupPath).equals(profile.backupBytes)).toBe(true) const expected = [ 'marker-invalidated', 'selected-export', @@ -365,7 +413,7 @@ describe('SQLite recovery process death', () => { : 'refused' assertRestart(profile, expected) retry(profile) - expect(readFileSync(profile.backupPath)).toEqual(profile.backupBytes) + expect(readFileSync(profile.backupPath).equals(profile.backupBytes)).toBe(true) }) it.skipIf(process.platform === 'win32')( @@ -378,3 +426,38 @@ describe('SQLite recovery process death', () => { } ) }) + +describe('seeded recovery fixture copies', () => { + it.each([ + ['json', false], + ['json', true], + ['sqlite', true] + ] as const)( + 'isolates %s/accepted=%s through corruption and WAL reopen', + async (kind, accepted) => { + const first = await fixture(kind, accepted) + const second = await fixture(kind, accepted) + const seed = seededFixtures.get(`${kind}/${accepted}`) + if (!seed) { + throw new Error('Seed fixture was not retained') + } + expect(new Set([first.root, second.root, seed.root]).size).toBe(3) + for (const [suffix, bytes] of seed.originalFamily) { + expect(readFileSync(`${first.databasePath}${suffix}`).equals(bytes)).toBe(true) + writeFileSync(`${first.databasePath}${suffix}`, 'corrupted-copy') + expect(readFileSync(`${second.databasePath}${suffix}`).equals(bytes)).toBe(true) + expect(readFileSync(`${seed.databasePath}${suffix}`).equals(bytes)).toBe(true) + } + const third = await fixture(kind, accepted) + expect(readSqlite(third.databasePath)).toEqual(oldState) + expect(readFileSync(seed.backupPath).equals(seed.backupBytes)).toBe(true) + expect(readFileSync(seed.exportPath, 'utf8')).toBe(selectedJson) + } + ) + + it('rejects an incomplete copied WAL family before a recovery child starts', async () => { + const incomplete = await fixture('json', false) + rmSync(`${incomplete.databasePath}-wal`) + expect(() => cloneFixture(incomplete)).toThrow(/ENOENT/) + }) +}) diff --git a/src/main/runtime/orca-runtime-tail-wait-memo.test.ts b/src/main/runtime/orca-runtime-tail-wait-memo.test.ts index 5ce8d9710cd..4ac28674dd8 100644 --- a/src/main/runtime/orca-runtime-tail-wait-memo.test.ts +++ b/src/main/runtime/orca-runtime-tail-wait-memo.test.ts @@ -1,124 +1,7 @@ import { describe, expect, it, vi } from 'vitest' -import { - appendNormalizedToTailBuffer, - buildPreview, - computeTerminalTailWaitState, - tailGainedNewerBlockedReason, - type TerminalTailWaitState -} from './orca-runtime' +import { computeTerminalTailWaitState } from './terminal-wait-tail-state' -// These tests pin the onPtyData wait-detection memoization: caching the -// post-append wait state and reusing it as the next chunk's pre-append state -// must produce byte-for-byte the same waitBlockedAt stamping as recomputing the -// wait-state scan on both sides of every chunk (the pre-memoization behavior), -// while doing roughly half the state computations. - -type RedrawCursor = ReturnType['redrawCursor'] - -type TailSim = { - tailBuffer: string[] - tailPartialLine: string - tailRedrawCursor: RedrawCursor - preview: string - waitBlockedAt: number | null - tailWaitState?: TerminalTailWaitState -} - -function newSim(): TailSim { - return { - tailBuffer: [], - tailPartialLine: '', - tailRedrawCursor: null, - preview: '', - waitBlockedAt: null - } -} - -type Compute = typeof computeTerminalTailWaitState - -// Mirrors the memoized onPtyData tail loop: reuse the cached tail-derived state -// as the previous state; only recompute it on a preview-fallback (empty tail). -function stepMemoized(sim: TailSim, chunk: string, at: number, compute: Compute): void { - const previousWaitState = - sim.tailWaitState?.fromTail === true - ? sim.tailWaitState - : compute(sim.tailBuffer, sim.tailPartialLine, sim.preview) - const nextTail = appendNormalizedToTailBuffer( - sim.tailBuffer, - sim.tailPartialLine, - chunk, - sim.tailRedrawCursor - ) - const nextWaitState = compute(nextTail.lines, nextTail.partialLine, sim.preview) - if (tailGainedNewerBlockedReason(previousWaitState, nextWaitState, chunk)) { - sim.waitBlockedAt = at - } - sim.tailWaitState = nextWaitState - sim.tailBuffer = nextTail.lines - sim.tailPartialLine = nextTail.partialLine - sim.tailRedrawCursor = nextTail.redrawCursor - sim.preview = buildPreview(nextTail.lines, nextTail.partialLine) -} - -// Reference: the pre-memoization behavior — recompute the previous state fresh -// from the current tail on every chunk (no cache). -function stepReference(sim: TailSim, chunk: string, at: number, compute: Compute): void { - const previousWaitState = compute(sim.tailBuffer, sim.tailPartialLine, sim.preview) - const nextTail = appendNormalizedToTailBuffer( - sim.tailBuffer, - sim.tailPartialLine, - chunk, - sim.tailRedrawCursor - ) - const nextWaitState = compute(nextTail.lines, nextTail.partialLine, sim.preview) - if (tailGainedNewerBlockedReason(previousWaitState, nextWaitState, chunk)) { - sim.waitBlockedAt = at - } - sim.tailBuffer = nextTail.lines - sim.tailPartialLine = nextTail.partialLine - sim.tailRedrawCursor = nextTail.redrawCursor - sim.preview = buildPreview(nextTail.lines, nextTail.partialLine) -} - -function runBoth(chunks: string[]): { memoized: (number | null)[]; reference: (number | null)[] } { - const memoSim = newSim() - const refSim = newSim() - const memoized: (number | null)[] = [] - const reference: (number | null)[] = [] - chunks.forEach((chunk, index) => { - const at = index + 1 - stepMemoized(memoSim, chunk, at, computeTerminalTailWaitState) - stepReference(refSim, chunk, at, computeTerminalTailWaitState) - memoized.push(memoSim.waitBlockedAt) - reference.push(refSim.waitBlockedAt) - }) - return { memoized, reference } -} - -const SCENARIOS: Record = { - 'plain output never blocks': ['building...\n', 'compiled ok\n', 'watching for changes\n'], - 'blocked prompt in one chunk': ['Update available! Press Enter to continue.\n'], - 'blocked prompt split across chunks': ['Update available!\n', 'Press Enter to continue.\n'], - 'blocked then plain output stays blocked': [ - 'Update available! Press Enter to continue.\n', - 'still here\n', - 'more logs\n' - ], - 'partial lines without newline then completion': [ - 'Update ava', - 'ilable! Press Enter ', - 'to continue.\n' - ], - 'empty and whitespace chunks': ['', ' ', '\n', 'ok\n', ''], - 'ready header after stale blocked prompt': [ - 'Update available! Press Enter to continue.\n', - 'OpenAI Codex\n', - 'model: gpt\n', - 'directory: /repo\n' - ] -} - -describe('onPtyData tail wait memoization', () => { +describe('terminal tail wait state', () => { it('computeTerminalTailWaitState reports fromTail and blocked signals', () => { const empty = computeTerminalTailWaitState([], '', '') expect(empty.fromTail).toBe(false) @@ -151,92 +34,4 @@ describe('onPtyData tail wait memoization', () => { lastIndexOf.mockRestore() } }) - - for (const [name, chunks] of Object.entries(SCENARIOS)) { - it(`memoized stamping matches recompute reference: ${name}`, () => { - const { memoized, reference } = runBoth(chunks) - expect(memoized).toEqual(reference) - }) - } - - it('stays equivalent across tail eviction beyond the retained cap', () => { - const chunks: string[] = [] - for (let i = 0; i < 2600; i += 1) { - chunks.push(`line ${i} of streaming build output that keeps the tail busy\n`) - } - // Introduce a real blocked prompt well past the eviction boundary. - chunks.push('Update available! Press Enter to continue.\n') - chunks.push('trailing log after prompt\n') - const { memoized, reference } = runBoth(chunks) - expect(memoized).toEqual(reference) - // The prompt must actually be detected (guards against a vacuous match). - expect(memoized.at(-1)).not.toBeNull() - }) - - it('recomputes correctly after a transcript prune empties the tail (prune-then-resume)', () => { - // pruneDisconnectedPtyTranscript empties the tail AND clears tailWaitState; - // model that here and assert the memoized stamping still tracks a fresh - // recompute across the reset (a stale cache would desync the first resumed - // chunk, since the pre-prune tail held a blocked prompt). - const prune = (sim: TailSim): void => { - sim.tailBuffer = [] - sim.tailPartialLine = '' - sim.tailRedrawCursor = null - sim.preview = '' - sim.waitBlockedAt = null - sim.tailWaitState = undefined - } - const memoSim = newSim() - const refSim = newSim() - const memoOut: (number | null)[] = [] - const refOut: (number | null)[] = [] - let at = 0 - const feed = (chunk: string): void => { - at += 1 - stepMemoized(memoSim, chunk, at, computeTerminalTailWaitState) - stepReference(refSim, chunk, at, computeTerminalTailWaitState) - memoOut.push(memoSim.waitBlockedAt) - refOut.push(refSim.waitBlockedAt) - } - // Pre-prune: leave a stale blocked prompt in the tail. - ;['building\n', 'Update available! Press Enter to continue.\n', 'more log\n'].forEach(feed) - prune(memoSim) - prune(refSim) - // Resume: a fresh blocked prompt must be stamped, not masked by stale cache. - ;['fresh start\n', 'Update available! Press Enter to continue.\n', 'after\n'].forEach(feed) - - expect(memoOut).toEqual(refOut) - expect(memoSim.waitBlockedAt).not.toBeNull() - }) - - it('does roughly half the wait-state computations of the recompute reference', () => { - const chunks: string[] = [] - for (let i = 0; i < 500; i += 1) { - chunks.push(`streaming line ${i}\n`) - } - - let memoCalls = 0 - const countingMemo: Compute = (lines, partial, preview) => { - memoCalls += 1 - return computeTerminalTailWaitState(lines, partial, preview) - } - let refCalls = 0 - const countingRef: Compute = (lines, partial, preview) => { - refCalls += 1 - return computeTerminalTailWaitState(lines, partial, preview) - } - - const memoSim = newSim() - const refSim = newSim() - chunks.forEach((chunk, index) => { - stepMemoized(memoSim, chunk, index + 1, countingMemo) - stepReference(refSim, chunk, index + 1, countingRef) - }) - - // Reference recomputes both sides every chunk: 2 per chunk. - expect(refCalls).toBe(chunks.length * 2) - // Memoized reuses the cached previous state on every non-empty-tail chunk: - // 1 per chunk plus a single first-chunk cold miss. - expect(memoCalls).toBe(chunks.length + 1) - }) }) diff --git a/src/main/runtime/pty-transcript-prune-wait-cache.test.ts b/src/main/runtime/pty-transcript-prune-wait-cache.test.ts index 6c9082d2552..6a99c7b25ee 100644 --- a/src/main/runtime/pty-transcript-prune-wait-cache.test.ts +++ b/src/main/runtime/pty-transcript-prune-wait-cache.test.ts @@ -1,14 +1,9 @@ -/** - * Regression: pruneDisconnectedPtyTranscript empties a disconnected PTY's - * retained tail. The onPtyData wait-scan memoization caches the tail's wait - * state on the record (tailWaitState) and reuses it as the next chunk's - * "previous" state — so the prune MUST also clear that cache, or a record that - * resumes output after adoption/reattach would reuse a stale (pre-prune, - * possibly blocked) wait state and mis-stamp waitBlockedAt on its first chunk. - */ -import { describe, expect, it } from 'vitest' +// A disconnected transcript must not keep a wait-scan cache from before its history was pruned. +import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' -import type { TerminalTailWaitState } from './orca-runtime' +import { MAX_TAIL_LINES } from './terminal-tail-limits' +import * as terminalWaitTailState from './terminal-wait-tail-state' +import type { TerminalTailWaitState } from './terminal-wait-tail-state' type PtyRecord = { connected: boolean @@ -20,9 +15,33 @@ type RuntimeInternals = { pruneDisconnectedPtyTranscript: (pty: PtyRecord) => void } +function onlyRuntimeLeaf(runtime: unknown) { + if (typeof runtime !== 'object' || runtime === null || !('leaves' in runtime)) { + throw new Error('Runtime has no leaves') + } + const leaves = runtime.leaves + if (!(leaves instanceof Map) || leaves.size !== 1) { + throw new Error('Expected exactly one runtime leaf') + } + const leaf: unknown = leaves.values().next().value + if ( + typeof leaf !== 'object' || + leaf === null || + !('tailBuffer' in leaf) || + !Array.isArray(leaf.tailBuffer) || + !('tailLinesTotal' in leaf) || + typeof leaf.tailLinesTotal !== 'number' || + !('waitBlockedAt' in leaf) + ) { + throw new Error('Runtime leaf has no terminal tail state') + } + return leaf +} + describe('pruneDisconnectedPtyTranscript clears the wait-scan cache', () => { it('empties the tail and drops tailWaitState so resume recomputes', () => { const runtime = new OrcaRuntimeService() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: both protected methods are declared on OrcaRuntimeService's inheritance chain. const internals = runtime as unknown as RuntimeInternals const pty = internals.recordPtyWorktree('pty-1', 'wt-1', { connected: true }) @@ -40,4 +59,65 @@ describe('pruneDisconnectedPtyTranscript clears the wait-scan cache', () => { expect(pty.tailBuffer).toEqual([]) expect(pty.tailWaitState).toBeUndefined() }) + + it('reuses the real leaf wait scan and stamps each newly arriving blocked prompt', async () => { + const runtime = new OrcaRuntimeService() + const ptyId = 'pty-memo' + const leafId = '11111111-1111-4111-8111-111111111111' + runtime.attachWindow(1) + runtime.syncWindowGraph(1, { + tabs: [{ tabId: 'tab-1', worktreeId: 'wt-1', title: '', activeLeafId: leafId, layout: null }], + leaves: [ + { tabId: 'tab-1', worktreeId: 'wt-1', leafId, paneRuntimeId: 1, ptyId, paneTitle: null } + ] + }) + const leaf = onlyRuntimeLeaf(runtime) + // Different retained history exercises the leaf scan rather than the shared PTY-tail path. + leaf.tailBuffer = ['leaf-only history'] + leaf.tailLinesTotal = 1 + const scan = vi.spyOn(terminalWaitTailState, 'computeTerminalTailWaitState') + const leafScans = () => + scan.mock.calls.filter(([lines]) => lines.includes('leaf-only history')).length + try { + runtime.onPtyData(ptyId, 'first plain line\n', 1_000) + expect(leafScans()).toBe(2) + expect('tailWaitState' in leaf ? leaf.tailWaitState : undefined).toMatchObject({ + fromTail: true, + signal: null + }) + runtime.onPtyData(ptyId, 'second plain line\n', 2_000) + expect(leafScans()).toBe(3) + expect(leaf.waitBlockedAt).toBeNull() + runtime.onPtyData(ptyId, 'Update ava', 3_000) + expect(leafScans()).toBe(4) + expect(leaf.waitBlockedAt).toBeNull() + runtime.onPtyData(ptyId, 'ilable! Press Enter to continue.\n', 4_000) + expect(leafScans()).toBe(5) + expect(leaf.waitBlockedAt).toBe(4_000) + runtime.onPtyData(ptyId, 'ordinary log after the prompt\n', 5_000) + expect(leafScans()).toBe(6) + expect(leaf.waitBlockedAt).toBe(4_000) + runtime.onPtyData(ptyId, 'Update available! Press Enter to continue.\n', 6_000) + expect(leafScans()).toBe(7) + expect(leaf.waitBlockedAt).toBe(6_000) + runtime.onPtyData( + ptyId, + Array.from({ length: MAX_TAIL_LINES + 1 }, (_, index) => `streaming line ${index}\n`).join( + '' + ), + 7_000 + ) + expect(leaf.tailBuffer).toHaveLength(MAX_TAIL_LINES) + expect(leaf.tailBuffer).not.toContain('leaf-only history') + expect('tailWaitState' in leaf ? leaf.tailWaitState : undefined).toMatchObject({ + fromTail: true, + signal: null + }) + runtime.onPtyData(ptyId, 'Update available! Press Enter to continue.\n', 8_000) + expect(leaf.waitBlockedAt).toBe(8_000) + } finally { + scan.mockRestore() + await runtime.onPtyExit(ptyId, 0) + } + }) }) diff --git a/src/main/runtime/retained-tail-redraw-window.equivalence.test.ts b/src/main/runtime/retained-tail-redraw-window.equivalence.test.ts index 30b36483675..b11ce57dcfb 100644 --- a/src/main/runtime/retained-tail-redraw-window.equivalence.test.ts +++ b/src/main/runtime/retained-tail-redraw-window.equivalence.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - appendNormalizedToTailBuffer, - appendNormalizedToMultilineTailBufferUnwindowed -} from './orca-runtime' +import { appendNormalizedToTailBuffer } from './terminal-tail-buffer' +import { appendNormalizedToMultilineTailBufferUnwindowed } from './terminal-tail-redraw-buffer' // Differential guard for the windowed redraw tail path: the public // appendNormalizedToTailBuffer routes vertical-control chunks through a diff --git a/src/main/runtime/terminal-projection.test.ts b/src/main/runtime/terminal-projection.test.ts index 7dff087e74c..c1634ddb19c 100644 --- a/src/main/runtime/terminal-projection.test.ts +++ b/src/main/runtime/terminal-projection.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import type { TerminalCursorContext } from '../../shared/terminal-composer-draft' import type { HeadlessEmulator } from '../daemon/headless-emulator' -import { projectTerminalTailLines } from './orca-runtime' +import { projectTerminalTailLines } from './orca-runtime-terminal-projection' describe('projectTerminalTailLines', () => { it('does not splice a scrolled viewport into unrelated tail rows', () => {