diff --git a/src/main/shell-ready-marker-scanner.ts b/src/main/shell-ready-marker-scanner.ts index 0e6250e1c4e..a8b58499852 100644 --- a/src/main/shell-ready-marker-scanner.ts +++ b/src/main/shell-ready-marker-scanner.ts @@ -11,6 +11,10 @@ export type ShellReadyScanResult = { postMarkerBytesObserved: boolean } +type ShellReadyBoundaryScanResult = ShellReadyScanResult & { + postMarkerOutputIndex: number | null +} + export function createShellReadyScanState(): ShellReadyScanState { return { matchPos: 0, heldBytes: '' } } @@ -22,7 +26,10 @@ export function drainShellReadyHeldBytes(state: ShellReadyScanState): string { return heldBytes } -export function scanForShellReady(state: ShellReadyScanState, data: string): ShellReadyScanResult { +export function scanForShellReadyBoundary( + state: ShellReadyScanState, + data: string +): ShellReadyBoundaryScanResult { let output = '' for (let i = 0; i < data.length; i += 1) { @@ -49,7 +56,8 @@ export function scanForShellReady(state: ShellReadyScanState, data: string): She return { output: output + remaining, matched: true, - postMarkerBytesObserved: remaining.length > 0 + postMarkerBytesObserved: remaining.length > 0, + postMarkerOutputIndex: output.length } } else { output += state.heldBytes @@ -64,5 +72,15 @@ export function scanForShellReady(state: ShellReadyScanState, data: string): She } } - return { output, matched: false, postMarkerBytesObserved: false } + return { + output, + matched: false, + postMarkerBytesObserved: false, + postMarkerOutputIndex: null + } +} + +export function scanForShellReady(state: ShellReadyScanState, data: string): ShellReadyScanResult { + const { postMarkerOutputIndex: _, ...result } = scanForShellReadyBoundary(state, data) + return result } diff --git a/src/main/shell-startup-output-scanner.test.ts b/src/main/shell-startup-output-scanner.test.ts index b4ace12a456..80dbfe4f7bf 100644 --- a/src/main/shell-startup-output-scanner.test.ts +++ b/src/main/shell-startup-output-scanner.test.ts @@ -6,6 +6,42 @@ import { } from './shell-startup-output-scanner' const READY_MARKER = '\x1b]777;orca-shell-ready\x07' +const IDENTITY_MARKER = '\x1b]777;orca-shell-start:12345\x07' + +function scanChunks(chunks: string[]): { + output: string + shellPid: number | null + ready: boolean + postMarkerBytesObserved: boolean +} { + const state = createShellStartupOutputScanState() + let output = '' + let shellPid: number | null = null + let ready = false + let postMarkerBytesObserved = false + + for (const chunk of chunks) { + const scanned = scanShellStartupOutput(state, chunk) + output += scanned.output + shellPid ??= scanned.shellPid + ready ||= scanned.ready + postMarkerBytesObserved ||= scanned.postMarkerBytesObserved + } + return { output, shellPid, ready, postMarkerBytesObserved } +} + +function expectEverySplitToProduce( + input: string, + expected: Omit, 'postMarkerBytesObserved'> +): void { + for (let split = 0; split <= input.length; split += 1) { + const { postMarkerBytesObserved: _, ...scanned } = scanChunks([ + input.slice(0, split), + input.slice(split) + ]) + expect(scanned, `split ${split}`).toEqual(expected) + } +} describe('shell startup output scanner', () => { it.each([ @@ -22,12 +58,30 @@ describe('shell startup output scanner', () => { expect(output).toBe('\x1b[?2004hfish> ') }) + it('preserves every post-ready byte at every chunk boundary', () => { + expectEverySplitToProduce(`${READY_MARKER}${IDENTITY_MARKER}prompt`, { + output: `${IDENTITY_MARKER}prompt`, + shellPid: null, + ready: true + }) + expectEverySplitToProduce(`${READY_MARKER}\x1b[?2004hfish> `, { + output: '\x1b[?2004hfish> ', + shellPid: null, + ready: true + }) + }) + + it('strips pre-ready markers at every chunk boundary', () => { + expectEverySplitToProduce(`${IDENTITY_MARKER}${READY_MARKER}prompt`, { + output: 'prompt', + shellPid: 12345, + ready: true + }) + }) + it('strips identity and readiness markers from one chunk', () => { const state = createShellStartupOutputScanState() - const scanned = scanShellStartupOutput( - state, - `\x1b]777;orca-shell-start:12345\x07${READY_MARKER}prompt` - ) + const scanned = scanShellStartupOutput(state, `${IDENTITY_MARKER}${READY_MARKER}prompt`) expect(scanned).toEqual({ output: 'prompt', diff --git a/src/main/shell-startup-output-scanner.ts b/src/main/shell-startup-output-scanner.ts index 99606f6d047..a0b086865ba 100644 --- a/src/main/shell-startup-output-scanner.ts +++ b/src/main/shell-startup-output-scanner.ts @@ -1,7 +1,7 @@ import { createShellReadyScanState, drainShellReadyHeldBytes, - scanForShellReady, + scanForShellReadyBoundary, type ShellReadyScanState } from './shell-ready-marker-scanner' import { @@ -34,30 +34,30 @@ export function scanShellStartupOutput( state: ShellStartupOutputScanState, data: string ): ShellStartupOutputScanResult { + const readiness = scanForShellReadyBoundary(state.ready, data) + const postMarkerOutputIndex = readiness.postMarkerOutputIndex ?? readiness.output.length + const identityInput = readiness.output.slice(0, postMarkerOutputIndex) let shellPid: number | null = null + let output = identityInput if (state.identity) { - const identity = scanForShellStartupIdentity(state.identity, data) - data = identity.output + const identity = scanForShellStartupIdentity(state.identity, identityInput) + output = identity.output shellPid = identity.shellPid if (shellPid) { state.identity = null } } - const readiness = scanForShellReady(state.ready, data) - let output = readiness.output - let postMarkerBytesObserved = readiness.postMarkerBytesObserved if (readiness.matched && state.identity) { - const heldOutput = drainShellStartupIdentityHeldBytes(state.identity) - output += heldOutput - postMarkerBytesObserved ||= heldOutput.length > 0 + output += drainShellStartupIdentityHeldBytes(state.identity) state.identity = null } + output += readiness.output.slice(postMarkerOutputIndex) return { output, shellPid, ready: readiness.matched, - postMarkerBytesObserved + postMarkerBytesObserved: readiness.postMarkerBytesObserved } } @@ -65,7 +65,7 @@ export function drainShellStartupOutputScanState(state: ShellStartupOutputScanSt let output = state.identity ? drainShellStartupIdentityHeldBytes(state.identity) : '' state.identity = null if (output) { - output = scanForShellReady(state.ready, output).output + output = scanForShellReadyBoundary(state.ready, output).output } return output + drainShellReadyHeldBytes(state.ready) }