From 1f078cb9e65b3e7e934e85f39015229d0fca8a3a Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:46:56 -0700 Subject: [PATCH] fix(remote): harden parking review edge cases --- config/reliability-gates.jsonc | 15 +- .../remote-shared-control-retirement-probe.ts | 134 ++++++++++-------- .../FloatingTerminalPanel.test.tsx | 34 ++++- .../FloatingTerminalPanel.tsx | 11 +- ...shared-control-retired-request-ids.test.ts | 29 ++++ ...time-shared-control-retired-request-ids.ts | 5 +- ...d-remote-terminal-retention-memory.spec.ts | 4 +- .../helpers/headless-paired-runtime-host.ts | 75 ++++++++-- .../headless-paired-runtime-host.unit.test.ts | 45 ++++++ .../paired-terminal-cold-activation-oracle.ts | 29 ++-- 10 files changed, 274 insertions(+), 107 deletions(-) create mode 100644 tests/e2e/helpers/headless-paired-runtime-host.unit.test.ts diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index ca094dce75d..8366fcc8fdf 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3105,7 +3105,7 @@ "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-probe-gap-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-stall-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", - "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", + "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-parked-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1" ], "testFiles": [ @@ -3167,7 +3167,10 @@ }, { "file": "tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts", - "assertions": ["ordinary-parks paired terminals against an isolated headless Orca host"] + "assertions": [ + "ordinary-parks paired terminals against an isolated headless Orca host", + "cold-activates only visible paired terminals against an isolated headless host" + ] }, { "file": "tests/e2e/terminal-parked-memory.spec.ts", @@ -3310,13 +3313,13 @@ "summary": "An isolated `orca serve` host and paired web renderer exhausted one terminal stream, recovered it, and preserved the original PTY and tab without exposing readiness or pairing material." }, { - "date": "2026-07-29", + "date": "2026-07-30", "runner": "local", "platform": "macos", - "command": "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1", + "command": "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "result": "passed", - "durationSeconds": 14.2, - "summary": "The isolated headless `orca serve` host passed the byte-identical six-PTY ordinary-parking, memory/lag, deep-history, same-PTY, parked-output, and post-reveal I/O oracle." + "durationSeconds": 23, + "summary": "The isolated headless `orca serve` host passed ordinary parking plus cold activation: deferred tabs stayed unmounted until activation, preserved PTY identity and parked output, and resumed live input." }, { "date": "2026-07-29", diff --git a/config/scripts/remote-shared-control-retirement-probe.ts b/config/scripts/remote-shared-control-retirement-probe.ts index d3da348cc6f..63abd01e376 100644 --- a/config/scripts/remote-shared-control-retirement-probe.ts +++ b/config/scripts/remote-shared-control-retirement-probe.ts @@ -33,63 +33,66 @@ async function main(): Promise { } originalWarn(message, details) } - const startedAt = Date.now() - const before = await requestMemorySnapshot(pairing, environment.id) - let ok = 0 - let subscriptionResponses = 0 - let runtimeStatus: RuntimeStatus | null = null - const cleanupDurationsMs: number[] = [] - for (let cycle = 0; cycle < cycles; cycle += 1) { - const result = await runCycle({ - pairing, - environmentId: environment.id, - concurrency, - settleMs, - cleanupTimeoutMs - }) - ok += result.ok - subscriptionResponses += result.subscriptionResponses - runtimeStatus ??= result.runtimeStatus - cleanupDurationsMs.push(result.cleanupDurationMs) - } - await wait(settleMs) - const after = await requestMemorySnapshot(pairing, environment.id) - console.warn = originalWarn - console.log( - JSON.stringify({ - environment: { id: environment.id, name: environment.name }, - runtime: runtimeStatus - ? { - appVersion: runtimeStatus.appVersion ?? null, - capabilities: runtimeStatus.capabilities ?? [], - hostPlatform: runtimeStatus.hostPlatform ?? null - } - : null, - cycles, - concurrency, - requests: cycles * concurrency, - ok, - subscriptionResponses, - cleanupDurationMs: { - average: Math.round( - cleanupDurationsMs.reduce((total, duration) => total + duration, 0) / - cleanupDurationsMs.length + try { + const startedAt = Date.now() + const before = await requestMemorySnapshot(pairing, environment.id) + let ok = 0 + let subscriptionResponses = 0 + let runtimeStatus: RuntimeStatus | null = null + const cleanupDurationsMs: number[] = [] + for (let cycle = 0; cycle < cycles; cycle += 1) { + const result = await runCycle({ + pairing, + environmentId: environment.id, + concurrency, + settleMs, + cleanupTimeoutMs + }) + ok += result.ok + subscriptionResponses += result.subscriptionResponses + runtimeStatus ??= result.runtimeStatus + cleanupDurationsMs.push(result.cleanupDurationMs) + } + await wait(settleMs) + const after = await requestMemorySnapshot(pairing, environment.id) + console.log( + JSON.stringify({ + environment: { id: environment.id, name: environment.name }, + runtime: runtimeStatus + ? { + appVersion: runtimeStatus.appVersion ?? null, + capabilities: runtimeStatus.capabilities ?? [], + hostPlatform: runtimeStatus.hostPlatform ?? null + } + : null, + cycles, + concurrency, + requests: cycles * concurrency, + ok, + subscriptionResponses, + cleanupDurationMs: { + average: Math.round( + cleanupDurationsMs.reduce((total, duration) => total + duration, 0) / + cleanupDurationsMs.length + ), + maximum: Math.max(...cleanupDurationsMs) + }, + unknownResponseFrames: Array.from(unknownResponses.values()).reduce( + (total, count) => total + count, + 0 ), - maximum: Math.max(...cleanupDurationsMs) - }, - unknownResponseFrames: Array.from(unknownResponses.values()).reduce( - (total, count) => total + count, - 0 - ), - unknownResponseIds: unknownResponses.size, - memory: { - before: summarizeMemory(before), - after: summarizeMemory(after), - appDelta: after.app.memory - before.app.memory - }, - elapsedMs: Date.now() - startedAt - }) - ) + unknownResponseIds: unknownResponses.size, + memory: { + before: summarizeMemory(before), + after: summarizeMemory(after), + appDelta: after.app.memory - before.app.memory + }, + elapsedMs: Date.now() - startedAt + }) + ) + } finally { + console.warn = originalWarn + } } async function runCycle(args: { @@ -134,6 +137,8 @@ async function runCycle(args: { subscription.close() } const cleanupDurationMs = await waitForConnectionIdle(connection, args.cleanupTimeoutMs) + // Let cleanup replies reach the retirement cache before closing the socket. + await wait(args.settleMs) return { ok: responses.filter((response) => response.ok).length, subscriptionResponses, @@ -165,12 +170,19 @@ async function requestMemorySnapshot( environmentId: string ): Promise { const connection = new RemoteRuntimeSharedControlConnection(pairing, { environmentId }) - const response = await connection.request('diagnostics.memory', undefined, 20_000) - connection.close() - if (!response.ok) { - throw new Error(`Memory snapshot failed: ${response.error.message}`) + try { + const response = await connection.request( + 'diagnostics.memory', + undefined, + 20_000 + ) + if (!response.ok) { + throw new Error(`Memory snapshot failed: ${response.error.message}`) + } + return response.result + } finally { + connection.close() } - return response.result } function summarizeMemory(snapshot: MemorySnapshot): { diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx index 8146acd818d..d9f846fbbda 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.test.tsx @@ -137,6 +137,7 @@ const mocks = vi.hoisted(() => ({ setTabColor: vi.fn(), setTabCustomTitle: vi.fn(), setTabPaneExpanded: vi.fn(), + shouldDeferParkedPtyExitTabClose: vi.fn(), useContextualTour: vi.fn() })) @@ -213,6 +214,10 @@ vi.mock('@/components/terminal-pane/use-terminal-tab-cold-parking', () => ({ useTerminalTabColdParking: () => parkingBox.parkedTabIds })) +vi.mock('@/components/terminal-pane/terminal-parked-tab-watchers', () => ({ + shouldDeferParkedPtyExitTabClose: mocks.shouldDeferParkedPtyExitTabClose +})) + vi.mock('@/components/terminal-pane/terminal-ime-input-context-refresh', () => ({ isTerminalImeInputContextRefreshing: mocks.isTerminalImeInputContextRefreshing })) @@ -825,6 +830,7 @@ describe('FloatingTerminalPanel close behavior', () => { mocks.isTerminalImeInputContextRefreshing.mockReturnValue(false) mocks.isWebRuntimeSessionActive.mockReturnValue(false) mocks.pickFloatingMarkdownDocument.mockResolvedValue(null) + mocks.shouldDeferParkedPtyExitTabClose.mockReturnValue(false) const localStorage = { clear: vi.fn(), getItem: vi.fn(() => null), @@ -2522,11 +2528,16 @@ describe('FloatingTerminalPanel close behavior', () => { const element = await renderPanel(true, onOpenChange) const terminalPane = findByTypeName(element, 'TerminalPane') - ;(terminalPane.props.onPtyExit as () => void)() - expect(mocks.closeTab).toHaveBeenCalledWith('tab-1', { reason: 'pty-exit' }) + ;(terminalPane.props.onPtyExit as (ptyId: string) => void)('pty-1') + expect(mocks.shouldDeferParkedPtyExitTabClose).toHaveBeenCalledWith('tab-1', 'pty-1') + expect(mocks.closeTerminalTab).toHaveBeenCalledWith('tab-1', { + lifecyclePtyId: 'pty-1', + reason: 'pty-exit' + }) + expect(mocks.closeTab).not.toHaveBeenCalled() expect(onOpenChange).not.toHaveBeenCalled() - mocks.closeTab.mockClear() + mocks.closeTerminalTab.mockClear() ;(terminalPane.props.onCloseTab as () => void)() // Explicit pane close routes through the confirmed-close authority, not a raw pty-exit prune. expect(mocks.closeTerminalTab).toHaveBeenCalledWith( @@ -2537,6 +2548,23 @@ describe('FloatingTerminalPanel close behavior', () => { expect(onOpenChange).not.toHaveBeenCalled() }) + it('preserves split siblings when a parked PTY exits during reveal', async () => { + setFloatingTabs([makeTab({ id: 'tab-1' })]) + mocks.shouldDeferParkedPtyExitTabClose.mockReturnValueOnce(true) + + await renderPanel(true) + runEffects() + await Promise.resolve() + const element = await renderPanel(true) + const terminalPane = findByTypeName(element, 'TerminalPane') + + ;(terminalPane.props.onPtyExit as (ptyId: string) => void)('split-pty') + + expect(mocks.shouldDeferParkedPtyExitTabClose).toHaveBeenCalledWith('tab-1', 'split-pty') + expect(mocks.closeTerminalTab).not.toHaveBeenCalled() + expect(mocks.closeTab).not.toHaveBeenCalled() + }) + it('renders and closes simulator tabs in the floating workspace', async () => { const tab = setFloatingSimulatorTab() diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx index 4c222d83e98..66c83ac063b 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tsx @@ -12,6 +12,7 @@ import { useContextualTour } from '@/components/contextual-tours/use-contextual- import TabBar from '@/components/tab-bar/TabBar' import { resolveGroupTabFromVisibleId } from '@/components/tab-group/tab-group-visible-id' import TerminalPane, { type TerminalPaneHandle } from '@/components/terminal-pane/TerminalPane' +import { shouldDeferParkedPtyExitTabClose } from '@/components/terminal-pane/terminal-parked-tab-watchers' import { useTerminalTabColdParking } from '@/components/terminal-pane/use-terminal-tab-cold-parking' import { isTerminalPaneCloseChord } from '@/components/terminal-pane/terminal-shortcut-policy' import { isTerminalImeInputContextRefreshing } from '@/components/terminal-pane/terminal-ime-input-context-refresh' @@ -1863,7 +1864,15 @@ export function FloatingTerminalPanel({ // atlas to corrupt) while hidden, and the resume on // reopen rebuilds the renderer from scratch. isVisible={isActive && open} - onPtyExit={() => closeTab(tab.id, { reason: 'pty-exit' })} + onPtyExit={(ptyId) => { + if (shouldDeferParkedPtyExitTabClose(tab.id, ptyId)) { + return + } + closeTerminalTab(tab.id, { + reason: 'pty-exit', + lifecyclePtyId: ptyId + }) + }} onCloseTab={() => closeFloatingItemConfirmed(tab.id)} /> diff --git a/src/shared/remote-runtime-shared-control-retired-request-ids.test.ts b/src/shared/remote-runtime-shared-control-retired-request-ids.test.ts index 8d7ba821581..4b3b5a0ef0c 100644 --- a/src/shared/remote-runtime-shared-control-retired-request-ids.test.ts +++ b/src/shared/remote-runtime-shared-control-retired-request-ids.test.ts @@ -30,4 +30,33 @@ describe('SharedControlRetiredRequestIds', () => { expect(ids.has('request-2')).toBe(true) expect(ids.has('request-3')).toBe(true) }) + + it('refreshes an existing id to the newest eviction rank', () => { + const ids = new SharedControlRetiredRequestIds({ maxIds: 2 }) + + ids.retire('request-1') + ids.retire('request-2') + ids.retire('request-1') + ids.retire('request-3') + + expect(ids.has('request-1')).toBe(true) + expect(ids.has('request-2')).toBe(false) + expect(ids.has('request-3')).toBe(true) + }) + + it('expires ids correctly after the clock moves backward', () => { + let now = 1_000 + const ids = new SharedControlRetiredRequestIds({ + ttlMs: 100, + now: () => now + }) + + ids.retire('request-1') + now = 900 + ids.retire('request-2') + now = 1_001 + + expect(ids.has('request-1')).toBe(true) + expect(ids.has('request-2')).toBe(false) + }) }) diff --git a/src/shared/remote-runtime-shared-control-retired-request-ids.ts b/src/shared/remote-runtime-shared-control-retired-request-ids.ts index 1e9c0f78539..37341d361a9 100644 --- a/src/shared/remote-runtime-shared-control-retired-request-ids.ts +++ b/src/shared/remote-runtime-shared-control-retired-request-ids.ts @@ -45,10 +45,9 @@ export class SharedControlRetiredRequestIds { private pruneExpired(now: number): void { for (const [requestId, expiresAt] of this.ids) { - if (expiresAt > now) { - return + if (expiresAt <= now) { + this.ids.delete(requestId) } - this.ids.delete(requestId) } } } diff --git a/tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts b/tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts index 7f9c07dd264..2211328b50c 100644 --- a/tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts +++ b/tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts @@ -4,7 +4,7 @@ import { launchPairedWebClient, type PairedWebClient } from './helpers/paired-el import { runPairedTerminalColdActivationOracle } from './helpers/paired-terminal-cold-activation-oracle' import { runPairedTerminalParkingOracle } from './helpers/paired-terminal-parking-oracle' -test('ordinary-parks paired terminals against an isolated headless Orca host @headful', async ({ +test('ordinary-parks paired terminals against an isolated headless Orca host', async ({ testRepoPath }) => { test.setTimeout(240_000) @@ -40,7 +40,7 @@ test('ordinary-parks paired terminals against an isolated headless Orca host @he } }) -test('cold-activates only visible paired terminals against an isolated headless host @headful', async ({ +test('cold-activates only visible paired terminals against an isolated headless host', async ({ testRepoPath }) => { test.setTimeout(240_000) diff --git a/tests/e2e/helpers/headless-paired-runtime-host.ts b/tests/e2e/helpers/headless-paired-runtime-host.ts index 99d4e42a232..83186e5a7c7 100644 --- a/tests/e2e/helpers/headless-paired-runtime-host.ts +++ b/tests/e2e/helpers/headless-paired-runtime-host.ts @@ -22,6 +22,8 @@ type ServeReady = { } const STARTUP_DIAGNOSTIC_LIMIT = 8_000 +const PAIRING_URL_PATTERN = /orca:\/\/[^\s"\\]+/g +const WEB_CLIENT_PAIRING_PATTERN = /([#&]pairing=)[^&\s"\\]+/g export type HeadlessPairedRuntimeHost = { app: ElectronApplication @@ -30,21 +32,62 @@ export type HeadlessPairedRuntimeHost = { offer: RuntimeDesktopPairingOffer } -function appendStartupDiagnostic(current: string, chunk: Buffer): string { - return `${current}${chunk.toString()}`.slice(-STARTUP_DIAGNOSTIC_LIMIT) +export class HeadlessPairedRuntimeStartupDiagnosticBuffer { + private completed = '' + private discardingOversizedLine = false + private pending = '' + + append(chunk: Buffer): void { + let value = chunk.toString() + if (this.discardingOversizedLine) { + const newlineIndex = value.indexOf('\n') + if (newlineIndex === -1) { + return + } + value = value.slice(newlineIndex + 1) + this.discardingOversizedLine = false + } + + const combined = `${this.pending}${value}` + const newlineIndex = combined.lastIndexOf('\n') + if (newlineIndex !== -1) { + this.completed = `${this.completed}${redactPairingMaterial( + combined.slice(0, newlineIndex + 1) + )}`.slice(-STARTUP_DIAGNOSTIC_LIMIT) + this.pending = combined.slice(newlineIndex + 1) + } else { + this.pending = combined + } + if (this.pending.length > STARTUP_DIAGNOSTIC_LIMIT) { + this.pending = '' + this.discardingOversizedLine = true + } + } + + read(): string { + const pending = this.discardingOversizedLine ? '' : redactPairingMaterial(this.pending) + return `${this.completed}${pending}`.slice(-STARTUP_DIAGNOSTIC_LIMIT) + } } -function formatStartupDiagnostics(stdout: string, stderr: string): string { - const redactPairingCode = (value: string): string => - value.replace(/orca:\/\/[^\s"\\]+/g, 'orca://[redacted]') +export function formatHeadlessPairedRuntimeStartupDiagnostics( + stdout: string, + stderr: string +): string { return [ - stdout ? `stdout:\n${redactPairingCode(stdout)}` : '', - stderr ? `stderr:\n${redactPairingCode(stderr)}` : '' + stdout ? `stdout:\n${redactPairingMaterial(stdout)}` : '', + stderr ? `stderr:\n${redactPairingMaterial(stderr)}` : '' ] .filter(Boolean) .join('\n') } +function redactPairingMaterial(value: string): string { + return value + .replace(PAIRING_URL_PATTERN, 'orca://[redacted]') + .replace(WEB_CLIENT_PAIRING_PATTERN, '$1[redacted]') +} + async function readPairingOffer(app: ElectronApplication): Promise { const child = app.process() const stdout = child.stdout @@ -53,12 +96,15 @@ async function readPairingOffer(app: ElectronApplication): Promise { let buffered = '' - let stdoutDiagnostic = '' - let stderrDiagnostic = '' + const stdoutDiagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() + const stderrDiagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() const stderr = child.stderr const timeout = setTimeout(() => { cleanup() - const diagnostics = formatStartupDiagnostics(stdoutDiagnostic, stderrDiagnostic) + const diagnostics = formatHeadlessPairedRuntimeStartupDiagnostics( + stdoutDiagnostic.read(), + stderrDiagnostic.read() + ) reject( new Error( `Headless runtime did not publish pairing readiness${diagnostics ? `\n${diagnostics}` : ''}` @@ -73,7 +119,10 @@ async function readPairingOffer(app: ElectronApplication): Promise { cleanup() - const diagnostics = formatStartupDiagnostics(stdoutDiagnostic, stderrDiagnostic) + const diagnostics = formatHeadlessPairedRuntimeStartupDiagnostics( + stdoutDiagnostic.read(), + stderrDiagnostic.read() + ) reject( new Error( `Headless runtime exited before pairing readiness (code=${code ?? 'none'}, signal=${signal ?? 'none'})${diagnostics ? `\n${diagnostics}` : ''}` @@ -81,10 +130,10 @@ async function readPairingOffer(app: ElectronApplication): Promise { - stderrDiagnostic = appendStartupDiagnostic(stderrDiagnostic, chunk) + stderrDiagnostic.append(chunk) } const onData = (chunk: Buffer): void => { - stdoutDiagnostic = appendStartupDiagnostic(stdoutDiagnostic, chunk) + stdoutDiagnostic.append(chunk) buffered += chunk.toString() const lines = buffered.split(/\r?\n/) buffered = lines.pop() ?? '' diff --git a/tests/e2e/helpers/headless-paired-runtime-host.unit.test.ts b/tests/e2e/helpers/headless-paired-runtime-host.unit.test.ts new file mode 100644 index 00000000000..69e7e0a668e --- /dev/null +++ b/tests/e2e/helpers/headless-paired-runtime-host.unit.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { + HeadlessPairedRuntimeStartupDiagnosticBuffer, + formatHeadlessPairedRuntimeStartupDiagnostics +} from './headless-paired-runtime-host' + +describe('headless paired runtime startup diagnostics', () => { + it('redacts pairing URLs before truncation can remove their prefix', () => { + const pairingUrl = `orca://${'secret'.repeat(1_500)}` + const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() + + diagnostic.append(Buffer.from(`prefix${pairingUrl}\n`)) + + expect(diagnostic.read()).toBe('prefixorca://[redacted]\n') + expect(diagnostic.read()).not.toContain('secret') + }) + + it('redacts pairing URLs split across chunks', () => { + const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() + diagnostic.append(Buffer.from('orca://p')) + diagnostic.append(Buffer.from('airing-secret\nready')) + + expect(formatHeadlessPairedRuntimeStartupDiagnostics(diagnostic.read(), '')).toBe( + 'stdout:\norca://[redacted]\nready' + ) + }) + + it('redacts encoded pairing material from web-client URLs', () => { + const pairingUrl = encodeURIComponent('orca://pairing-secret') + const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() + + diagnostic.append(Buffer.from(`https://host/web-index.html#pairing=${pairingUrl}\n`)) + + expect(diagnostic.read()).toBe('https://host/web-index.html#pairing=[redacted]\n') + expect(diagnostic.read()).not.toContain('pairing-secret') + }) + + it('drops oversized unfinished lines instead of retaining a pairing fragment', () => { + const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer() + diagnostic.append(Buffer.from(`orca://${'secret'.repeat(1_500)}`)) + diagnostic.append(Buffer.from('still-secret\nsafe')) + + expect(diagnostic.read()).toBe('safe') + }) +}) diff --git a/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts b/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts index 6616a157e07..f11bb6e284a 100644 --- a/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts +++ b/tests/e2e/helpers/paired-terminal-cold-activation-oracle.ts @@ -132,37 +132,30 @@ export async function runPairedTerminalColdActivationOracle( }) } - const originalPtyIds = await expect + let originalPtyIds: string[] | null = null + await expect .poll( - () => - page.evaluate( + async () => { + originalPtyIds = await page.evaluate( ({ tabIds, targetWorktreeId }) => { const tabs = window.__store?.getState().tabsByWorktree[targetWorktreeId] ?? [] const byId = new Map(tabs.map((tab) => [tab.id, tab.ptyId])) const ids = tabIds.map((id) => byId.get(id) ?? null) - return ids.every((id) => typeof id === 'string') ? ids : null + return ids.every((id): id is string => typeof id === 'string') ? ids : null }, { tabIds: pendingTabs.map((tab) => tab.tabId), targetWorktreeId: worktreeId } - ), + ) + return originalPtyIds + }, { timeout: 30_000 } ) .not.toBeNull() - .then(() => - page.evaluate( - ({ tabIds, targetWorktreeId }) => { - const tabs = window.__store?.getState().tabsByWorktree[targetWorktreeId] ?? [] - const byId = new Map(tabs.map((tab) => [tab.id, tab.ptyId])) - return tabIds.map((id) => byId.get(id)!) - }, - { - tabIds: pendingTabs.map((tab) => tab.tabId), - targetWorktreeId: worktreeId - } - ) - ) + if (originalPtyIds === null) { + throw new Error('Paired cold-activation PTY ids were not captured') + } const tabs: ColdTab[] = pendingTabs.map((tab, index) => ({ ...tab, originalPtyId: originalPtyIds[index]!