mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
fix(terminal): require split source incarnation proof
This commit is contained in:
@@ -3463,7 +3463,7 @@
|
||||
"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 legacy SSH owner is refused before split spawn, and missing spawn proof never reaches ID-only cleanup"
|
||||
"missing source proof and a legacy SSH owner are refused before split spawn, while missing spawn proof never reaches ID-only cleanup"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3695,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.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."
|
||||
"durationSeconds": 4.72,
|
||||
"summary": "Fourteen files and 233 tests passed on the final candidate, including controlled headless and rejected-split same-ID barriers, missing-source and legacy-owner pre-spawn 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) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ const TAB_ID = 'tab-remote'
|
||||
const SOURCE_LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const SOURCE_PTY_ID = 'pty-source'
|
||||
const SPLIT_PTY_ID = 'pty-split'
|
||||
const SOURCE_INCARNATION_ID = '11111111-1111-4111-8111-111111111112'
|
||||
const SPLIT_INCARNATION_ID = '22222222-2222-4222-8222-222222222222'
|
||||
const REPLACEMENT_SPLIT_INCARNATION_ID = '33333333-3333-4333-8333-333333333333'
|
||||
|
||||
@@ -83,12 +84,14 @@ function createHarness(
|
||||
includePairedSnapshot?: boolean
|
||||
rendererMounted?: boolean
|
||||
spawnIncarnationId?: string | null
|
||||
sourceIncarnationId?: string
|
||||
sourceIncarnationId?: string | null
|
||||
stopAndWaitResult?: boolean
|
||||
} = {}
|
||||
) {
|
||||
let session = persistedSession(includeSource)
|
||||
const connectionId = options.connectionId ?? null
|
||||
const sourceIncarnationId =
|
||||
options.sourceIncarnationId === undefined ? SOURCE_INCARNATION_ID : options.sourceIncarnationId
|
||||
const ownerHostId = connectionId ? `ssh:${connectionId}` : 'local'
|
||||
const requestedSessionHostIds: (string | undefined)[] = []
|
||||
const repo = {
|
||||
@@ -196,7 +199,7 @@ function createHarness(
|
||||
runtime.registerPty(SOURCE_PTY_ID, WORKTREE_ID, connectionId, {
|
||||
tabId: TAB_ID,
|
||||
leafId: SOURCE_LEAF_ID,
|
||||
...(options.sourceIncarnationId ? { incarnationId: options.sourceIncarnationId } : {})
|
||||
...(sourceIncarnationId ? { incarnationId: sourceIncarnationId } : {})
|
||||
})
|
||||
const internals = runtime as unknown as {
|
||||
issuePtyHandle: (pty: unknown) => string
|
||||
@@ -314,6 +317,19 @@ describe('remote runtime terminal split authority', () => {
|
||||
expect(harness.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a split source without incarnation proof before capability probing', async () => {
|
||||
const harness = createHarness(true, { sourceIncarnationId: null })
|
||||
|
||||
await expect(
|
||||
harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' })
|
||||
).rejects.toThrow('terminal_incarnation_fence_unavailable')
|
||||
|
||||
expect(harness.supportsIncarnationAddressedStop).not.toHaveBeenCalled()
|
||||
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,
|
||||
@@ -404,6 +420,7 @@ describe('remote runtime terminal split authority', () => {
|
||||
const harness = createHarness(true, {
|
||||
deferSpawn: true,
|
||||
includePairedSnapshot: false,
|
||||
sourceIncarnationId: null,
|
||||
stopAndWaitResult: true
|
||||
})
|
||||
harness.replacePersistedSourceIncarnation('persisted-before')
|
||||
|
||||
@@ -6451,8 +6451,14 @@ describe('OrcaRuntimeService', () => {
|
||||
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-remote-initial' })
|
||||
.mockResolvedValueOnce({ id: 'pty-remote-setup-split' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-remote-initial',
|
||||
incarnationId: 'pty-remote-initial-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-remote-setup-split',
|
||||
incarnationId: 'pty-remote-setup-split-incarnation'
|
||||
})
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-remote-split' })
|
||||
registerSshGitProvider('ssh-1', provider as never)
|
||||
registerSshFilesystemProvider('ssh-1', fsProvider as never)
|
||||
@@ -15288,8 +15294,8 @@ describe('OrcaRuntimeService', () => {
|
||||
it('splits visible pty-backed terminal sessions through the parent renderer tab', async () => {
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-source' })
|
||||
.mockResolvedValueOnce({ id: 'pty-split' })
|
||||
.mockResolvedValueOnce({ id: 'pty-source', incarnationId: 'pty-source-incarnation' })
|
||||
.mockResolvedValueOnce({ id: 'pty-split', incarnationId: 'pty-split-incarnation' })
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg' })
|
||||
const splitTerminal = vi.fn()
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
@@ -15498,7 +15504,8 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
||||
runtime.registerPty(ptyId, TEST_WORKTREE_ID, null, {
|
||||
tabId,
|
||||
leafId: HEADLESS_LEAF_ID
|
||||
leafId: HEADLESS_LEAF_ID,
|
||||
incarnationId: 'closing-persisted-incarnation'
|
||||
})
|
||||
const internals = runtime as unknown as {
|
||||
issuePtyHandle: (pty: unknown) => string
|
||||
@@ -15592,8 +15599,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const folderPath = await mkdtemp(join(tmpdir(), 'orca-runtime-folder-split-'))
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-folder-source' })
|
||||
.mockResolvedValueOnce({ id: 'pty-folder-split' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-folder-source',
|
||||
incarnationId: 'pty-folder-source-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-folder-split',
|
||||
incarnationId: 'pty-folder-split-incarnation'
|
||||
})
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-folder' })
|
||||
const folderWorkspace = makeFolderWorkspace({ folderPath })
|
||||
const projectGroup = makeFolderProjectGroup({ parentPath: folderPath })
|
||||
@@ -15693,7 +15706,10 @@ describe('OrcaRuntimeService', () => {
|
||||
}),
|
||||
'ssh:ssh-1'
|
||||
)
|
||||
const spawn = vi.fn().mockResolvedValue({ id: splitPtyId })
|
||||
const spawn = vi.fn().mockResolvedValue({
|
||||
id: splitPtyId,
|
||||
incarnationId: 'ssh-split-incarnation'
|
||||
})
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...runtimeStore,
|
||||
getRepos: () => [remoteRepo],
|
||||
@@ -15708,7 +15724,8 @@ describe('OrcaRuntimeService', () => {
|
||||
runtime.syncWindowGraph(1, { tabs: [], leaves: [] })
|
||||
runtime.registerPty(sourcePtyId, TEST_WORKTREE_ID, 'ssh-1', {
|
||||
tabId,
|
||||
leafId: HEADLESS_LEAF_ID
|
||||
leafId: HEADLESS_LEAF_ID,
|
||||
incarnationId: 'ssh-source-incarnation'
|
||||
})
|
||||
const internals = runtime as unknown as {
|
||||
issuePtyHandle: (pty: unknown) => string
|
||||
@@ -28722,7 +28739,13 @@ describe('OrcaRuntimeService', () => {
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValueOnce({ id: ptyId }).mockResolvedValueOnce({ id: splitPtyId }),
|
||||
spawn: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: ptyId, incarnationId: 'runtime-owned-incarnation' })
|
||||
.mockResolvedValueOnce({
|
||||
id: splitPtyId,
|
||||
incarnationId: 'runtime-owned-split-incarnation'
|
||||
}),
|
||||
write: () => true,
|
||||
kill,
|
||||
getForegroundProcess: async () => null,
|
||||
@@ -28942,8 +28965,14 @@ describe('OrcaRuntimeService', () => {
|
||||
it('publishes laptop-created remote runtime split terminals to phone session tabs', async () => {
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'laptop-created-pty' })
|
||||
.mockResolvedValueOnce({ id: 'laptop-split-pty' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'laptop-created-pty',
|
||||
incarnationId: 'laptop-created-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'laptop-split-pty',
|
||||
incarnationId: 'laptop-split-incarnation'
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
@@ -44700,8 +44729,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-cli-setup-split' })
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-cli-setup-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-cli-setup-setup' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-cli-setup-main',
|
||||
incarnationId: 'pty-cli-setup-main-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-cli-setup-setup',
|
||||
incarnationId: 'pty-cli-setup-child-incarnation'
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
@@ -44788,8 +44823,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-bg-setup-split' })
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-bg-split-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-bg-split-setup' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-bg-split-main',
|
||||
incarnationId: 'pty-bg-split-main-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-bg-split-setup',
|
||||
incarnationId: 'pty-bg-split-child-incarnation'
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
@@ -44870,8 +44911,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-active-setup-split' })
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-active-split-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-active-split-setup' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-active-split-main',
|
||||
incarnationId: 'pty-active-split-main-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-active-split-setup',
|
||||
incarnationId: 'pty-active-split-child-incarnation'
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
write: () => true,
|
||||
@@ -45577,8 +45624,14 @@ describe('OrcaRuntimeService', () => {
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const spawn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 'pty-startup-split-main' })
|
||||
.mockResolvedValueOnce({ id: 'pty-startup-split-setup' })
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-startup-split-main',
|
||||
incarnationId: 'pty-startup-split-main-incarnation'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'pty-startup-split-setup',
|
||||
incarnationId: 'pty-startup-split-child-incarnation'
|
||||
})
|
||||
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-startup-split' })
|
||||
runtime.setPtyController({
|
||||
spawn,
|
||||
|
||||
@@ -30275,10 +30275,13 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
const sourceIncarnationId =
|
||||
sourceAuthority.liveIncarnationId ?? sourceAuthority.persistedIncarnationId
|
||||
if (!sourceIncarnationId) {
|
||||
throw new Error('terminal_incarnation_fence_unavailable')
|
||||
}
|
||||
if (
|
||||
(await this.ptyController.supportsIncarnationAddressedStop?.(pty.ptyId, {
|
||||
deadlineMs: Date.now() + REJECTED_SPLIT_PTY_STOP_TIMEOUT_MS,
|
||||
...(sourceIncarnationId ? { expectedIncarnationId: sourceIncarnationId } : {})
|
||||
expectedIncarnationId: sourceIncarnationId
|
||||
})) === false
|
||||
) {
|
||||
throw new Error('terminal_incarnation_fence_unavailable')
|
||||
|
||||
Reference in New Issue
Block a user