From 799ec595ba75892f6b6afcd34311a0722992a032 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 12 Oct 2026 03:59:05 -0700 Subject: [PATCH] refactor(ssh): remove the ssh-relay-deploy-helpers re-export module (#27961) Co-authored-by: m4air --- config/scripts/ci-shard-timings.json | 2 +- .../ssh-connect-decision-cancellation.test.ts | 4 +- src/main/ipc/ssh-host-server-connect.ts | 2 +- .../ssh-host-server-on-connect-wiring.test.ts | 6 +- src/main/ipc/ssh.test.ts | 2 +- ...untime-daemon-adoption.integration.test.ts | 6 +- .../legacy-relay-bridge-registry.ts | 2 +- .../legacy-relay-endpoints.test.ts | 6 +- .../legacy-relay-endpoints.ts | 2 +- .../orcad-activation-crash-recovery.test.ts | 8 +- .../orcad-activation-fence-generation.test.ts | 6 +- src/main/ssh/orcad-activation-lock.ts | 2 +- .../ssh/orcad-activation-transaction.test.ts | 8 +- .../ssh/orcad-candidate-launch-verdict.ts | 2 +- src/main/ssh/orcad-decommission-stop.ts | 2 +- src/main/ssh/orcad-deployment-target.test.ts | 8 +- src/main/ssh/orcad-deployment-target.ts | 2 +- ...cad-exited-own-fence-mutation-race.test.ts | 6 +- src/main/ssh/orcad-exited-own-fence.test.ts | 6 +- .../ssh/orcad-exited-own-install-lock.test.ts | 6 +- .../ssh/orcad-exited-own-lock-windows.test.ts | 6 +- src/main/ssh/orcad-installed-activation.ts | 2 +- src/main/ssh/orcad-managed-remote-stop.ts | 2 +- src/main/ssh/orcad-managed-wake.test.ts | 8 +- ...-migration-terminal-gate-host-wide.test.ts | 6 +- .../ssh/orcad-remote-decommission.test.ts | 8 +- src/main/ssh/orcad-remote-deploy.test.ts | 9 +- src/main/ssh/orcad-remote-gc.test.ts | 9 +- src/main/ssh/orcad-remote-gc.ts | 3 +- .../orcad-remote-install-termination.test.ts | 8 +- src/main/ssh/orcad-remote-install.ts | 3 +- .../orcad-remote-lifecycle-windows.test.ts | 9 +- src/main/ssh/orcad-remote-log-tail.test.ts | 6 +- src/main/ssh/orcad-remote-log-tail.ts | 2 +- ...remote-node-runtime-promotion-lock.test.ts | 8 +- ...rcad-remote-node-runtime-slow-hash.test.ts | 8 +- .../orcad-remote-node-runtime-windows.test.ts | 8 +- src/main/ssh/orcad-remote-node-runtime.ts | 3 +- src/main/ssh/orcad-remote-preflight.ts | 2 +- src/main/ssh/orcad-remote-primitives.test.ts | 6 +- src/main/ssh/orcad-remote-record-file.ts | 2 +- src/main/ssh/orcad-remote-rollback.test.ts | 9 +- .../ssh/orcad-remote-runtime-control.test.ts | 6 +- src/main/ssh/orcad-remote-runtime-control.ts | 2 +- .../ssh/orcad-remote-windows-commands.test.ts | 9 +- .../orcad-remote-windows-decommission.test.ts | 9 +- src/main/ssh/orcad-remote-windows-node.ts | 2 +- src/main/ssh/orcad-rollback-transition.ts | 2 +- src/main/ssh/orcad-runtime-target.test.ts | 8 +- src/main/ssh/orcad-state-mutation-exec.ts | 2 +- .../ssh/orcad-state-mutation-timeout.test.ts | 13 +- .../ssh/remote-install-gc-termination.test.ts | 8 +- src/main/ssh/remote-install-gc-tombstone.ts | 3 +- .../ssh/remote-node-runtime-store-gc.test.ts | 8 +- src/main/ssh/remote-node-runtime-store-gc.ts | 3 +- ...de-runtime-store-lock-host-release.test.ts | 8 +- .../remote-node-runtime-store-windows.test.ts | 8 +- src/main/ssh/remote-versioned-install.test.ts | 9 +- src/main/ssh/ssh-connection-file-transfers.ts | 2 +- .../ssh-host-relay-endpoint-census.test.ts | 6 +- .../ssh/ssh-host-relay-endpoint-census.ts | 2 +- .../ssh-host-relay-terminals-on-connect.ts | 2 +- .../ssh/ssh-host-relay-windows-census.test.ts | 6 +- src/main/ssh/ssh-host-relay-windows-census.ts | 2 +- ...h-host-server-on-connect-telemetry.test.ts | 2 +- .../ssh/ssh-host-server-on-connect.test.ts | 22 +- src/main/ssh/ssh-host-server-on-connect.ts | 20 +- src/main/ssh/ssh-host-unserved-status.ts | 2 +- .../ssh/ssh-relay-endpoint-incumbent.test.ts | 4 +- src/main/ssh/ssh-relay-endpoint-incumbent.ts | 2 +- .../ssh/ssh-relay-endpoint-pty-count.test.ts | 2 +- src/main/ssh/ssh-relay-endpoint-pty-count.ts | 2 +- src/main/ssh/ssh-relay-endpoint-runtime.ts | 2 +- ...test.ts => ssh-relay-exec-command.test.ts} | 234 +--------------- .../ssh-relay-gc-claim-termination.test.ts | 8 +- src/main/ssh/ssh-relay-host-exec.ts | 2 +- .../ssh/ssh-relay-incumbent-process.test.ts | 7 +- ...ssh-relay-install-lock-termination.test.ts | 8 +- src/main/ssh/ssh-relay-install-lock.test.ts | 6 +- .../ssh-relay-sentinel-copy-budget.test.ts | 2 +- src/main/ssh/ssh-relay-sentinel.test.ts | 249 ++++++++++++++++++ ...eploy-helpers.ts => ssh-relay-sentinel.ts} | 5 - ...elay-versioned-install-termination.test.ts | 8 +- .../ssh/ssh-remote-node-install-guidance.ts | 2 +- .../ssh/ssh-remote-node-resolution.test.ts | 4 +- src/main/ssh/ssh-remote-node-resolution.ts | 3 +- .../ssh/ssh-remote-platform-detection.test.ts | 4 +- src/main/ssh/ssh-remote-platform-detection.ts | 7 +- .../ssh/system-ssh-port-forward-provider.ts | 2 +- 89 files changed, 521 insertions(+), 423 deletions(-) rename src/main/ssh/{ssh-relay-deploy-helpers.test.ts => ssh-relay-exec-command.test.ts} (61%) create mode 100644 src/main/ssh/ssh-relay-sentinel.test.ts rename src/main/ssh/{ssh-relay-deploy-helpers.ts => ssh-relay-sentinel.ts} (96%) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index b2b86ec33d0..4c088e0e977 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -5462,7 +5462,7 @@ "src/main/ssh/ssh-reconnect-error-classification.test.ts": 123, "src/main/ssh/ssh-reconnect-ladder.test.ts": 96, "src/main/ssh/ssh-relay-answered-failure-producers.test.ts": 301, - "src/main/ssh/ssh-relay-deploy-helpers.test.ts": 571, + "src/main/ssh/ssh-relay-sentinel.test.ts": 571, "src/main/ssh/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 2228, "src/main/ssh/ssh-relay-endpoint-incumbent.test.ts": 115, "src/main/ssh/ssh-relay-endpoint-pty-count.test.ts": 695, diff --git a/src/main/ipc/ssh-connect-decision-cancellation.test.ts b/src/main/ipc/ssh-connect-decision-cancellation.test.ts index a3f06e23b7c..2cc7834882f 100644 --- a/src/main/ipc/ssh-connect-decision-cancellation.test.ts +++ b/src/main/ipc/ssh-connect-decision-cancellation.test.ts @@ -62,7 +62,7 @@ describe('a connect cancelled during its server decision', () => { // The census dials after the teardown, opening a transport no one else holds. recordSshConnectionOpened(asTransport(opened)) mockConnectionManager.getConnection.mockReturnValue(opened) - return { route: 'relay', reason: 'failed' } + return { route: 'unserved', reason: 'failed' } }) await expect( Promise.resolve(handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })) @@ -90,7 +90,7 @@ describe('a connect cancelled during its server decision', () => { recordSshConnectionOpened(asTransport(opened)) mockConnectionManager.getConnection.mockReturnValue(opened) await new Promise((resolve) => (resumeStale = resolve)) - return { route: 'relay', reason: 'orcad_unavailable', detail: 'unsupported_host' } + return { route: 'unserved', reason: 'orcad_unavailable', detail: 'unsupported_host' } }) const stale = Promise.resolve(handlers.get('ssh:connect')!(null, { targetId: 'ssh-1' })) await vi.waitFor(() => expect(staleDecided).toBe(true)) diff --git a/src/main/ipc/ssh-host-server-connect.ts b/src/main/ipc/ssh-host-server-connect.ts index 91d24b34cb7..56f01ea22e8 100644 --- a/src/main/ipc/ssh-host-server-connect.ts +++ b/src/main/ipc/ssh-host-server-connect.ts @@ -99,7 +99,7 @@ export function publishManagedServerConnect( */ export function publishUnservedHostServer( target: SshTarget, - decision: Extract + decision: Extract ): Error { const managedServer = unservedServerStatus(decision) setSshHostServerStatus(target.id, managedServer) diff --git a/src/main/ipc/ssh-host-server-on-connect-wiring.test.ts b/src/main/ipc/ssh-host-server-on-connect-wiring.test.ts index 356c9150f9e..76ab2bab8cf 100644 --- a/src/main/ipc/ssh-host-server-on-connect-wiring.test.ts +++ b/src/main/ipc/ssh-host-server-on-connect-wiring.test.ts @@ -80,7 +80,11 @@ describe('connect-time server decision against the real profile', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) await expect( resolveHostServerOnConnect(target(), hostServerOnConnectDeps(userDataPath)) - ).resolves.toEqual({ route: 'relay', reason: 'orcad_unavailable', detail: 'unsupported_host' }) + ).resolves.toEqual({ + route: 'unserved', + reason: 'orcad_unavailable', + detail: 'unsupported_host' + }) warn.mockRestore() expect(target().orcadFence).toBeUndefined() expect(target().managedServerUnavailable).toEqual({ diff --git a/src/main/ipc/ssh.test.ts b/src/main/ipc/ssh.test.ts index 785d48c4419..2ade4c448e8 100644 --- a/src/main/ipc/ssh.test.ts +++ b/src/main/ipc/ssh.test.ts @@ -70,7 +70,7 @@ describe('SSH IPC handlers', () => { it('fails a host the managed server does not serve, with no fallback', async () => { const decision = { - route: 'relay' as const, + route: 'unserved' as const, reason: 'orcad_unavailable' as const, detail: 'unsupported_host' } diff --git a/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts b/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts index 2c68365bb07..8cd4d2d05fc 100644 --- a/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts +++ b/src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts @@ -4,6 +4,7 @@ * are installed side by side as `~/.orca-remote/orcad-/`, launched and stopped with * the client's own deploy commands, and share one data root. */ +import type * as ExecCommandModule from '../ssh/ssh-relay-exec-command' import { build } from 'esbuild' import { copyFileSync, @@ -59,7 +60,10 @@ import { // GC runs its real commands, against this machine instead of an SSH host. const { execMock } = vi.hoisted(() => ({ execMock: vi.fn() })) -vi.mock('../ssh/ssh-relay-deploy-helpers', () => ({ execCommand: execMock })) +vi.mock('../ssh/ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: execMock +})) type Runtime = 'Bun' | 'Node' type Slot = { runtime: Runtime; dir: string; version: string } diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-registry.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-registry.ts index eb0ff2cc0e0..9ee2d2fbae5 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-registry.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-registry.ts @@ -10,7 +10,7 @@ import type { import { parseAppSshPtyId } from '../../../shared/ssh-pty-id' import type { SshRemotePtyLease } from '../../../shared/ssh-types' import type { SshConnection } from '../ssh-connection' -import { waitForSentinel } from '../ssh-relay-deploy-helpers' +import { waitForSentinel } from '../ssh-relay-sentinel' import { probeRelayEndpointIncumbent } from '../ssh-relay-endpoint-incumbent' import { isWindowsRemoteHost } from '../ssh-remote-platform' import { LegacyRelayClient } from './legacy-relay-client' diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts index e77d7223dba..e039dfc38dc 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from '../ssh-relay-exec-command' import { describe, expect, it, vi } from 'vitest' import type { SshConnection } from '../ssh-connection' import { relaySocketNameForInstanceId } from '../ssh-relay-instance-id' @@ -5,7 +6,10 @@ import { windowsRelayPipePathsForSocketName } from '../ssh-relay-endpoints' import { getRemoteHostPlatform, joinRemotePath } from '../ssh-remote-platform' const { execCommand } = vi.hoisted(() => ({ execCommand: vi.fn() })) -vi.mock('../ssh-relay-deploy-helpers', () => ({ execCommand })) +vi.mock('../ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand +})) vi.mock('../ssh-remote-platform-detection', () => ({ detectRemoteHostPlatform: async () => getRemoteHostPlatform('win32-x64') })) diff --git a/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.ts b/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.ts index 51de63cad5a..97d2681677e 100644 --- a/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.ts +++ b/src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.ts @@ -9,7 +9,7 @@ import type { SshConnection } from '../ssh-connection' import { shellEscape } from '../ssh-connection-utils' import { RELAY_REMOTE_DIR } from '../relay-protocol' import { SHORT_RELAY_SOCKET_DIR_PREFIX } from '../relay-socket-path-limit' -import { execCommand } from '../ssh-relay-deploy-helpers' +import { execCommand } from '../ssh-relay-exec-command' import { readRelayDaemonRuntimes } from '../ssh-relay-endpoint-runtime' import { relaySocketNameForInstanceId } from '../ssh-relay-instance-id' import { readRemoteHomeCommand } from '../ssh-remote-commands' diff --git a/src/main/ssh/orcad-activation-crash-recovery.test.ts b/src/main/ssh/orcad-activation-crash-recovery.test.ts index 45e0ac2c4b9..72c64e1da97 100644 --- a/src/main/ssh/orcad-activation-crash-recovery.test.ts +++ b/src/main/ssh/orcad-activation-crash-recovery.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as RecordFile from './orcad-remote-record-file' import type * as InstallLock from './ssh-relay-install-lock' import type * as VersionedInstall from './remote-versioned-install' @@ -11,8 +11,8 @@ vi.mock('node:crypto', async (importOriginal) => { return { ...actual, randomUUID: () => uuid.fixed ?? actual.randomUUID() } }) -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ @@ -38,7 +38,7 @@ vi.mock('./orcad-local-build-hash', () => ({ computeLocalOrcadBuildHash: () => 'abc123def4567890' })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock } from './ssh-relay-install-lock' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' import { deployOrcad } from './orcad-remote-deploy' diff --git a/src/main/ssh/orcad-activation-fence-generation.test.ts b/src/main/ssh/orcad-activation-fence-generation.test.ts index 36a04f9bd4d..192de60060e 100644 --- a/src/main/ssh/orcad-activation-fence-generation.test.ts +++ b/src/main/ssh/orcad-activation-fence-generation.test.ts @@ -11,14 +11,14 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' // Every remote command runs in a real local shell, so the host-side checks are the real ones. const shell = vi.hoisted((): { env: NodeJS.ProcessEnv | undefined } => ({ env: undefined })) -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => { +vi.mock('./ssh-relay-exec-command', async (importOriginal) => { const { runProcess } = await import('@orca/process-host') return { - ...(await importOriginal()), + ...(await importOriginal()), execCommand: async (_conn: unknown, command: string) => { const result = await runProcess({ program: '/bin/sh', args: ['-c', command], env: shell.env }) if (result.code !== 0) { diff --git a/src/main/ssh/orcad-activation-lock.ts b/src/main/ssh/orcad-activation-lock.ts index 392d497a574..216a4ebf15e 100644 --- a/src/main/ssh/orcad-activation-lock.ts +++ b/src/main/ssh/orcad-activation-lock.ts @@ -12,7 +12,7 @@ import { withoutAbortSignal, type OrcadRemoteExecTarget } from './orcad-remote-runtime-control' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { acquireInstallLock, RemoteInstallLockBusyError } from './ssh-relay-install-lock' import { orphanInstallLockCommand, diff --git a/src/main/ssh/orcad-activation-transaction.test.ts b/src/main/ssh/orcad-activation-transaction.test.ts index 519bb328da5..4e5446107ce 100644 --- a/src/main/ssh/orcad-activation-transaction.test.ts +++ b/src/main/ssh/orcad-activation-transaction.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as InstallLock from './ssh-relay-install-lock' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn().mockResolvedValue('') })) vi.mock('./ssh-relay-install-lock', async (importOriginal) => ({ @@ -11,7 +11,7 @@ vi.mock('./ssh-relay-install-lock', async (importOriginal) => ({ acquireInstallLock: vi.fn().mockResolvedValue(undefined) })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock, RemoteInstallLockBusyError } from './ssh-relay-install-lock' import { parseOrcadActivationTransaction, diff --git a/src/main/ssh/orcad-candidate-launch-verdict.ts b/src/main/ssh/orcad-candidate-launch-verdict.ts index 69d1430937b..9a50691a544 100644 --- a/src/main/ssh/orcad-candidate-launch-verdict.ts +++ b/src/main/ssh/orcad-candidate-launch-verdict.ts @@ -5,7 +5,7 @@ import { launchOrcadAndAwaitReadiness, type OrcadRemoteExecTarget } from './orcad-remote-runtime-control' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' type CandidateLaunchTarget = Parameters[0] & OrcadRemoteExecTarget & { diff --git a/src/main/ssh/orcad-decommission-stop.ts b/src/main/ssh/orcad-decommission-stop.ts index 69b3e85e01d..ff1f0613762 100644 --- a/src/main/ssh/orcad-decommission-stop.ts +++ b/src/main/ssh/orcad-decommission-stop.ts @@ -5,7 +5,7 @@ * lost answer is `unverifiable`, never exit; cancelling is how a stop that did not finish is * withdrawn, and only an orcad-confirmed cancellation lets the fence go. */ -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { cancelRemoteOrcadManagedStop, completeRemoteOrcadManagedStop diff --git a/src/main/ssh/orcad-deployment-target.test.ts b/src/main/ssh/orcad-deployment-target.test.ts index cdf54d438e0..cfe60b88bb0 100644 --- a/src/main/ssh/orcad-deployment-target.test.ts +++ b/src/main/ssh/orcad-deployment-target.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import { parseGlibcVersion, @@ -6,10 +7,13 @@ import { } from './orcad-deployment-target' import { SshConnection } from './ssh-connection' import { createCallbacks, createTarget } from './ssh-connection-test-fixtures' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) async function resolveOrcadDeploymentTarget( options: Parameters[0] diff --git a/src/main/ssh/orcad-deployment-target.ts b/src/main/ssh/orcad-deployment-target.ts index d57ac712ca9..1aa2c874c82 100644 --- a/src/main/ssh/orcad-deployment-target.ts +++ b/src/main/ssh/orcad-deployment-target.ts @@ -7,7 +7,7 @@ import { } from '../../shared/node-runtime-pin' import { ORCAD_SERVER_TARGET_FILENAME } from '../../shared/orcad-artifacts' import type { RemoteInstallTransport } from './remote-install-transport' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import type { RemoteHostPlatform } from './ssh-remote-platform' /** The host answered the libc probe, but with nothing this client recognises. */ diff --git a/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts b/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts index 0a2f7ecad01..e55f5f3d7ac 100644 --- a/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts +++ b/src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts @@ -11,13 +11,13 @@ import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { runProcess, spawnProcess } from '@orca/process-host' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' // The proof's remote commands run in a real local shell. -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => { +vi.mock('./ssh-relay-exec-command', async (importOriginal) => { const { runProcess: run } = await import('@orca/process-host') return { - ...(await importOriginal()), + ...(await importOriginal()), execCommand: async (_conn: unknown, command: string) => (await run({ program: '/bin/sh', args: ['-c', command] })).stdout } diff --git a/src/main/ssh/orcad-exited-own-fence.test.ts b/src/main/ssh/orcad-exited-own-fence.test.ts index 65a4b1c8d19..e994de9bf2d 100644 --- a/src/main/ssh/orcad-exited-own-fence.test.ts +++ b/src/main/ssh/orcad-exited-own-fence.test.ts @@ -11,15 +11,15 @@ import { import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' // Every remote command runs in a real local shell, so the host-side checks are the real ones. const shell = vi.hoisted((): { env: NodeJS.ProcessEnv | undefined } => ({ env: undefined })) -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => { +vi.mock('./ssh-relay-exec-command', async (importOriginal) => { const { runProcess } = await import('@orca/process-host') const { sshCommandExitError } = await import('./ssh-relay-exec-command') return { - ...(await importOriginal()), + ...(await importOriginal()), execCommand: async (_conn: unknown, command: string) => { const result = await runProcess({ program: '/bin/sh', args: ['-c', command], env: shell.env }) if (result.code !== 0) { diff --git a/src/main/ssh/orcad-exited-own-install-lock.test.ts b/src/main/ssh/orcad-exited-own-install-lock.test.ts index 8c4f9815c38..7b1d7bbd1dd 100644 --- a/src/main/ssh/orcad-exited-own-install-lock.test.ts +++ b/src/main/ssh/orcad-exited-own-install-lock.test.ts @@ -11,14 +11,14 @@ import { import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' // Every remote command runs in a real local shell, so the host-side checks are the real ones. -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => { +vi.mock('./ssh-relay-exec-command', async (importOriginal) => { const { runProcess } = await import('@orca/process-host') const { sshCommandExitError } = await import('./ssh-relay-exec-command') return { - ...(await importOriginal()), + ...(await importOriginal()), execCommand: async (_conn: unknown, command: string) => { const result = await runProcess({ program: '/bin/sh', args: ['-c', command] }) if (result.code !== 0) { diff --git a/src/main/ssh/orcad-exited-own-lock-windows.test.ts b/src/main/ssh/orcad-exited-own-lock-windows.test.ts index 1643ae9cd79..e61da3dc3d2 100644 --- a/src/main/ssh/orcad-exited-own-lock-windows.test.ts +++ b/src/main/ssh/orcad-exited-own-lock-windows.test.ts @@ -2,15 +2,15 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { hostname, tmpdir, uptime } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' // The host script's own checks are covered against the real script; here only the client's part. const remote = vi.hoisted((): { commands: string[]; reply: string } => ({ commands: [], reply: '' })) -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: async (_conn: unknown, command: string) => { remote.commands.push(command) return command.includes('script-present') ? 'ORCAD_HOST_SCRIPT_PRESENT\n' : remote.reply diff --git a/src/main/ssh/orcad-installed-activation.ts b/src/main/ssh/orcad-installed-activation.ts index 0aa236fa468..4d105bebd4f 100644 --- a/src/main/ssh/orcad-installed-activation.ts +++ b/src/main/ssh/orcad-installed-activation.ts @@ -8,7 +8,7 @@ import { orcadRemoteBaseDir } from './orcad-remote-windows-node' import { randomUUID } from 'node:crypto' import type { OrcadDeployOptions, OrcadDeployResult } from './orcad-remote-deploy' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { withActivatedVersion, type OrcadActivationRecord, diff --git a/src/main/ssh/orcad-managed-remote-stop.ts b/src/main/ssh/orcad-managed-remote-stop.ts index de0b5c4f437..9e9fe969169 100644 --- a/src/main/ssh/orcad-managed-remote-stop.ts +++ b/src/main/ssh/orcad-managed-remote-stop.ts @@ -16,7 +16,7 @@ import { execOrcadRemote } from './orcad-remote-runtime-control' import { readBoundedOrcadRemoteRecord } from './orcad-remote-record-file' import { orcadSlotDir, type OrcadSlotOptions } from './orcad-recovery-slot' import { isWindowsRemoteHost, joinRemotePath } from './ssh-remote-platform' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { orcadWindowsNodeCommandLine, orcadWindowsSlotOpCommand, diff --git a/src/main/ssh/orcad-managed-wake.test.ts b/src/main/ssh/orcad-managed-wake.test.ts index 446ddbe2b3a..cba8bc9cc25 100644 --- a/src/main/ssh/orcad-managed-wake.test.ts +++ b/src/main/ssh/orcad-managed-wake.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as InstallLock from './ssh-relay-install-lock' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) @@ -12,7 +12,7 @@ vi.mock('./ssh-relay-install-lock', async (importOriginal) => ({ acquireInstallLock: vi.fn() })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock, RemoteInstallLockBusyError } from './ssh-relay-install-lock' import { wakeStoppedManagedOrcad } from './orcad-managed-wake' import { getRemoteHostPlatform } from './ssh-remote-platform' diff --git a/src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts b/src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts index 8b83dc66d47..86816361e3c 100644 --- a/src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts +++ b/src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts @@ -1,6 +1,7 @@ // Astra pass 4 §2: desktop A leases nothing, but desktop B's live shell, under another target id on // the same account, must never read as exited. COMPAT(legacy-relay-bridge): the host converts anyway // and leaves B's relay alone, so the census reads `bridged` with nothing of A's to serve. +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from './ssh-connection' import { relaySocketNameForInstanceId } from './ssh-relay-instance-id' @@ -19,7 +20,10 @@ const { countRelayPtysOverBridge: vi.fn() })) -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand +})) vi.mock('./ssh-relay-endpoint-incumbent', async (importOriginal) => ({ ...(await importOriginal>()), probeRelayEndpointIncumbent diff --git a/src/main/ssh/orcad-remote-decommission.test.ts b/src/main/ssh/orcad-remote-decommission.test.ts index 280f18a1331..469a4e8d2ff 100644 --- a/src/main/ssh/orcad-remote-decommission.test.ts +++ b/src/main/ssh/orcad-remote-decommission.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as RecordFile from './orcad-remote-record-file' import type * as InstallLock from './ssh-relay-install-lock' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) @@ -17,7 +17,7 @@ vi.mock('./orcad-remote-record-file', async (importOriginal) => ({ writeAtomicOrcadRemoteRecord: vi.fn() })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock } from './ssh-relay-install-lock' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' import { decommissionRemoteOrcad } from './orcad-remote-decommission' diff --git a/src/main/ssh/orcad-remote-deploy.test.ts b/src/main/ssh/orcad-remote-deploy.test.ts index a80ef08c349..c033592c47b 100644 --- a/src/main/ssh/orcad-remote-deploy.test.ts +++ b/src/main/ssh/orcad-remote-deploy.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { chmodSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs' import type * as RecordFile from './orcad-remote-record-file' import { tmpdir } from 'node:os' @@ -5,9 +6,9 @@ import { dirname, join } from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' import { runProcess } from '@orca/process-host' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) vi.mock('./ssh-relay-install-lock', () => ({ @@ -31,7 +32,7 @@ vi.mock('./orcad-local-build-hash', () => ({ computeLocalOrcadBuildHash: () => 'abc123def4567890' })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock, isRelayInstallLockStale } from './ssh-relay-install-lock' import { uploadRelayDirectory } from './ssh-relay-install-transfers' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' diff --git a/src/main/ssh/orcad-remote-gc.test.ts b/src/main/ssh/orcad-remote-gc.test.ts index 60a73c1d9ae..654b6fcecb2 100644 --- a/src/main/ssh/orcad-remote-gc.test.ts +++ b/src/main/ssh/orcad-remote-gc.test.ts @@ -1,8 +1,9 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) vi.mock('./ssh-relay-gc-claim', () => ({ @@ -20,7 +21,7 @@ vi.mock('./ssh-relay-install-lock', () => ({ isRelayInstallLockStale: vi.fn().mockResolvedValue(false) })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { gcOldOrcadVersions } from './orcad-remote-gc' import { emptyOrcadActivationRecord } from './orcad-activation-record' import { serializeOrcadActivationTransaction } from './orcad-activation-transaction' diff --git a/src/main/ssh/orcad-remote-gc.ts b/src/main/ssh/orcad-remote-gc.ts index b5fb161bca2..0d6cd9068e7 100644 --- a/src/main/ssh/orcad-remote-gc.ts +++ b/src/main/ssh/orcad-remote-gc.ts @@ -14,8 +14,7 @@ * pinned too, and an unreadable journal skips the pass entirely. */ import type { OrcadHostTransport } from './orcad-host-transport' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { ORCAD_INSTALL_MODEL } from './remote-install-model' import { gcOldRemoteInstallVersions } from './remote-install-gc' import { orcadGcPinnedDirNames, type OrcadActivationRecord } from './orcad-activation-record' diff --git a/src/main/ssh/orcad-remote-install-termination.test.ts b/src/main/ssh/orcad-remote-install-termination.test.ts index 092d884e2b1..ce84152b15b 100644 --- a/src/main/ssh/orcad-remote-install-termination.test.ts +++ b/src/main/ssh/orcad-remote-install-termination.test.ts @@ -1,6 +1,10 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) vi.mock('./ssh-relay-install-lock', () => ({ acquireInstallLock: vi.fn().mockResolvedValue(undefined), RELAY_INSTALL_LOCK_NAME: '.install-lock' @@ -14,7 +18,7 @@ vi.mock('./orcad-remote-node-runtime', () => ({ })) import type { OrcadHostTransport } from './orcad-host-transport' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { uploadRelayDirectory, writeRelayFile } from './ssh-relay-install-transfers' import { installOrcadBundle } from './orcad-remote-install' import { getRemoteHostPlatform } from './ssh-remote-platform' diff --git a/src/main/ssh/orcad-remote-install.ts b/src/main/ssh/orcad-remote-install.ts index abc7a11305e..f62823d4817 100644 --- a/src/main/ssh/orcad-remote-install.ts +++ b/src/main/ssh/orcad-remote-install.ts @@ -3,8 +3,7 @@ import { orcadNodePtyNativeArtifacts, orcadRipgrepArtifact } from '../../shared/ import { orcadAgentBrowserNativeName } from '../../shared/orcad-agent-browser-name' import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin' import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { shellEscape } from './ssh-connection-utils' import type { OrcadHostTransport } from './orcad-host-transport' import { joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform' diff --git a/src/main/ssh/orcad-remote-lifecycle-windows.test.ts b/src/main/ssh/orcad-remote-lifecycle-windows.test.ts index b2ca209f452..6c82fc093ad 100644 --- a/src/main/ssh/orcad-remote-lifecycle-windows.test.ts +++ b/src/main/ssh/orcad-remote-lifecycle-windows.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as TerminalBarrier from './orcad-rollback-terminal-barrier' /** * The deploy and rollback drivers end to end against a Windows host whose every host op is answered by a @@ -6,9 +7,9 @@ import type * as TerminalBarrier from './orcad-rollback-terminal-barrier' import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as RecordFile from './orcad-remote-record-file' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) vi.mock('./ssh-relay-install-lock', () => ({ acquireInstallLock: vi.fn().mockResolvedValue(undefined), @@ -49,7 +50,7 @@ vi.mock('./remote-versioned-install', async (importOriginal) => ({ abandonInstall: vi.fn().mockResolvedValue(undefined) })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { deployOrcad, type OrcadDeployOptions } from './orcad-remote-deploy' import { rollbackOrcad } from './orcad-remote-rollback' import { emptyOrcadActivationRecord } from './orcad-activation-record' diff --git a/src/main/ssh/orcad-remote-log-tail.test.ts b/src/main/ssh/orcad-remote-log-tail.test.ts index 4c0fcc2770d..c7ce7155870 100644 --- a/src/main/ssh/orcad-remote-log-tail.test.ts +++ b/src/main/ssh/orcad-remote-log-tail.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -14,7 +15,10 @@ import { ORCAD_WINDOWS_HOST_SCRIPT } from './orcad-windows-host-script' import { getRemoteHostPlatform } from './ssh-remote-platform' const execCommand = vi.hoisted(() => vi.fn()) -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand +})) const linux = getRemoteHostPlatform('linux-x64') const windows = getRemoteHostPlatform('win32-x64') diff --git a/src/main/ssh/orcad-remote-log-tail.ts b/src/main/ssh/orcad-remote-log-tail.ts index f62dc910cd7..1329231a2cb 100644 --- a/src/main/ssh/orcad-remote-log-tail.ts +++ b/src/main/ssh/orcad-remote-log-tail.ts @@ -6,7 +6,7 @@ import { redactString } from '../observability/redactor' import { shellEscape } from './ssh-connection-utils' import { ORCAD_LOG_FILENAME } from './orcad-remote-host-support' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform' import { orcadWindowsSlotOpCommand, diff --git a/src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts b/src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts index 50e7004eeef..942c493f083 100644 --- a/src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts +++ b/src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -10,10 +11,13 @@ import { REMOTE_NODE_RUNTIME_READY } from './orcad-remote-node-runtime' import { RUNTIME_STORE_LOCK_NAME } from './remote-node-runtime-store-lock' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) vi.mock('./ssh-relay-install-transfers', () => ({ uploadRelayDirectory: vi.fn(async () => {}) })) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand. diff --git a/src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts b/src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts index 3677c1cb729..35b16e5c291 100644 --- a/src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts +++ b/src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -13,11 +14,14 @@ import { } from './orcad-remote-node-runtime' import { REMOTE_NODE_RUNTIME_VERIFIED_MARKER } from './orcad-remote-node-runtime-report' import { RUNTIME_STORE_LOCK_NAME, withRuntimeStoreLock } from './remote-node-runtime-store-lock' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { uploadRelayDirectory } from './ssh-relay-install-transfers' import { getRemoteHostPlatform } from './ssh-remote-platform' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) vi.mock('./ssh-relay-install-transfers', () => ({ uploadRelayDirectory: vi.fn() })) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell. diff --git a/src/main/ssh/orcad-remote-node-runtime-windows.test.ts b/src/main/ssh/orcad-remote-node-runtime-windows.test.ts index 007c6d649ce..ae3e8609128 100644 --- a/src/main/ssh/orcad-remote-node-runtime-windows.test.ts +++ b/src/main/ssh/orcad-remote-node-runtime-windows.test.ts @@ -1,8 +1,9 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import path from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' import { NODE_RUNTIME_ASSETS, NODE_RUNTIME_PIN } from '../../shared/node-runtime-pin' import type { OrcadHostTransport } from './orcad-host-transport' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { uploadRelayDirectory } from './ssh-relay-install-transfers' import { ensureRemoteOrcadNodeRuntime, @@ -22,7 +23,10 @@ import { import { getRemoteHostPlatform } from './ssh-remote-platform' import { decodeRemotePowerShellScript } from './ssh-remote-powershell' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) vi.mock('./ssh-relay-install-transfers', () => ({ uploadRelayDirectory: vi.fn() })) const host = getRemoteHostPlatform('win32-x64') diff --git a/src/main/ssh/orcad-remote-node-runtime.ts b/src/main/ssh/orcad-remote-node-runtime.ts index 7e020e0f12b..b791ba6e259 100644 --- a/src/main/ssh/orcad-remote-node-runtime.ts +++ b/src/main/ssh/orcad-remote-node-runtime.ts @@ -24,8 +24,7 @@ import { } from '../../shared/orcad-artifacts' import type { OrcadHostTransport } from './orcad-host-transport' import { shellEscape } from './ssh-connection-utils' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { uploadRelayDirectory } from './ssh-relay-install-transfers' import { isWindowsRemoteHost, diff --git a/src/main/ssh/orcad-remote-preflight.ts b/src/main/ssh/orcad-remote-preflight.ts index 5c0bb973e39..a62dad47334 100644 --- a/src/main/ssh/orcad-remote-preflight.ts +++ b/src/main/ssh/orcad-remote-preflight.ts @@ -15,7 +15,7 @@ import { import { readWindowsOrcadSlotEntry } from './orcad-remote-launch-windows' import { ORCAD_WINDOWS_RUNTIME_MARKER } from './orcad-windows-host-script' import { orcadNodeSlotRuntimeCommand, selectOrcadSlotEntryCommand } from './orcad-remote-runtime' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { shellEscape } from './ssh-connection-utils' import { isWindowsRemoteHost, joinRemotePath, type RemoteHostPlatform } from './ssh-remote-platform' import type { OrcadHostTransport } from './orcad-host-transport' diff --git a/src/main/ssh/orcad-remote-primitives.test.ts b/src/main/ssh/orcad-remote-primitives.test.ts index b0a51b549f2..a0412bfcefe 100644 --- a/src/main/ssh/orcad-remote-primitives.test.ts +++ b/src/main/ssh/orcad-remote-primitives.test.ts @@ -1,6 +1,8 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn(), isUnconfirmedSshCommandTermination: (error: unknown) => error instanceof Error && error.message === 'channel lost' @@ -9,7 +11,7 @@ vi.mock('./ssh-relay-deploy-helpers', () => ({ vi.mock('./ssh-remote-platform-detection', () => ({ detectRemoteHostPlatform: vi.fn() })) vi.mock('./orcad-windows-host-preparation', () => ({ prepareWindowsOrcadHost: vi.fn() })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { detectRemoteHostPlatform } from './ssh-remote-platform-detection' import { prepareWindowsOrcadHost } from './orcad-windows-host-preparation' import { resolveOrcadRemoteContext } from './orcad-remote-context' diff --git a/src/main/ssh/orcad-remote-record-file.ts b/src/main/ssh/orcad-remote-record-file.ts index 810917fb658..caaf8c14c16 100644 --- a/src/main/ssh/orcad-remote-record-file.ts +++ b/src/main/ssh/orcad-remote-record-file.ts @@ -8,7 +8,7 @@ import { randomUUID } from 'node:crypto' import { shellEscape } from './ssh-connection-utils' import { removeRemoteFileCommand } from './ssh-remote-commands' import { execOrcadRemote, type OrcadRemoteExecTarget } from './orcad-remote-runtime-control' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { isWindowsRemoteHost } from './ssh-remote-platform' import { orcadRemoteBaseDir, diff --git a/src/main/ssh/orcad-remote-rollback.test.ts b/src/main/ssh/orcad-remote-rollback.test.ts index 2cda52e8c0e..b9662fb3c52 100644 --- a/src/main/ssh/orcad-remote-rollback.test.ts +++ b/src/main/ssh/orcad-remote-rollback.test.ts @@ -1,11 +1,12 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as RecordFile from './orcad-remote-record-file' import type * as InstallLock from './ssh-relay-install-lock' import type * as TerminalBarrier from './orcad-rollback-terminal-barrier' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) vi.mock('./ssh-relay-install-transfers', () => ({ @@ -51,7 +52,7 @@ vi.mock('./orcad-rollback-terminal-barrier', async (importOriginal) => ({ } })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' import { rollbackOrcad, type OrcadRollbackOptions } from './orcad-remote-rollback' import { emptyOrcadActivationRecord, type OrcadActivationRecord } from './orcad-activation-record' diff --git a/src/main/ssh/orcad-remote-runtime-control.test.ts b/src/main/ssh/orcad-remote-runtime-control.test.ts index 5490db384dd..1afebadc505 100644 --- a/src/main/ssh/orcad-remote-runtime-control.test.ts +++ b/src/main/ssh/orcad-remote-runtime-control.test.ts @@ -1,12 +1,14 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn(), isUnconfirmedSshCommandTermination: (error: unknown) => error instanceof Error && error.message === 'unconfirmed' })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { launchOrcadAndAwaitReadiness } from './orcad-remote-runtime-control' import { getRemoteHostPlatform } from './ssh-remote-platform' import type { OrcadLaunchSpec } from './orcad-remote-launch' diff --git a/src/main/ssh/orcad-remote-runtime-control.ts b/src/main/ssh/orcad-remote-runtime-control.ts index 7994eadf005..ace8c8cec8e 100644 --- a/src/main/ssh/orcad-remote-runtime-control.ts +++ b/src/main/ssh/orcad-remote-runtime-control.ts @@ -20,7 +20,7 @@ import { } from './orcad-remote-readiness-wait' import type { OrcadHostTransport } from './orcad-host-transport' import { errorMessage } from '../../shared/error-message' -import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { currentOrcadFence, isOrcadFenceLost, diff --git a/src/main/ssh/orcad-remote-windows-commands.test.ts b/src/main/ssh/orcad-remote-windows-commands.test.ts index f52bf785ef1..c24d43e44e4 100644 --- a/src/main/ssh/orcad-remote-windows-commands.test.ts +++ b/src/main/ssh/orcad-remote-windows-commands.test.ts @@ -1,11 +1,12 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import type { OrcadHostTransport } from './orcad-host-transport' import { getRemoteHostPlatform } from './ssh-remote-platform' import { orcadLivenessProbeCommand, type OrcadLaunchSpec } from './orcad-remote-launch' diff --git a/src/main/ssh/orcad-remote-windows-decommission.test.ts b/src/main/ssh/orcad-remote-windows-decommission.test.ts index a522d6f2b0c..51f4e69064e 100644 --- a/src/main/ssh/orcad-remote-windows-decommission.test.ts +++ b/src/main/ssh/orcad-remote-windows-decommission.test.ts @@ -3,13 +3,14 @@ * a staged managed request run by the slot's node.exe, and GC screens every candidate in one * node.exe instead of one per version dir. */ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as RecordFile from './orcad-remote-record-file' import type * as InstallLock from './ssh-relay-install-lock' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) vi.mock('./ssh-relay-install-lock', async (importOriginal) => ({ ...(await importOriginal()), @@ -24,7 +25,7 @@ vi.mock('./remote-install-gc', async (importOriginal) => ({ gcOldRemoteInstallVersions: vi.fn().mockResolvedValue(undefined) })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' import { gcOldRemoteInstallVersions } from './remote-install-gc' import { decommissionRemoteOrcad } from './orcad-remote-decommission' diff --git a/src/main/ssh/orcad-remote-windows-node.ts b/src/main/ssh/orcad-remote-windows-node.ts index d74b4c762ac..af9f097de26 100644 --- a/src/main/ssh/orcad-remote-windows-node.ts +++ b/src/main/ssh/orcad-remote-windows-node.ts @@ -19,7 +19,7 @@ import { import { pinnedNodeRuntimeAsset, type NodeRuntimeTarget } from '../../shared/node-runtime-pin' import { randomUUID } from 'node:crypto' import type { OrcadHostTransport } from './orcad-host-transport' -import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { RELAY_REMOTE_DIR } from './relay-protocol' import { joinRemotePath, remoteDirname, type RemoteHostPlatform } from './ssh-remote-platform' import { powerShellLiteral } from './ssh-remote-powershell' diff --git a/src/main/ssh/orcad-rollback-transition.ts b/src/main/ssh/orcad-rollback-transition.ts index 86f3c42ad39..d11b7ee0b92 100644 --- a/src/main/ssh/orcad-rollback-transition.ts +++ b/src/main/ssh/orcad-rollback-transition.ts @@ -2,7 +2,7 @@ import { orcadRemoteBaseDir } from './orcad-remote-windows-node' import { randomUUID } from 'node:crypto' import type { OrcadRollbackOptions, OrcadRollbackResult } from './orcad-remote-rollback' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { withRolledBackVersion } from './orcad-activation-record' import { writeOrcadActivationRecord } from './orcad-activation-record-store' import { launchAndJudgeOrcadSlot } from './orcad-candidate-launch-verdict' diff --git a/src/main/ssh/orcad-runtime-target.test.ts b/src/main/ssh/orcad-runtime-target.test.ts index 242a7023aa1..94ce9f3e73a 100644 --- a/src/main/ssh/orcad-runtime-target.test.ts +++ b/src/main/ssh/orcad-runtime-target.test.ts @@ -1,11 +1,15 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import { resolveOrcadRuntimeTarget } from './orcad-runtime-target' import { SshConnection } from './ssh-connection' import { createCallbacks, createTarget } from './ssh-connection-test-fixtures' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) beforeEach(() => vi.mocked(execCommand).mockReset()) function resolveOn(platform: Parameters[0], ldd: string) { diff --git a/src/main/ssh/orcad-state-mutation-exec.ts b/src/main/ssh/orcad-state-mutation-exec.ts index a940d88029f..7a74bb1798e 100644 --- a/src/main/ssh/orcad-state-mutation-exec.ts +++ b/src/main/ssh/orcad-state-mutation-exec.ts @@ -12,7 +12,7 @@ import { ORCAD_STATE_MUTATION_DEADLINE } from './orcad-state-snapshot-members' import type { OrcadRemoteExecTarget } from './orcad-remote-runtime-control' -import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { isOrcadFenceLost, OrcadFenceLostError } from './orcad-activation-fence-scope' // Longer than the host's own deadline, so the host kills the work before the client gives up. diff --git a/src/main/ssh/orcad-state-mutation-timeout.test.ts b/src/main/ssh/orcad-state-mutation-timeout.test.ts index 51672fd3a9d..e9000e56b67 100644 --- a/src/main/ssh/orcad-state-mutation-timeout.test.ts +++ b/src/main/ssh/orcad-state-mutation-timeout.test.ts @@ -5,13 +5,13 @@ * command shape) runs beside it, as the two restores did on a real host. */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' import type * as RecordFile from './orcad-remote-record-file' import type * as InstallLock from './ssh-relay-install-lock' import type * as VersionedInstall from './remote-versioned-install' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) vi.mock('./ssh-connection-utils', () => ({ shellEscape: (s: string) => `'${s}'` })) @@ -33,14 +33,17 @@ vi.mock('./orcad-local-build-hash', () => ({ computeLocalOrcadBuildHash: () => 'abc123def4567890' })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { + execCommand, + SSH_EXEC_TIMEOUT_CODE, + isUnconfirmedSshCommandTermination +} from './ssh-relay-exec-command' import { acquireInstallLock } from './ssh-relay-install-lock' import { writeAtomicOrcadRemoteRecord } from './orcad-remote-record-file' import { deployOrcad } from './orcad-remote-deploy' import { rollbackOrcad } from './orcad-remote-rollback' import { recoverInterruptedOrcadActivation } from './orcad-activation-recovery' import { getRemoteHostPlatform } from './ssh-remote-platform' -import { SSH_EXEC_TIMEOUT_CODE, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import type { OrcadHostTransport } from './orcad-host-transport' import { BUILD_HASH, FakeOrcadHost, NEW, OLD } from './orcad-activation-host-test-harness' import { isSnapshotCaptureCommand } from './orcad-snapshot-capture-command' diff --git a/src/main/ssh/remote-install-gc-termination.test.ts b/src/main/ssh/remote-install-gc-termination.test.ts index 812b6bc6257..e1be22505f6 100644 --- a/src/main/ssh/remote-install-gc-termination.test.ts +++ b/src/main/ssh/remote-install-gc-termination.test.ts @@ -1,15 +1,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) import type { SshConnection } from './ssh-connection' import { gcOldRemoteInstallVersions } from './remote-install-gc' import { ORCAD_INSTALL_MODEL } from './remote-install-model' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock. diff --git a/src/main/ssh/remote-install-gc-tombstone.ts b/src/main/ssh/remote-install-gc-tombstone.ts index 33d015feb3f..7e37c1c2d72 100644 --- a/src/main/ssh/remote-install-gc-tombstone.ts +++ b/src/main/ssh/remote-install-gc-tombstone.ts @@ -1,6 +1,5 @@ import type { RemoteInstallTransport } from './remote-install-transport' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { remoteInstallGcTombstoneRegex, type RemoteInstallModel } from './remote-install-model' import { removeRemoteTreeCommand } from './ssh-remote-commands' import { diff --git a/src/main/ssh/remote-node-runtime-store-gc.test.ts b/src/main/ssh/remote-node-runtime-store-gc.test.ts index 9877fa9a7b3..d200871a582 100644 --- a/src/main/ssh/remote-node-runtime-store-gc.test.ts +++ b/src/main/ssh/remote-node-runtime-store-gc.test.ts @@ -13,10 +13,10 @@ import { once } from 'node:events' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) @@ -34,7 +34,7 @@ import { runtimeStoreInventoryCommand, type RuntimeStoreInventory } from './remote-node-runtime-store-inventory' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: all connection access is replaced by execCommand's mock. diff --git a/src/main/ssh/remote-node-runtime-store-gc.ts b/src/main/ssh/remote-node-runtime-store-gc.ts index 79ccd61a6fc..c4056f67102 100644 --- a/src/main/ssh/remote-node-runtime-store-gc.ts +++ b/src/main/ssh/remote-node-runtime-store-gc.ts @@ -28,9 +28,8 @@ import { tryWithRuntimeStoreLock } from './remote-node-runtime-store-lock' import { shellEscape } from './ssh-connection-utils' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { INSTALL_LOCK_STALE_MS } from './ssh-relay-install-lock' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { moveRemoteTreeCommand, removeRemoteTreeCommand, diff --git a/src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts b/src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts index 69f3e5b3997..a9ca83613aa 100644 --- a/src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts +++ b/src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -9,11 +10,14 @@ import { RUNTIME_STORE_LOCK_OWNER_FILENAME, withRuntimeStoreLock } from './remote-node-runtime-store-lock' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { tryCreateInstallLockCommand } from './ssh-relay-install-lock-commands' import { getRemoteHostPlatform } from './ssh-remote-platform' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote call goes through the mocked execCommand, which runs it in a local shell. const conn = {} as SshConnection diff --git a/src/main/ssh/remote-node-runtime-store-windows.test.ts b/src/main/ssh/remote-node-runtime-store-windows.test.ts index ba43516f9c1..d73cbd54b97 100644 --- a/src/main/ssh/remote-node-runtime-store-windows.test.ts +++ b/src/main/ssh/remote-node-runtime-store-windows.test.ts @@ -3,10 +3,10 @@ import { copyFileSync, mkdirSync, mkdtempSync, utimesSync, writeFileSync } from import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) @@ -18,7 +18,7 @@ import { windowsRuntimeStoreInventoryCommand, windowsSweepStaleRuntimeStagesCommand } from './remote-node-runtime-store-windows' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { getRemoteHostPlatform } from './ssh-remote-platform' import { decodeRemotePowerShellScript } from './ssh-remote-powershell' import { removeTreeSync } from '../../shared/windows-transient-lock-removal' diff --git a/src/main/ssh/remote-versioned-install.test.ts b/src/main/ssh/remote-versioned-install.test.ts index 1b7953246e9..4484554a05b 100644 --- a/src/main/ssh/remote-versioned-install.test.ts +++ b/src/main/ssh/remote-versioned-install.test.ts @@ -1,13 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as DeployHelpers from './ssh-relay-deploy-helpers' +import type * as ExecCommandModule from './ssh-relay-exec-command' vi.mock('fs', () => ({ existsSync: vi.fn(), readFileSync: vi.fn() })) -vi.mock('./ssh-relay-deploy-helpers', async (importOriginal) => ({ - ...(await importOriginal()), +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn() })) @@ -24,7 +24,7 @@ import { } from './remote-versioned-install' import { gcOldRemoteInstallVersions } from './remote-install-gc' import { ORCAD_INSTALL_MODEL } from './remote-install-model' -import { isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { isUnconfirmedSshCommandTermination, execCommand } from './ssh-relay-exec-command' import { acquireInstallLock } from './ssh-relay-install-lock' import { isRelayGcClaimed, @@ -33,7 +33,6 @@ import { tryAcquireRelayGcClaim, waitForRelayGcClaimRelease } from './ssh-relay-gc-claim' -import { execCommand } from './ssh-relay-deploy-helpers' import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' import type { SshConnection } from './ssh-connection' diff --git a/src/main/ssh/ssh-connection-file-transfers.ts b/src/main/ssh/ssh-connection-file-transfers.ts index 7fb52784a8e..0382fa0495a 100644 --- a/src/main/ssh/ssh-connection-file-transfers.ts +++ b/src/main/ssh/ssh-connection-file-transfers.ts @@ -42,7 +42,7 @@ export async function uploadSshDirectory( ): Promise { await withLinkedSftpTransfer(host, remoteDir, options, { sftp: async (sftp, targetDir, signal) => { - const { uploadDirectory } = await import('./ssh-relay-deploy-helpers') + const { uploadDirectory } = await import('./sftp-upload') await uploadDirectory(sftp, localDir, targetDir, localDir, { signal }) }, system: (signal) => diff --git a/src/main/ssh/ssh-host-relay-endpoint-census.test.ts b/src/main/ssh/ssh-host-relay-endpoint-census.test.ts index e1f05ef81ba..17df84f8d3a 100644 --- a/src/main/ssh/ssh-host-relay-endpoint-census.test.ts +++ b/src/main/ssh/ssh-host-relay-endpoint-census.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from './ssh-connection' import { @@ -12,7 +13,10 @@ const { execCommand, probeRelayEndpointIncumbent, countRelayEndpointPtys } = vi. countRelayEndpointPtys: vi.fn() })) -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand +})) vi.mock('./ssh-relay-endpoint-pty-count', () => ({ countRelayEndpointPtys })) vi.mock('./ssh-relay-endpoint-incumbent', async (importOriginal) => ({ ...(await importOriginal>()), diff --git a/src/main/ssh/ssh-host-relay-endpoint-census.ts b/src/main/ssh/ssh-host-relay-endpoint-census.ts index ff4de224960..e76e7d836c4 100644 --- a/src/main/ssh/ssh-host-relay-endpoint-census.ts +++ b/src/main/ssh/ssh-host-relay-endpoint-census.ts @@ -10,7 +10,7 @@ import type { SshConnection } from './ssh-connection' import { shellEscape } from './ssh-connection-utils' import { RELAY_REMOTE_DIR } from './relay-protocol' import { SHORT_RELAY_SOCKET_DIR_PREFIX } from './relay-socket-path-limit' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { isReapableRelayHusk, probeRelayEndpointIncumbent, diff --git a/src/main/ssh/ssh-host-relay-terminals-on-connect.ts b/src/main/ssh/ssh-host-relay-terminals-on-connect.ts index 9ed1efe0efe..4627fea67a1 100644 --- a/src/main/ssh/ssh-host-relay-terminals-on-connect.ts +++ b/src/main/ssh/ssh-host-relay-terminals-on-connect.ts @@ -10,7 +10,7 @@ import { } from './ssh-host-relay-endpoint-census' import type { SshConnection } from './ssh-connection' import { censusWindowsHostRelays, windowsCensusNodePath } from './ssh-host-relay-windows-census' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { readRemoteHomeCommand } from './ssh-remote-commands' import { resolveRemoteNodePath } from './ssh-remote-node-resolution' import { detectRemoteHostPlatform } from './ssh-remote-platform-detection' diff --git a/src/main/ssh/ssh-host-relay-windows-census.test.ts b/src/main/ssh/ssh-host-relay-windows-census.test.ts index bb8daa604af..321838afb6f 100644 --- a/src/main/ssh/ssh-host-relay-windows-census.test.ts +++ b/src/main/ssh/ssh-host-relay-windows-census.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from './ssh-connection' import { relaySocketNameForInstanceId } from './ssh-relay-instance-id' @@ -9,7 +10,10 @@ const { execCommand, countRelayPtysOverBridge } = vi.hoisted(() => ({ countRelayPtysOverBridge: vi.fn() })) -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand +})) vi.mock('./ssh-relay-endpoint-pty-count', () => ({ countRelayPtysOverBridge })) vi.mock('./ssh-remote-commands', async (importOriginal) => ({ ...(await importOriginal>()), diff --git a/src/main/ssh/ssh-host-relay-windows-census.ts b/src/main/ssh/ssh-host-relay-windows-census.ts index d4645b51e95..4cf4a92f80d 100644 --- a/src/main/ssh/ssh-host-relay-windows-census.ts +++ b/src/main/ssh/ssh-host-relay-windows-census.ts @@ -19,7 +19,7 @@ import { windowsRelayInventoryCommand } from './ssh-host-relay-windows-inventory' import { countRelayPtysOverBridge } from './ssh-relay-endpoint-pty-count' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { relaySocketNameForInstanceId } from './ssh-relay-instance-id' import { windowsRelayConnectCommand } from './ssh-relay-windows-launch-command' import { listRemoteInstallBaseDirsCommand } from './ssh-remote-commands' diff --git a/src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts b/src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts index 653e030bba1..ef870210dae 100644 --- a/src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts +++ b/src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts @@ -155,7 +155,7 @@ describe('connect-decision telemetry', () => { }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'deferred', detail: reason }) diff --git a/src/main/ssh/ssh-host-server-on-connect.test.ts b/src/main/ssh/ssh-host-server-on-connect.test.ts index ab52496e2d7..dbce90ed896 100644 --- a/src/main/ssh/ssh-host-server-on-connect.test.ts +++ b/src/main/ssh/ssh-host-server-on-connect.test.ts @@ -67,7 +67,7 @@ describe('which server an SSH host runs on connect', () => { for (const verdict of ['live', 'unverifiable'] as const) { const d = deps({ relayTerminals: async () => ({ verdict, count: 2 }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: verdict === 'live' ? 'relay_terminals_live' : 'relay_terminals_unverifiable', terminals: 2 }) @@ -77,7 +77,7 @@ describe('which server an SSH host runs on connect', () => { relayTerminals: async () => ({ verdict: 'live', count: 1, elsewhere: true }) }) await expect(resolveHostServerOnConnect(target, elsewhere)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'relay_terminals_live', terminals: 1, terminalsElsewhere: true @@ -91,7 +91,7 @@ describe('which server an SSH host runs on connect', () => { }) }) await expect(resolveHostServerOnConnect(target, raced)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'relay_terminals_live' }) }) @@ -118,7 +118,7 @@ describe('which server an SSH host runs on connect', () => { }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'refused', detail: 'An automation still runs on this host.' }) @@ -132,7 +132,7 @@ describe('which server an SSH host runs on connect', () => { } }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'orcad_unavailable', detail: 'unsupported_host' }) @@ -173,7 +173,7 @@ describe('which server an SSH host runs on connect', () => { }, changed ) - ).resolves.toEqual({ route: 'relay', reason: 'source_changed' }) + ).resolves.toEqual({ route: 'unserved', reason: 'source_changed' }) expect(changed.ensureTunnel).not.toHaveBeenCalled() const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) @@ -194,7 +194,7 @@ describe('which server an SSH host runs on connect', () => { it('touches no host when this build carries no orcad template', async () => { const d = deps({ hasTemplate: () => false, isEmptyHost: () => true }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'orcad_unavailable', detail: 'artifacts_unavailable' }) @@ -209,7 +209,7 @@ describe('which server an SSH host runs on connect', () => { } }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'failed', detail: 'Connection lost' }) @@ -226,7 +226,7 @@ describe('which server an SSH host runs on connect', () => { }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'orcad_unavailable', detail: 'ssh_tunnel_unavailable' }) @@ -244,7 +244,7 @@ describe('which server an SSH host runs on connect', () => { }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toEqual({ - route: 'relay', + route: 'unserved', reason: 'orcad_unavailable', detail: 'ssh_tunnel_unavailable' }) @@ -285,7 +285,7 @@ describe('which server an SSH host runs on connect', () => { }) }) await expect(resolveHostServerOnConnect(target, d)).resolves.toMatchObject({ - route: 'relay', + route: 'unserved', reason: 'failed', detail: 'orcad_migration_source_changed' }) diff --git a/src/main/ssh/ssh-host-server-on-connect.ts b/src/main/ssh/ssh-host-server-on-connect.ts index 6de959f9908..f32e502318c 100644 --- a/src/main/ssh/ssh-host-server-on-connect.ts +++ b/src/main/ssh/ssh-host-server-on-connect.ts @@ -52,7 +52,7 @@ export type HostServerOnConnectResult = fenceHeld?: true } | { - route: 'relay' + route: 'unserved' reason: SshManagedServerRelayReason /** A blocker the user must act on, or why orcad can't run on this host. */ detail?: string @@ -129,7 +129,7 @@ async function decide( trace: HostServerDecisionTrace ): Promise { if (target.orcadFence?.sourceChangedAt) { - return { route: 'relay', reason: 'source_changed' } + return { route: 'unserved', reason: 'source_changed' } } const existing = deps.managedEnvironmentId(target) if (existing) { @@ -146,7 +146,7 @@ async function decide( await deps.releaseUnreachableSetup(target) deps.recordUnavailable(target, ORCAD_TUNNEL_UNAVAILABLE_REASON) return { - route: 'relay', + route: 'unserved', reason: 'orcad_unavailable', detail: ORCAD_TUNNEL_UNAVAILABLE_REASON } @@ -177,10 +177,10 @@ async function decide( const recorded = deps.recordedUnavailable(target) if (recorded) { trace.recorded = true - return { route: 'relay', reason: 'orcad_unavailable', detail: recorded } + return { route: 'unserved', reason: 'orcad_unavailable', detail: recorded } } if (!deps.hasTemplate()) { - return { route: 'relay', reason: 'orcad_unavailable', detail: 'artifacts_unavailable' } + return { route: 'unserved', reason: 'orcad_unavailable', detail: 'artifacts_unavailable' } } if (deps.isEmptyHost(target)) { try { @@ -207,7 +207,7 @@ async function convertHost( if (terminals && terminals.verdict !== 'exited' && !terminals.bridged) { // Why unverifiable too: loss of contact is never evidence that a relay terminal exited. return { - route: 'relay', + route: 'unserved', reason: terminalReason(terminals.verdict), terminals: terminals.count, ...(terminals.elsewhere ? { terminalsElsewhere: true } : {}) @@ -269,9 +269,9 @@ async function afterConversion( return deferred(target, deps, result, false) case 'refused': if (result.code === 'orcad_migration_terminals') { - return { route: 'relay', reason: terminalReason(result.verdict) } + return { route: 'unserved', reason: terminalReason(result.verdict) } } - return { route: 'relay', reason: 'refused', detail: result.reason } + return { route: 'unserved', reason: 'refused', detail: result.reason } } } @@ -306,10 +306,10 @@ async function unfinished( } if (!unavailable) { // Shown under the SSH host, with the host's orcad.log tail when the failure carries one. - return { route: 'relay', reason, detail: boundedDetail(failure) } + return { route: 'unserved', reason, detail: boundedDetail(failure) } } deps.recordUnavailable(target, unavailable) - return { route: 'relay', reason: 'orcad_unavailable', detail: unavailable } + return { route: 'unserved', reason: 'orcad_unavailable', detail: unavailable } } function boundedDetail(text: string): string { diff --git a/src/main/ssh/ssh-host-unserved-status.ts b/src/main/ssh/ssh-host-unserved-status.ts index feada869c7e..2991854e5e0 100644 --- a/src/main/ssh/ssh-host-unserved-status.ts +++ b/src/main/ssh/ssh-host-unserved-status.ts @@ -2,7 +2,7 @@ import type { SshManagedServerStatus } from '../../shared/ssh-types' import type { HostServerOnConnectResult } from './ssh-host-server-on-connect' -type UnservedDecision = Extract +type UnservedDecision = Extract // Why `kind: 'relay'`: the published status keeps its shipped shape; it now means "not served". export function unservedServerStatus( diff --git a/src/main/ssh/ssh-relay-endpoint-incumbent.test.ts b/src/main/ssh/ssh-relay-endpoint-incumbent.test.ts index fe109fd7834..fffd4df3c68 100644 --- a/src/main/ssh/ssh-relay-endpoint-incumbent.test.ts +++ b/src/main/ssh/ssh-relay-endpoint-incumbent.test.ts @@ -1,8 +1,10 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { describe, expect, it, vi } from 'vitest' import { RELAY_LSOF_PROBE_JS } from '@orca/process-host/posix-lsof-probe' const execCommand = vi.fn() -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: (...args: unknown[]) => execCommand(...args), isUnconfirmedSshCommandTermination: (error: unknown) => (error as { sshChannelCloseConfirmed?: boolean } | null)?.sshChannelCloseConfirmed === false diff --git a/src/main/ssh/ssh-relay-endpoint-incumbent.ts b/src/main/ssh/ssh-relay-endpoint-incumbent.ts index ee9a0e93bd1..28f47197cfc 100644 --- a/src/main/ssh/ssh-relay-endpoint-incumbent.ts +++ b/src/main/ssh/ssh-relay-endpoint-incumbent.ts @@ -26,7 +26,7 @@ import { RELAY_UNRECOGNIZED_CHILD_COUNT_VAR, relayDaemonChildCensusShell } from './relay-daemon-service-children' -import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-deploy-helpers' +import { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' import { isWindowsRemoteHost, type RemoteHostPlatform } from './ssh-remote-platform' type RelayEndpointVerdict = 'live' | 'unverifiable' | 'exited' diff --git a/src/main/ssh/ssh-relay-endpoint-pty-count.test.ts b/src/main/ssh/ssh-relay-endpoint-pty-count.test.ts index df6dceeacfb..a19b3d39038 100644 --- a/src/main/ssh/ssh-relay-endpoint-pty-count.test.ts +++ b/src/main/ssh/ssh-relay-endpoint-pty-count.test.ts @@ -7,7 +7,7 @@ const { waitForSentinel, request, dispose } = vi.hoisted(() => ({ dispose: vi.fn() })) -vi.mock('./ssh-relay-deploy-helpers', () => ({ waitForSentinel })) +vi.mock('./ssh-relay-sentinel', () => ({ waitForSentinel })) vi.mock('./ssh-channel-multiplexer', () => ({ SshChannelMultiplexer: class { request = request diff --git a/src/main/ssh/ssh-relay-endpoint-pty-count.ts b/src/main/ssh/ssh-relay-endpoint-pty-count.ts index fa7854df72b..01e162b6891 100644 --- a/src/main/ssh/ssh-relay-endpoint-pty-count.ts +++ b/src/main/ssh/ssh-relay-endpoint-pty-count.ts @@ -10,7 +10,7 @@ import type { SshConnection } from './ssh-connection' import { SshChannelMultiplexer } from './ssh-channel-multiplexer' import { shellEscape } from './ssh-connection-utils' import { SHORT_RELAY_SOCKET_DIR_PREFIX } from './relay-socket-path-limit' -import { waitForSentinel } from './ssh-relay-deploy-helpers' +import { waitForSentinel } from './ssh-relay-sentinel' const RELAY_PTY_COUNT_TIMEOUT_MS = 10_000 diff --git a/src/main/ssh/ssh-relay-endpoint-runtime.ts b/src/main/ssh/ssh-relay-endpoint-runtime.ts index 7c9a216f632..257e7a1ed6b 100644 --- a/src/main/ssh/ssh-relay-endpoint-runtime.ts +++ b/src/main/ssh/ssh-relay-endpoint-runtime.ts @@ -7,7 +7,7 @@ * host with no PATH Node from reading every relay as unverifiable. */ import type { SshConnection } from './ssh-connection' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' const RELAY_DAEMON_MARKER = ' relay.js --detached ' diff --git a/src/main/ssh/ssh-relay-deploy-helpers.test.ts b/src/main/ssh/ssh-relay-exec-command.test.ts similarity index 61% rename from src/main/ssh/ssh-relay-deploy-helpers.test.ts rename to src/main/ssh/ssh-relay-exec-command.test.ts index 41a8022f0be..f4cfebe464f 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.test.ts +++ b/src/main/ssh/ssh-relay-exec-command.test.ts @@ -1,16 +1,7 @@ import { EventEmitter } from 'node:events' import { describe, expect, it, vi } from 'vitest' import type { ClientChannel } from 'ssh2' -import { execCommand, waitForSentinel } from './ssh-relay-deploy-helpers' -import { RELAY_SENTINEL } from './relay-protocol' -import { - RelayVersionMismatchError, - RELAY_EXIT_CODE_VERSION_MISMATCH -} from './ssh-relay-version-mismatch-error' -import { - RelayCredentialMismatchError, - RELAY_EXIT_CODE_CREDENTIAL_MISMATCH -} from './ssh-relay-credential-mismatch-error' +import { execCommand } from './ssh-relay-exec-command' type MockChannel = ClientChannel & { stdin: EventEmitter & { write: ReturnType } @@ -36,229 +27,6 @@ async function execCommandRejection(promise: Promise): Promise { ) } -describe('waitForSentinel', () => { - it('closes and rejects with AbortError while clearing startup resources on abort', async () => { - vi.useFakeTimers() - try { - const channel = createMockChannel() - const controller = new AbortController() - const removeListener = vi.spyOn(controller.signal, 'removeEventListener') - const transportPromise = waitForSentinel(channel, controller.signal) - - expect(vi.getTimerCount()).toBe(1) - controller.abort() - - await expect(transportPromise).rejects.toMatchObject({ name: 'AbortError' }) - expect(channel.close).toHaveBeenCalledTimes(1) - expect(removeListener).toHaveBeenCalledWith('abort', expect.any(Function)) - expect(vi.getTimerCount()).toBe(0) - } finally { - vi.useRealTimers() - } - }) - - it('removes the abort listener and timers when startup fails before the sentinel', async () => { - vi.useFakeTimers() - try { - const channel = createMockChannel() - const controller = new AbortController() - const removeListener = vi.spyOn(controller.signal, 'removeEventListener') - const transportPromise = waitForSentinel(channel, controller.signal) - - channel.emit('error', new Error('remote host rebooted')) - - await expect(transportPromise).rejects.toThrow('remote host rebooted') - expect(removeListener).toHaveBeenCalledWith('abort', expect.any(Function)) - expect(vi.getTimerCount()).toBe(0) - } finally { - vi.useRealTimers() - } - }) - - it('buffers post-sentinel chunks until the transport subscribes', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.emit('data', Buffer.from(RELAY_SENTINEL)) - channel.emit('data', Buffer.from('first-frame-after-sentinel')) - - const transport = await transportPromise - const chunks: string[] = [] - transport.onData((chunk) => chunks.push(chunk.toString('utf-8'))) - - expect(chunks).toEqual(['first-frame-after-sentinel']) - }) - - it('buffers post-sentinel bytes from the sentinel chunk', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.emit('data', Buffer.from(`${RELAY_SENTINEL}same-chunk-frame`)) - - const transport = await transportPromise - const chunks: string[] = [] - transport.onData((chunk) => chunks.push(chunk.toString('utf-8'))) - - expect(chunks).toEqual(['same-chunk-frame']) - }) - - it('rejects on relay channel errors before the sentinel instead of emitting uncaught errors', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - expect(() => channel.emit('error', new Error('remote host rebooted'))).not.toThrow() - await expect(transportPromise).rejects.toThrow('remote host rebooted') - }) - - it('notifies transport close subscribers on relay channel errors after the sentinel', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.emit('data', Buffer.from(RELAY_SENTINEL)) - const transport = await transportPromise - const onClose = vi.fn() - transport.onClose(onClose) - - expect(() => channel.emit('error', new Error('remote host rebooted'))).not.toThrow() - expect(onClose).toHaveBeenCalledTimes(1) - }) - - it('translates a pre-sentinel exit-42 + close into RelayVersionMismatchError', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.stderr.emit( - 'data', - Buffer.from( - '[relay-connect] Handshake mismatch: expected=0.1.0+aaa, daemon=0.1.0+bbb; exiting 42\n' - ) - ) - channel.emit('exit', RELAY_EXIT_CODE_VERSION_MISMATCH) - channel.emit('close') - - await expect(transportPromise).rejects.toBeInstanceOf(RelayVersionMismatchError) - await transportPromise.catch((err: RelayVersionMismatchError) => { - expect(err.expected).toBe('0.1.0+aaa') - expect(err.got).toBe('0.1.0+bbb') - }) - }) - - it('translates a pre-sentinel exit-42 even when the version detail is missing', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.stderr.emit('data', Buffer.from('boom\n')) - channel.emit('exit', RELAY_EXIT_CODE_VERSION_MISMATCH) - channel.emit('close') - - await expect(transportPromise).rejects.toBeInstanceOf(RelayVersionMismatchError) - }) - - it('translates a pre-sentinel exit-43 + close into RelayCredentialMismatchError', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.stderr.emit( - 'data', - Buffer.from('[relay-connect] Endpoint credential refused by daemon; exiting 43\n') - ) - channel.emit('exit', RELAY_EXIT_CODE_CREDENTIAL_MISMATCH) - channel.emit('close') - - await expect(transportPromise).rejects.toBeInstanceOf(RelayCredentialMismatchError) - await transportPromise.catch((err: unknown) => { - expect(err).not.toBeInstanceOf(RelayVersionMismatchError) - }) - }) - - it('rejects with a generic error (not RelayVersionMismatchError) on a non-42 exit code', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.stderr.emit('data', Buffer.from('node: bad bytecode\n')) - channel.emit('exit', 1) - channel.emit('close') - - await expect(transportPromise).rejects.toThrow(/Relay process exited before ready/) - await transportPromise.catch((err: unknown) => { - expect(err).not.toBeInstanceOf(RelayVersionMismatchError) - }) - }) - - it('rejects and closes the channel when pre-sentinel stdout exceeds the startup buffer cap', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - - channel.emit('data', Buffer.alloc(65 * 1024, 'a')) - - const outcome = await Promise.race([ - transportPromise.then( - () => 'resolved', - (err: Error) => `rejected:${err.message}` - ), - new Promise((resolve) => setTimeout(() => resolve('pending'), 0)) - ]) - if (outcome === 'pending') { - channel.emit('close') - await transportPromise.catch(() => {}) - } - - expect(outcome).toContain('Relay startup output exceeded') - expect(channel.close).toHaveBeenCalledTimes(1) - }) - - it('does not count same-chunk post-sentinel payload against the startup buffer cap', async () => { - const channel = createMockChannel() - const transportPromise = waitForSentinel(channel) - const postSentinelPayload = Buffer.alloc(70 * 1024, 'b') - - channel.emit( - 'data', - Buffer.concat([ - Buffer.alloc(64 * 1024, 'a'), - Buffer.from(RELAY_SENTINEL), - postSentinelPayload - ]) - ) - - const transport = await transportPromise - const chunks: Buffer[] = [] - transport.onData((chunk) => chunks.push(chunk)) - - expect(Buffer.concat(chunks)).toEqual(postSentinelPayload) - expect(channel.close).not.toHaveBeenCalled() - }) - - it.each(['ssh2 channel', 'system-SSH child stdio'])( - 'forwards write(false), callback settlement, and drain for a %s', - async (channelKind) => { - const channel = createMockChannel() - if (channelKind.startsWith('system')) { - Object.assign(channel, { _process: new EventEmitter() }) - } - const callback = vi.fn() - const drain = vi.fn() - channel.stdin.write.mockImplementation((...args: unknown[]) => { - const onWritten = args.find((arg) => typeof arg === 'function') as ( - error?: Error | null - ) => void - onWritten(null) - return false - }) - const transportPromise = waitForSentinel(channel) - channel.emit('data', Buffer.from(RELAY_SENTINEL)) - const transport = await transportPromise - - transport.onDrain?.(drain) - expect(transport.write(Buffer.from('frame'), callback)).toBe(false) - expect(callback).toHaveBeenCalledWith({ ok: true }) - channel.stdin.emit('drain') - expect(drain).toHaveBeenCalledOnce() - expect(transport.supportsWriteSettlement).toBe(true) - } - ) -}) - describe('execCommand', () => { const installMarkerToken = 'a'.repeat(32) const installMarkerPath = `/home/u/.install-lock/.sftp-namespace-${installMarkerToken}` diff --git a/src/main/ssh/ssh-relay-gc-claim-termination.test.ts b/src/main/ssh/ssh-relay-gc-claim-termination.test.ts index d460e3e94b9..7e23d980f21 100644 --- a/src/main/ssh/ssh-relay-gc-claim-termination.test.ts +++ b/src/main/ssh/ssh-relay-gc-claim-termination.test.ts @@ -1,9 +1,13 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) import type { SshConnection } from './ssh-connection' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { isRelayGcClaimOwned, releaseRelayGcClaim, diff --git a/src/main/ssh/ssh-relay-host-exec.ts b/src/main/ssh/ssh-relay-host-exec.ts index 3aef869b833..96545521a28 100644 --- a/src/main/ssh/ssh-relay-host-exec.ts +++ b/src/main/ssh/ssh-relay-host-exec.ts @@ -1,5 +1,5 @@ import type { RemoteInstallTransport } from './remote-install-transport' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import type { RemoteHostPlatform } from './ssh-remote-platform' export function execHostCommand( diff --git a/src/main/ssh/ssh-relay-incumbent-process.test.ts b/src/main/ssh/ssh-relay-incumbent-process.test.ts index 6584c2b3239..d1a8e223631 100644 --- a/src/main/ssh/ssh-relay-incumbent-process.test.ts +++ b/src/main/ssh/ssh-relay-incumbent-process.test.ts @@ -1,12 +1,13 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { createServer } from 'node:net' import { describe, expect, it, vi } from 'vitest' import { runProcess } from '@orca/process-host' -vi.mock('./ssh-relay-deploy-helpers', () => ({ - execCommand: vi.fn(), - isUnconfirmedSshCommandTermination: () => false +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() })) import { relayEndpointIncumbentProbeCommand, diff --git a/src/main/ssh/ssh-relay-install-lock-termination.test.ts b/src/main/ssh/ssh-relay-install-lock-termination.test.ts index de4c57a4977..01f1b45ff45 100644 --- a/src/main/ssh/ssh-relay-install-lock-termination.test.ts +++ b/src/main/ssh/ssh-relay-install-lock-termination.test.ts @@ -1,9 +1,13 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) import type { SshConnection } from './ssh-connection' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock, isRelayInstallLockStale } from './ssh-relay-install-lock' import { getRemoteHostPlatform } from './ssh-remote-platform' diff --git a/src/main/ssh/ssh-relay-install-lock.test.ts b/src/main/ssh/ssh-relay-install-lock.test.ts index c561c1c3acb..b5ed1ad729f 100644 --- a/src/main/ssh/ssh-relay-install-lock.test.ts +++ b/src/main/ssh/ssh-relay-install-lock.test.ts @@ -1,7 +1,9 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { afterEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from './ssh-connection' -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: vi.fn(), isUnconfirmedSshCommandTermination: (error: unknown) => error instanceof Error && @@ -13,7 +15,7 @@ vi.mock('./ssh-relay-gc-claim', () => ({ waitForRelayGcClaimRelease: vi.fn().mockResolvedValue(undefined) })) -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { acquireInstallLock, RemoteInstallLockBusyError } from './ssh-relay-install-lock' import { getRemoteHostPlatform } from './ssh-remote-platform' diff --git a/src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts b/src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts index b05c8a71d92..fdc02b3568d 100644 --- a/src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts +++ b/src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events' import type { ClientChannel } from 'ssh2' import { expect, it, vi } from 'vitest' import { RELAY_SENTINEL } from './relay-protocol' -import { waitForSentinel } from './ssh-relay-deploy-helpers' +import { waitForSentinel } from './ssh-relay-sentinel' it.each([1, 256])('copies each startup prefix once across %i chunks', async (chunks) => { const channel = Object.assign(new EventEmitter(), { diff --git a/src/main/ssh/ssh-relay-sentinel.test.ts b/src/main/ssh/ssh-relay-sentinel.test.ts new file mode 100644 index 00000000000..4bd267a3e34 --- /dev/null +++ b/src/main/ssh/ssh-relay-sentinel.test.ts @@ -0,0 +1,249 @@ +import { EventEmitter } from 'node:events' +import { describe, expect, it, vi } from 'vitest' +import type { ClientChannel } from 'ssh2' +import { waitForSentinel } from './ssh-relay-sentinel' +import { RELAY_SENTINEL } from './relay-protocol' +import { + RelayVersionMismatchError, + RELAY_EXIT_CODE_VERSION_MISMATCH +} from './ssh-relay-version-mismatch-error' +import { + RelayCredentialMismatchError, + RELAY_EXIT_CODE_CREDENTIAL_MISMATCH +} from './ssh-relay-credential-mismatch-error' + +type MockChannel = ClientChannel & { + stdin: EventEmitter & { write: ReturnType } +} + +function createMockChannel(): MockChannel { + return Object.assign(new EventEmitter(), { + stderr: Object.assign(new EventEmitter(), { resume: vi.fn() }), + stdin: Object.assign(new EventEmitter(), { write: vi.fn(() => true) }), + close: vi.fn(), + resume: vi.fn() + }) as unknown as MockChannel +} + +describe('waitForSentinel', () => { + it('closes and rejects with AbortError while clearing startup resources on abort', async () => { + vi.useFakeTimers() + try { + const channel = createMockChannel() + const controller = new AbortController() + const removeListener = vi.spyOn(controller.signal, 'removeEventListener') + const transportPromise = waitForSentinel(channel, controller.signal) + + expect(vi.getTimerCount()).toBe(1) + controller.abort() + + await expect(transportPromise).rejects.toMatchObject({ name: 'AbortError' }) + expect(channel.close).toHaveBeenCalledTimes(1) + expect(removeListener).toHaveBeenCalledWith('abort', expect.any(Function)) + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + + it('removes the abort listener and timers when startup fails before the sentinel', async () => { + vi.useFakeTimers() + try { + const channel = createMockChannel() + const controller = new AbortController() + const removeListener = vi.spyOn(controller.signal, 'removeEventListener') + const transportPromise = waitForSentinel(channel, controller.signal) + + channel.emit('error', new Error('remote host rebooted')) + + await expect(transportPromise).rejects.toThrow('remote host rebooted') + expect(removeListener).toHaveBeenCalledWith('abort', expect.any(Function)) + expect(vi.getTimerCount()).toBe(0) + } finally { + vi.useRealTimers() + } + }) + + it('buffers post-sentinel chunks until the transport subscribes', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.emit('data', Buffer.from(RELAY_SENTINEL)) + channel.emit('data', Buffer.from('first-frame-after-sentinel')) + + const transport = await transportPromise + const chunks: string[] = [] + transport.onData((chunk) => chunks.push(chunk.toString('utf-8'))) + + expect(chunks).toEqual(['first-frame-after-sentinel']) + }) + + it('buffers post-sentinel bytes from the sentinel chunk', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.emit('data', Buffer.from(`${RELAY_SENTINEL}same-chunk-frame`)) + + const transport = await transportPromise + const chunks: string[] = [] + transport.onData((chunk) => chunks.push(chunk.toString('utf-8'))) + + expect(chunks).toEqual(['same-chunk-frame']) + }) + + it('rejects on relay channel errors before the sentinel instead of emitting uncaught errors', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + expect(() => channel.emit('error', new Error('remote host rebooted'))).not.toThrow() + await expect(transportPromise).rejects.toThrow('remote host rebooted') + }) + + it('notifies transport close subscribers on relay channel errors after the sentinel', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.emit('data', Buffer.from(RELAY_SENTINEL)) + const transport = await transportPromise + const onClose = vi.fn() + transport.onClose(onClose) + + expect(() => channel.emit('error', new Error('remote host rebooted'))).not.toThrow() + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('translates a pre-sentinel exit-42 + close into RelayVersionMismatchError', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.stderr.emit( + 'data', + Buffer.from( + '[relay-connect] Handshake mismatch: expected=0.1.0+aaa, daemon=0.1.0+bbb; exiting 42\n' + ) + ) + channel.emit('exit', RELAY_EXIT_CODE_VERSION_MISMATCH) + channel.emit('close') + + await expect(transportPromise).rejects.toBeInstanceOf(RelayVersionMismatchError) + await transportPromise.catch((err: RelayVersionMismatchError) => { + expect(err.expected).toBe('0.1.0+aaa') + expect(err.got).toBe('0.1.0+bbb') + }) + }) + + it('translates a pre-sentinel exit-42 even when the version detail is missing', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.stderr.emit('data', Buffer.from('boom\n')) + channel.emit('exit', RELAY_EXIT_CODE_VERSION_MISMATCH) + channel.emit('close') + + await expect(transportPromise).rejects.toBeInstanceOf(RelayVersionMismatchError) + }) + + it('translates a pre-sentinel exit-43 + close into RelayCredentialMismatchError', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.stderr.emit( + 'data', + Buffer.from('[relay-connect] Endpoint credential refused by daemon; exiting 43\n') + ) + channel.emit('exit', RELAY_EXIT_CODE_CREDENTIAL_MISMATCH) + channel.emit('close') + + await expect(transportPromise).rejects.toBeInstanceOf(RelayCredentialMismatchError) + await transportPromise.catch((err: unknown) => { + expect(err).not.toBeInstanceOf(RelayVersionMismatchError) + }) + }) + + it('rejects with a generic error (not RelayVersionMismatchError) on a non-42 exit code', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.stderr.emit('data', Buffer.from('node: bad bytecode\n')) + channel.emit('exit', 1) + channel.emit('close') + + await expect(transportPromise).rejects.toThrow(/Relay process exited before ready/) + await transportPromise.catch((err: unknown) => { + expect(err).not.toBeInstanceOf(RelayVersionMismatchError) + }) + }) + + it('rejects and closes the channel when pre-sentinel stdout exceeds the startup buffer cap', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + + channel.emit('data', Buffer.alloc(65 * 1024, 'a')) + + const outcome = await Promise.race([ + transportPromise.then( + () => 'resolved', + (err: Error) => `rejected:${err.message}` + ), + new Promise((resolve) => setTimeout(() => resolve('pending'), 0)) + ]) + if (outcome === 'pending') { + channel.emit('close') + await transportPromise.catch(() => {}) + } + + expect(outcome).toContain('Relay startup output exceeded') + expect(channel.close).toHaveBeenCalledTimes(1) + }) + + it('does not count same-chunk post-sentinel payload against the startup buffer cap', async () => { + const channel = createMockChannel() + const transportPromise = waitForSentinel(channel) + const postSentinelPayload = Buffer.alloc(70 * 1024, 'b') + + channel.emit( + 'data', + Buffer.concat([ + Buffer.alloc(64 * 1024, 'a'), + Buffer.from(RELAY_SENTINEL), + postSentinelPayload + ]) + ) + + const transport = await transportPromise + const chunks: Buffer[] = [] + transport.onData((chunk) => chunks.push(chunk)) + + expect(Buffer.concat(chunks)).toEqual(postSentinelPayload) + expect(channel.close).not.toHaveBeenCalled() + }) + + it.each(['ssh2 channel', 'system-SSH child stdio'])( + 'forwards write(false), callback settlement, and drain for a %s', + async (channelKind) => { + const channel = createMockChannel() + if (channelKind.startsWith('system')) { + Object.assign(channel, { _process: new EventEmitter() }) + } + const callback = vi.fn() + const drain = vi.fn() + channel.stdin.write.mockImplementation((...args: unknown[]) => { + const onWritten = args.find((arg) => typeof arg === 'function') as ( + error?: Error | null + ) => void + onWritten(null) + return false + }) + const transportPromise = waitForSentinel(channel) + channel.emit('data', Buffer.from(RELAY_SENTINEL)) + const transport = await transportPromise + + transport.onDrain?.(drain) + expect(transport.write(Buffer.from('frame'), callback)).toBe(false) + expect(callback).toHaveBeenCalledWith({ ok: true }) + channel.stdin.emit('drain') + expect(drain).toHaveBeenCalledOnce() + expect(transport.supportsWriteSettlement).toBe(true) + } + ) +}) diff --git a/src/main/ssh/ssh-relay-deploy-helpers.ts b/src/main/ssh/ssh-relay-sentinel.ts similarity index 96% rename from src/main/ssh/ssh-relay-deploy-helpers.ts rename to src/main/ssh/ssh-relay-sentinel.ts index dc809804127..4a6916de7ef 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.ts +++ b/src/main/ssh/ssh-relay-sentinel.ts @@ -4,11 +4,6 @@ import { RELAY_SENTINEL, RELAY_SENTINEL_TIMEOUT_MS } from './relay-protocol' import type { MultiplexerTransport } from './ssh-channel-multiplexer' import { buildRelayHandshakeRefusalError } from './ssh-relay-handshake-mismatch' -export { uploadFile, uploadDirectory, mkdirSftp } from './sftp-upload' -export { execCommand, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' - -// ── Sentinel detection ──────────────────────────────────────────────── - const MAX_RELAY_STARTUP_BUFFER_BYTES = 64 * 1024 const RELAY_SENTINEL_BUFFER = Buffer.from(RELAY_SENTINEL, 'utf-8') diff --git a/src/main/ssh/ssh-relay-versioned-install-termination.test.ts b/src/main/ssh/ssh-relay-versioned-install-termination.test.ts index 06ce0a54812..ae45d755fa7 100644 --- a/src/main/ssh/ssh-relay-versioned-install-termination.test.ts +++ b/src/main/ssh/ssh-relay-versioned-install-termination.test.ts @@ -1,10 +1,14 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn() })) +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), + execCommand: vi.fn() +})) import type { SshConnection } from './ssh-connection' import { REMOTE_INSTALL_MODELS } from './remote-install-model' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { abandonInstall, finalizeInstall, diff --git a/src/main/ssh/ssh-remote-node-install-guidance.ts b/src/main/ssh/ssh-remote-node-install-guidance.ts index de7a1094d9c..6e16b2e71d5 100644 --- a/src/main/ssh/ssh-remote-node-install-guidance.ts +++ b/src/main/ssh/ssh-remote-node-install-guidance.ts @@ -1,6 +1,6 @@ import type { SshConnection } from './ssh-connection' import { createSshOperationAbortError } from './ssh-connection-utils' -import { execCommand } from './ssh-relay-deploy-helpers' +import { execCommand } from './ssh-relay-exec-command' import { isSshSessionLimitError } from './ssh-session-limit-error' export type RemoteNodeResolutionOptions = { diff --git a/src/main/ssh/ssh-remote-node-resolution.test.ts b/src/main/ssh/ssh-remote-node-resolution.test.ts index 28202d88e37..7b7d885b5f5 100644 --- a/src/main/ssh/ssh-remote-node-resolution.test.ts +++ b/src/main/ssh/ssh-remote-node-resolution.test.ts @@ -1,3 +1,4 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { execFileSync } from 'node:child_process' import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import os from 'node:os' @@ -8,7 +9,8 @@ import { getRemoteHostPlatform } from './ssh-remote-platform' const execCommandMock = vi.hoisted(() => vi.fn()) -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: execCommandMock })) diff --git a/src/main/ssh/ssh-remote-node-resolution.ts b/src/main/ssh/ssh-remote-node-resolution.ts index a85e8acc36d..9e9e5a89276 100644 --- a/src/main/ssh/ssh-remote-node-resolution.ts +++ b/src/main/ssh/ssh-remote-node-resolution.ts @@ -7,8 +7,7 @@ import { buildPosixNodeInstallGuidance, type RemoteNodeResolutionOptions } from './ssh-remote-node-install-guidance' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isSshCommandExitError } from './ssh-relay-exec-command' +import { execCommand, isSshCommandExitError } from './ssh-relay-exec-command' import { buildPosixNodeToolchainProbe, buildWindowsNodeToolchainProbe, diff --git a/src/main/ssh/ssh-remote-platform-detection.test.ts b/src/main/ssh/ssh-remote-platform-detection.test.ts index a69f01d04b4..b39c4a0af31 100644 --- a/src/main/ssh/ssh-remote-platform-detection.test.ts +++ b/src/main/ssh/ssh-remote-platform-detection.test.ts @@ -1,10 +1,12 @@ +import type * as ExecCommandModule from './ssh-relay-exec-command' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SshConnection } from './ssh-connection' import { SSH_EXEC_TIMEOUT_CODE } from './ssh-relay-exec-command' const execCommandMock = vi.hoisted(() => vi.fn()) -vi.mock('./ssh-relay-deploy-helpers', () => ({ +vi.mock('./ssh-relay-exec-command', async (importOriginal) => ({ + ...(await importOriginal()), execCommand: execCommandMock })) diff --git a/src/main/ssh/ssh-remote-platform-detection.ts b/src/main/ssh/ssh-remote-platform-detection.ts index 572ec71b67b..93bde1e7e56 100644 --- a/src/main/ssh/ssh-remote-platform-detection.ts +++ b/src/main/ssh/ssh-remote-platform-detection.ts @@ -4,8 +4,11 @@ import { iterateProcessOutputLines } from '../../shared/process-output-field-scanner' import { parseUnameToRelayPlatform, type RelayPlatform } from './relay-protocol' -import { execCommand } from './ssh-relay-deploy-helpers' -import { isSshExecTimeout, isUnconfirmedSshCommandTermination } from './ssh-relay-exec-command' +import { + execCommand, + isSshExecTimeout, + isUnconfirmedSshCommandTermination +} from './ssh-relay-exec-command' import { isSshSessionLimitError } from './ssh-session-limit-error' import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' import { powerShellCommand } from './ssh-remote-powershell' diff --git a/src/main/ssh/system-ssh-port-forward-provider.ts b/src/main/ssh/system-ssh-port-forward-provider.ts index 3cadffbe146..c5c499ff4ac 100644 --- a/src/main/ssh/system-ssh-port-forward-provider.ts +++ b/src/main/ssh/system-ssh-port-forward-provider.ts @@ -28,7 +28,7 @@ export class SystemSshPortForwardProvider implements SshPortForwardProvider { // Why: this stderr stays attached for the forward's whole lifetime but is // only used to build the exit-error detail, so keep a bounded tail — a chatty // remote sshd could otherwise grow it unbounded on a long-lived forward - // (mirrors MAX_RELAY_STARTUP_BUFFER_BYTES in ssh-relay-deploy-helpers). + // (mirrors MAX_RELAY_STARTUP_BUFFER_BYTES in ssh-relay-sentinel). const MAX_STDERR_TAIL_BYTES = 64 * 1024 let stderr = '' const onStderr = (chunk: Buffer): void => {