diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 9b0939373ff..62c9b3c29ce 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3963,6 +3963,7 @@ "provider reconnect retries durable shutdown intent with its generation and pane identity", "persisted local shutdown waits for daemon startup instead of accepting unknown-id success from the fallback provider", "an in-flight replaced local provider cannot complete or delete the newer shutdown owner", + "local and SSH stale-provider not-found rejections preserve replacement ownership and coalesced callers fail closed", "host-scoped liveness lists only the selected provider instead of fanning out to unrelated SSH hosts", "relay generation reuse removes stale retry ownership without touching the new PTY" ] diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index ae48f61811d..77ec50a8f02 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -3496,7 +3496,10 @@ describe('registerPtyHandlers', () => { it('does not let an in-flight old provider complete persisted shutdown ownership', async () => { vi.useFakeTimers() - const oldShutdown = makeDeferred() + let rejectOldShutdown!: (error: Error) => void + const oldShutdown = new Promise((_resolve, reject) => { + rejectOldShutdown = reject + }) const makeProvider = (shutdown: (id: string) => Promise) => ({ shutdown, @@ -3506,7 +3509,7 @@ describe('registerPtyHandlers', () => { }) as never const newShutdown = vi.fn().mockResolvedValue(undefined) const removePendingLocalPtyShutdown = vi.fn() - setLocalPtyProvider(makeProvider(() => oldShutdown.promise)) + setLocalPtyProvider(makeProvider(() => oldShutdown)) handlers.clear() registerPtyHandlers(mainWindow as never, undefined, undefined, undefined, undefined, { getPendingLocalPtyShutdowns: () => [{ ptyId: 'daemon-pty', requestedAt: 1 }], @@ -3515,8 +3518,10 @@ describe('registerPtyHandlers', () => { } as never) await vi.advanceTimersByTimeAsync(0) + const coalescedKill = handlers.get('pty:kill')!(null, { id: 'daemon-pty' }) as Promise setLocalPtyProvider(makeProvider(newShutdown)) - oldShutdown.resolve() + rejectOldShutdown(new Error('Session not found: daemon-pty')) + await expect(coalescedKill).rejects.toThrow('PTY shutdown still pending') await vi.advanceTimersByTimeAsync(0) expect(newShutdown).toHaveBeenCalledOnce() @@ -3526,6 +3531,64 @@ describe('registerPtyHandlers', () => { vi.useRealTimers() }) + it('does not let a stale SSH provider not-found rejection delete replacement ownership', async () => { + vi.useFakeTimers() + let rejectOldShutdown!: (error: Error) => void + const oldShutdown = vi.fn( + () => + new Promise((_resolve, reject) => { + rejectOldShutdown = reject + }) + ) + const newShutdown = vi.fn().mockResolvedValue(undefined) + const lease = { + targetId: 'ssh-1', + ptyId: 'relay-pty', + relayInstanceId: 'boot-1', + state: 'detached' as const, + shutdownRequestedAt: 2, + createdAt: 1, + updatedAt: 2 + } + const store = { + getSshRemotePtyLeases: vi.fn(() => [lease]), + markSshRemotePtyLease: vi.fn() + } + handlers.clear() + registerPtyHandlers( + mainWindow as never, + undefined, + undefined, + undefined, + undefined, + store as never + ) + registerSshPtyProvider('ssh-1', { + shutdown: oldShutdown, + onExit: vi.fn(() => () => {}) + } as never) + await vi.advanceTimersByTimeAsync(0) + + registerSshPtyProvider('ssh-1', { + shutdown: newShutdown, + onExit: vi.fn(() => () => {}) + } as never) + rejectOldShutdown(new Error('PTY "relay-pty" not found')) + await vi.advanceTimersByTimeAsync(0) + + expect(oldShutdown).toHaveBeenCalledOnce() + expect(newShutdown).toHaveBeenCalledOnce() + expect(store.markSshRemotePtyLease).toHaveBeenCalledWith( + 'ssh-1', + 'ssh:ssh-1@@boot-1@@relay-pty', + 'terminated' + ) + unregisterSshPtyProvider('ssh-1') + await vi.advanceTimersByTimeAsync(30_000) + expect(vi.getTimerCount()).toBe(0) + vi.useRealTimers() + }) + it('synthesizes runtime exit after ordinary daemon-backed pty kill', async () => { const shutdown = vi.fn(async () => undefined) const runtime = { diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index e570bc2e52e..9355b07b3fc 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -647,6 +647,9 @@ function attemptRetainedPtyShutdown(retained: RetainedPtyShutdown): Promise