mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(terminal): refuse unaddressable split cleanup
This commit is contained in:
@@ -3462,7 +3462,8 @@
|
||||
"file": "src/main/runtime/orca-runtime-terminal-split-authority.test.ts",
|
||||
"assertions": [
|
||||
"rejected split cleanup passes the spawned incarnation through exact stop and final retirement",
|
||||
"a same-id replacement installed behind the stop barrier remains current and never reaches an ID-only kill"
|
||||
"a same-id replacement installed behind the stop barrier remains current and never reaches an ID-only kill",
|
||||
"a legacy SSH owner is refused before split spawn, and missing spawn proof never reaches ID-only cleanup"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3694,8 +3695,8 @@
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime-terminal-close-incarnation-race.test.ts src/main/runtime/orca-runtime-terminal-close-continuity.test.ts src/main/runtime/orca-runtime-terminal-split-authority.test.ts src/main/ipc/pty-daemon-controller-teardown.test.ts src/main/ipc/pty-ssh-stop-verdict.test.ts src/main/providers/local-pty-provider-spawn-session.test.ts src/main/providers/local-pty-provider-shutdown.test.ts src/main/providers/ssh-pty-provider.test.ts src/main/daemon/daemon-pty-adapter-session-adoption.test.ts src/main/daemon/daemon-pty-router-incarnation-shutdown.test.ts src/main/daemon/daemon-protocol-version.test.ts src/main/daemon/daemon-foreground-confirmation-protocol.test.ts src/relay/pty-handler-shutdown-signals.test.ts src/main/daemon/terminal-host.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 4.87,
|
||||
"summary": "Fourteen files and 230 tests passed on the final candidate, including controlled headless and rejected-split same-ID barriers, failed split-leaf retention, the runtime close transaction, local same-id admission, controller cleanup fencing, daemon protocol skew and owner routing, host duplicate teardown, SSH method skew, and relay process-owner rejection."
|
||||
"durationSeconds": 4.75,
|
||||
"summary": "Fourteen files and 232 tests passed on the final candidate, including controlled headless and rejected-split same-ID barriers, legacy split-owner preflight refusal, missing-proof cleanup refusal, failed split-leaf retention, the runtime close transaction, local same-id admission, controller cleanup fencing, daemon protocol skew and owner routing, host duplicate teardown, SSH method skew, and relay process-owner rejection."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-20",
|
||||
|
||||
+2
-2
@@ -5904,8 +5904,8 @@ export function registerPtyHandlers(
|
||||
(await provider?.supportsIncarnationAddressedShutdown?.(ptyId, opts)) === true
|
||||
return (
|
||||
supported &&
|
||||
opts?.expectedIncarnationId !== undefined &&
|
||||
ptyIncarnationById.get(ptyId) === opts.expectedIncarnationId
|
||||
(opts?.expectedIncarnationId === undefined ||
|
||||
ptyIncarnationById.get(ptyId) === opts.expectedIncarnationId)
|
||||
)
|
||||
},
|
||||
getForegroundProcess: async (ptyId) => {
|
||||
|
||||
@@ -79,8 +79,10 @@ function createHarness(
|
||||
deferReveal?: boolean
|
||||
deferSpawn?: boolean
|
||||
deferStop?: boolean
|
||||
exactStopSupported?: boolean
|
||||
includePairedSnapshot?: boolean
|
||||
rendererMounted?: boolean
|
||||
spawnIncarnationId?: string | null
|
||||
sourceIncarnationId?: string
|
||||
stopAndWaitResult?: boolean
|
||||
} = {}
|
||||
@@ -109,15 +111,20 @@ function createHarness(
|
||||
},
|
||||
persistPtyBinding: () => true
|
||||
}
|
||||
let resolveSpawn: ((result: { id: string; incarnationId: string }) => void) | undefined
|
||||
const spawnIncarnationId =
|
||||
options.spawnIncarnationId === undefined ? SPLIT_INCARNATION_ID : options.spawnIncarnationId
|
||||
let resolveSpawn: ((result: { id: string; incarnationId?: string }) => void) | undefined
|
||||
const spawn = options.deferSpawn
|
||||
? vi.fn(
|
||||
() =>
|
||||
new Promise<{ id: string; incarnationId: string }>((resolve) => {
|
||||
new Promise<{ id: string; incarnationId?: string }>((resolve) => {
|
||||
resolveSpawn = resolve
|
||||
})
|
||||
)
|
||||
: vi.fn(async () => ({ id: SPLIT_PTY_ID, incarnationId: SPLIT_INCARNATION_ID }))
|
||||
: vi.fn(async () => ({
|
||||
id: SPLIT_PTY_ID,
|
||||
...(spawnIncarnationId ? { incarnationId: spawnIncarnationId } : {})
|
||||
}))
|
||||
const kill = vi.fn(() => true)
|
||||
const retireRejectedPty = vi.fn()
|
||||
let resolveStop: ((stopped: boolean) => void) | undefined
|
||||
@@ -129,6 +136,7 @@ function createHarness(
|
||||
})
|
||||
)
|
||||
: vi.fn(async () => options.stopAndWaitResult ?? true)
|
||||
const supportsIncarnationAddressedStop = vi.fn(async () => options.exactStopSupported ?? true)
|
||||
let resolveReveal: ((result: { tabId: string }) => void) | undefined
|
||||
const revealTerminalSession = options.deferReveal
|
||||
? vi.fn(
|
||||
@@ -153,6 +161,7 @@ function createHarness(
|
||||
write: () => true,
|
||||
kill,
|
||||
retireRejectedPty,
|
||||
supportsIncarnationAddressedStop,
|
||||
...(options.stopAndWaitResult !== undefined || options.deferStop ? { stopAndWait } : {}),
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
@@ -202,6 +211,7 @@ function createHarness(
|
||||
kill,
|
||||
retireRejectedPty,
|
||||
stopAndWait,
|
||||
supportsIncarnationAddressedStop,
|
||||
revealTerminalSession,
|
||||
getSession: () => session,
|
||||
getSnapshot: () => internals.mobileSessionTabsByWorktree.get(WORKTREE_ID),
|
||||
@@ -228,7 +238,11 @@ function createHarness(
|
||||
}),
|
||||
getSplitIncarnation: () => internals.ptysById.get(SPLIT_PTY_ID)?.incarnationId,
|
||||
resolveStop: (stopped: boolean) => resolveStop?.(stopped),
|
||||
resolveSpawn: () => resolveSpawn?.({ id: SPLIT_PTY_ID, incarnationId: SPLIT_INCARNATION_ID })
|
||||
resolveSpawn: () =>
|
||||
resolveSpawn?.({
|
||||
id: SPLIT_PTY_ID,
|
||||
...(spawnIncarnationId ? { incarnationId: spawnIncarnationId } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +294,46 @@ describe('remote runtime terminal split authority', () => {
|
||||
expect.soft(harness.revealTerminalSession).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a legacy SSH split owner before spawning an unaddressable PTY', async () => {
|
||||
const harness = createHarness(true, {
|
||||
connectionId: 'ssh-legacy',
|
||||
exactStopSupported: false,
|
||||
sourceIncarnationId: 'source-incarnation'
|
||||
})
|
||||
|
||||
await expect(
|
||||
harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' })
|
||||
).rejects.toThrow('terminal_incarnation_fence_unavailable')
|
||||
|
||||
expect(harness.supportsIncarnationAddressedStop).toHaveBeenCalledWith(SOURCE_PTY_ID, {
|
||||
deadlineMs: expect.any(Number),
|
||||
expectedIncarnationId: 'source-incarnation'
|
||||
})
|
||||
expect(harness.spawn).not.toHaveBeenCalled()
|
||||
expect(harness.stopAndWait).not.toHaveBeenCalled()
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('never stops a rejected split by ID when spawn omits its incarnation', async () => {
|
||||
const harness = createHarness(false, {
|
||||
deferReveal: true,
|
||||
includePairedSnapshot: true,
|
||||
sourceIncarnationId: 'projected-before',
|
||||
spawnIncarnationId: null,
|
||||
stopAndWaitResult: true
|
||||
})
|
||||
|
||||
const split = harness.runtime.splitTerminal(harness.handle, { direction: 'horizontal' })
|
||||
await vi.waitFor(() => expect(harness.revealTerminalSession).toHaveBeenCalledOnce())
|
||||
harness.replaceSourceIncarnation('projected-after')
|
||||
harness.resolveReveal()
|
||||
|
||||
await expect(split).rejects.toThrow('terminal_split_source_not_found')
|
||||
expect(harness.stopAndWait).not.toHaveBeenCalled()
|
||||
expect(harness.retireRejectedPty).toHaveBeenCalledWith(SPLIT_PTY_ID, false, undefined)
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ label: 'local', connectionId: null, expectedHostId: 'local' },
|
||||
{ label: 'SSH', connectionId: 'ssh-1', expectedHostId: 'ssh:ssh-1' }
|
||||
|
||||
@@ -30275,6 +30275,14 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
const sourceIncarnationId =
|
||||
sourceAuthority.liveIncarnationId ?? sourceAuthority.persistedIncarnationId
|
||||
if (
|
||||
(await this.ptyController.supportsIncarnationAddressedStop?.(pty.ptyId, {
|
||||
deadlineMs: Date.now() + REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS,
|
||||
...(sourceIncarnationId ? { expectedIncarnationId: sourceIncarnationId } : {})
|
||||
})) === false
|
||||
) {
|
||||
throw new Error('terminal_incarnation_fence_unavailable')
|
||||
}
|
||||
const leafId = randomUUID()
|
||||
const preAllocatedHandle = this.createPreAllocatedTerminalHandle()
|
||||
const paneKey = makePaneKey(parentTabId, leafId)
|
||||
@@ -30393,14 +30401,16 @@ export class OrcaRuntimeService {
|
||||
} catch (error) {
|
||||
this.setPairedRendererSessionOwnership(result.id, false)
|
||||
let stopped = false
|
||||
try {
|
||||
stopped =
|
||||
(await this.ptyController.stopAndWait?.(result.id, {
|
||||
deadlineMs: Date.now() + REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS,
|
||||
...(result.incarnationId ? { expectedIncarnationId: result.incarnationId } : {})
|
||||
})) ?? false
|
||||
} catch {
|
||||
// Exact cleanup remains unverifiable; preserve the original split authority error.
|
||||
if (result.incarnationId) {
|
||||
try {
|
||||
stopped =
|
||||
(await this.ptyController.stopAndWait?.(result.id, {
|
||||
deadlineMs: Date.now() + REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS,
|
||||
expectedIncarnationId: result.incarnationId
|
||||
})) ?? false
|
||||
} catch {
|
||||
// Exact cleanup remains unverifiable; preserve the original split authority error.
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.ptyController.retireRejectedPty?.(result.id, stopped, result.incarnationId)
|
||||
|
||||
Reference in New Issue
Block a user