From 488e3733b8554204e9529a0cca4e2ea7ee069465 Mon Sep 17 00:00:00 2001 From: orca-bug-scan-bot Date: Thu, 2 Jul 2026 20:51:03 -0700 Subject: [PATCH] fix: address pr-bug-scan validated finding from #6952 throwNodeNotFound() now re-raises AbortError when the shared signal is aborted, so a signal-cancelled node probe no longer launders into 'Node.js not found'; sequential fallback runs. --- .../ssh/ssh-remote-node-resolution.test.ts | 19 +++++++++++++++++++ src/main/ssh/ssh-remote-node-resolution.ts | 16 ++++++++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/ssh/ssh-remote-node-resolution.test.ts b/src/main/ssh/ssh-remote-node-resolution.test.ts index 23ea9097540..b3cad3c0ecc 100644 --- a/src/main/ssh/ssh-remote-node-resolution.test.ts +++ b/src/main/ssh/ssh-remote-node-resolution.test.ts @@ -331,6 +331,25 @@ describe('resolveRemoteNodePath', () => { ).rejects.toBe(sessionLimitError) }) + it('surfaces an AbortError instead of not-found when the shared signal is aborted', async () => { + // Why: in concurrent bootstrap a sibling probe aborts this resolution via + // the shared signal. Each strategy catch swallows the injected AbortError, + // so without re-raising it here the resolver would launder cancellation + // into a fatal "Node.js not found" and defeat the sequential fallback. + const controller = new AbortController() + const abortError = Object.assign(new Error('SSH operation was cancelled'), { + name: 'AbortError' + }) + execCommandMock.mockImplementation(() => { + controller.abort() + return Promise.reject(abortError) + }) + + await expect( + resolveRemoteNodePath(conn, undefined, { signal: controller.signal }) + ).rejects.toMatchObject({ name: 'AbortError' }) + }) + it('throws when every candidate across both strategies is below the minimum', async () => { execCommandMock .mockResolvedValueOnce('/old/node\n') // path probe diff --git a/src/main/ssh/ssh-remote-node-resolution.ts b/src/main/ssh/ssh-remote-node-resolution.ts index 4117ddf97ef..e4bb3181b79 100644 --- a/src/main/ssh/ssh-remote-node-resolution.ts +++ b/src/main/ssh/ssh-remote-node-resolution.ts @@ -1,5 +1,5 @@ import type { SshConnection } from './ssh-connection' -import { shellEscape } from './ssh-connection-utils' +import { createSshOperationAbortError, shellEscape } from './ssh-connection-utils' import type { RemoteHostPlatform } from './ssh-remote-platform' import { isWindowsRemoteHost, normalizeWindowsRemotePath } from './ssh-remote-platform' import { powerShellCommand } from './ssh-remote-powershell' @@ -47,7 +47,7 @@ export async function resolveRemoteNodePath( return loginShellPath } - throwNodeNotFound() + throwNodeNotFound(options) } // Probe the on-disk install directories of every common Node version manager @@ -245,10 +245,18 @@ async function resolveRemoteWindowsNodePath( // Fall through to the shared error below. } - throwNodeNotFound() + throwNodeNotFound(options) } -function throwNodeNotFound(): never { +function throwNodeNotFound(options?: RemoteNodeResolutionOptions): never { + // Why: when a sibling bootstrap probe aborts this resolution via the shared + // signal, the strategy catch-blocks swallow the injected AbortError and fall + // through to here. Surfacing "Node.js not found" would launder a cancellation + // into a fatal non-session failure, defeating the concurrent-bootstrap + // sequential fallback. Re-raise the abort so callers classify it correctly. + if (options?.signal?.aborted) { + throw createSshOperationAbortError() + } throw new Error( 'Node.js not found on remote host. Orca relay requires Node.js 18+. ' + 'Install Node.js on the remote and try again.'