diff --git a/src/main/runtime/mobile-subscribe-integration.test.ts b/src/main/runtime/mobile-subscribe-integration.test.ts index a4da469a538..e2475da2218 100644 --- a/src/main/runtime/mobile-subscribe-integration.test.ts +++ b/src/main/runtime/mobile-subscribe-integration.test.ts @@ -851,18 +851,16 @@ describe('mobile subscribe integration', () => { expect(ptySizes.get('pty-1')).toEqual({ cols: 150, rows: 40 }) }) - it('reclaim cancels the soft-leave timer before it can clear the desktop driver', async () => { + it('reclaim cancels the soft-leave timer in the remote-layout branch', async () => { const { runtime } = createRuntime() + await runtime.updateRemoteDesktopViewer('pty-1', 'sub-a', 'viewer-a', 120, 32) await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) runtime.handleMobileUnsubscribe('pty-1', 'client-a') - // The last unsubscribe arms the 250ms soft-leave timer while the fit is held. - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'mobile', clientId: 'client-a' }) expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(true) - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' }) - - await vi.advanceTimersByTimeAsync(250) - expect(runtime.getDriver('pty-1')).toEqual({ kind: 'desktop' }) + expect( + (Reflect.get(runtime, 'pendingSoftLeavers') as Map).has('pty-1') + ).toBe(false) }) it('reclaim cancels a stale soft-leave timer in the active-subscriber branch', async () => { @@ -872,13 +870,35 @@ describe('mobile subscribe integration', () => { // A different client can subscribe while the first client's grace timer is armed. await runtime.handleMobileSubscribe('pty-1', 'client-b', { cols: 40, rows: 18 }) - expect( - (Reflect.get(runtime, 'pendingSoftLeavers') as Map).has('pty-1') - ).toBe(true) + const pending = Reflect.get(runtime, 'pendingSoftLeavers') as Map + expect(pending.has('pty-1')).toBe(true) await runtime.reclaimTerminalForDesktop('pty-1') - expect( - (Reflect.get(runtime, 'pendingSoftLeavers') as Map).has('pty-1') - ).toBe(false) + expect(pending.has('pty-1')).toBe(false) + }) + + it('reclaim cancels pending restore timers on active, orphan, and no-lock branches', async () => { + for (const branch of ['active', 'orphan', 'none'] as const) { + const { runtime } = createRuntime() + await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) + if (branch !== 'active') { + runtime.handleMobileUnsubscribe('pty-1', 'client-a') + } + if (branch !== 'active') { + ;(Reflect.get(runtime, 'terminalFitOverrides') as Map).delete('pty-1') + } + if (branch === 'none') { + ;(Reflect.get(runtime, 'currentDriver') as Map).set('pty-1', { + kind: 'idle' + }) + } + const pending = Reflect.get(runtime, 'pendingRestoreTimers') as Map + pending.set('pty-1', { timer: setTimeout(() => {}, 60_000), clientId: 'client-a' }) + await runtime.reclaimTerminalForDesktop('pty-1') + expect(pending.has('pty-1')).toBe(false) + expect( + (Reflect.get(runtime, 'pendingSoftLeavers') as Map).has('pty-1') + ).toBe(false) + } }) it('reclaim cancels a stale soft-leave timer in the orphan-driver branch', async () => { @@ -895,21 +915,6 @@ describe('mobile subscribe integration', () => { ).toBe(false) }) - it('reclaim cancels pending timers even when no driver lock remains', async () => { - const { runtime } = createRuntime() - await runtime.handleMobileSubscribe('pty-1', 'client-a', { cols: 45, rows: 20 }) - runtime.handleMobileUnsubscribe('pty-1', 'client-a') - const currentDriver = Reflect.get(runtime, 'currentDriver') as Map - currentDriver.set('pty-1', { kind: 'idle' }) - const fitOverrides = Reflect.get(runtime, 'terminalFitOverrides') as Map - fitOverrides.delete('pty-1') - - expect(await runtime.reclaimTerminalForDesktop('pty-1')).toBe(false) - expect( - (Reflect.get(runtime, 'pendingSoftLeavers') as Map).has('pty-1') - ).toBe(false) - }) - it('reclaimTerminalForDesktop prefers fresh desktop geometry for a held PTY', async () => { // Why: manual restore must honor fresh desktop geometry measured while the PTY was phone-held, not the stale baseline. settingsState.mobileAutoRestoreFitMs = null diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 8f16ddc6f8d..54243d3edac 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -15387,16 +15387,7 @@ export class OrcaRuntimeService { this.layouts.delete(ptyId) this.layoutQueues.delete(ptyId) this.freshSubscribeGuard.delete(ptyId) - const pendingRestore = this.pendingRestoreTimers.get(ptyId) - if (pendingRestore) { - clearTimeout(pendingRestore.timer) - this.pendingRestoreTimers.delete(ptyId) - } - const pendingSoft = this.pendingSoftLeavers.get(ptyId) - if (pendingSoft) { - clearTimeout(pendingSoft.timer) - this.pendingSoftLeavers.delete(ptyId) - } + this.cancelPendingDriverMutations(ptyId) // Why: a cold restore can respawn under the same session id within the // delayed-Enter window; the armed Enter would inject \r into the // replacement and stamp rows it never received. @@ -16109,7 +16100,7 @@ export class OrcaRuntimeService { return false } - // Why: an explicit desktop take-back supersedes every delayed driver mutation. + // Why: explicit take-back supersedes delayed mutations, revoking soft-leave grace admission for mobile input floors. private cancelPendingDriverMutations(ptyId: string): void { const pendingRestore = this.pendingRestoreTimers.get(ptyId) if (pendingRestore) {