mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(remote): harden parking review edge cases
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -33,63 +33,66 @@ async function main(): Promise<void> {
|
||||
}
|
||||
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<MemorySnapshot> {
|
||||
const connection = new RemoteRuntimeSharedControlConnection(pairing, { environmentId })
|
||||
const response = await connection.request<MemorySnapshot>('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<MemorySnapshot>(
|
||||
'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): {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<RuntimeDesktopPairingOffer> {
|
||||
const child = app.process()
|
||||
const stdout = child.stdout
|
||||
@@ -53,12 +96,15 @@ async function readPairingOffer(app: ElectronApplication): Promise<RuntimeDeskto
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
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<RuntimeDeskto
|
||||
}
|
||||
const onClose = (code: number | null, signal: NodeJS.Signals | null): void => {
|
||||
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<RuntimeDeskto
|
||||
)
|
||||
}
|
||||
const onStderr = (chunk: Buffer): void => {
|
||||
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() ?? ''
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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]!
|
||||
|
||||
Reference in New Issue
Block a user