diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index ab0e866a253..5095cea186c 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -31451,6 +31451,86 @@ describe('OrcaRuntimeService', () => { expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) }) + it('settles a renderer-owned close when another transaction retires its surface first', async () => { + const ptyId = 'renderer-worker-retired-tab' + const { runtimeStore, getSession } = makeRuntimeStoreWithWorkspaceSession( + makeWorkspaceSessionWithHeadlessTerminal({ + tabsByWorktree: { + [TEST_WORKTREE_ID]: [ + { + id: 'host-tab', + ptyId, + worktreeId: TEST_WORKTREE_ID, + title: 'Renderer worker', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + terminalLayoutsByTabId: { + 'host-tab': makeHeadlessTerminalLayout({ [HEADLESS_LEAF_ID]: ptyId }) + } + }) + ) + const kill = vi.fn(() => true) + const closeTerminalTab = vi.fn(async () => {}) + const runtime = new OrcaRuntimeService(runtimeStore as never) + runtime.setNotifier({ closeTerminalTab } as never) + runtime.setPtyController({ + write: () => true, + kill, + getForegroundProcess: async () => null, + listProcesses: async () => [] + }) + runtime.registerPty(ptyId, TEST_WORKTREE_ID, null, { + tabId: 'host-tab', + leafId: HEADLESS_LEAF_ID + }) + runtime.syncWindowGraph(1, { + tabs: [ + { + tabId: 'host-tab', + worktreeId: TEST_WORKTREE_ID, + title: 'Renderer worker', + activeLeafId: HEADLESS_LEAF_ID, + layout: null + } + ], + leaves: [ + { + tabId: 'host-tab', + worktreeId: TEST_WORKTREE_ID, + leafId: HEADLESS_LEAF_ID, + paneRuntimeId: 1, + ptyId + } + ] + }) + const listed = await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`) + const terminal = listed.tabs.find((tab) => tab.type === 'terminal') + if (!terminal || terminal.type !== 'terminal' || !terminal.terminal) { + throw new Error('Expected a ready terminal') + } + const closeMobileSessionTab = runtime.closeMobileSessionTab.bind(runtime) + vi.spyOn(runtime, 'closeMobileSessionTab').mockImplementation(async (...args) => { + runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) + await closeMobileSessionTab(...args) + throw new Error('tab_not_found') + }) + + await expect(runtime.closeTerminal(terminal.terminal)).resolves.toEqual({ + handle: terminal.terminal, + tabId: 'host-tab', + ptyKilled: true + }) + expect(kill).toHaveBeenCalledWith(ptyId) + expect(closeTerminalTab).not.toHaveBeenCalled() + expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([]) + expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs).toEqual([]) + }) + it('does not mask a missing durable tab while its headless surface remains live', async () => { const ptyId = 'headless-worker-live-tab' const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession( diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 6d22603a132..8de190f392d 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -29185,6 +29185,17 @@ export class OrcaRuntimeService { return surface } + private isAlreadyRetiredMobileTerminalSurfaceError( + error: Error, + worktreeId: string, + parentTabId: string + ): boolean { + // Why: missing persistence is ambiguous while a sibling or stale live publication still owns the surface. + return ( + error.message === 'tab_not_found' && !this.findMobileTerminalSurface(worktreeId, parentTabId) + ) + } + private findMobileTerminalSurfaceForPty( worktreeId: string, ptyId: string @@ -29799,10 +29810,16 @@ export class OrcaRuntimeService { localPtyTeardownOwnedExternally: true }) } catch (error) { - if (!(error instanceof Error) || error.message !== 'workspace_session_unavailable') { + if (!(error instanceof Error)) { + throw error + } + if (error.message === 'workspace_session_unavailable') { + this.notifier.closeTerminal?.(tabId) + } else if ( + !this.isAlreadyRetiredMobileTerminalSurfaceError(error, pty.pty.worktreeId, tabId) + ) { throw error } - this.notifier.closeTerminal?.(tabId) } const ptyKilled = await this.stopExplicitlyClosedTabPtys(ptyIdsToKill, pty.pty.ptyId) return this.describeTerminalClose(handle, tabId, pty.pty.ptyId, ptyKilled) @@ -29826,10 +29843,8 @@ export class OrcaRuntimeService { if (error.message === 'workspace_session_unavailable') { this.notifier?.closeTerminal(tabId) } else if ( - error.message !== 'tab_not_found' || - this.findMobileTerminalSurface(pty.pty.worktreeId, tabId) + !this.isAlreadyRetiredMobileTerminalSurfaceError(error, pty.pty.worktreeId, tabId) ) { - // Only absence in both persistence and the live publication proves another owner finished teardown. throw error } }