diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 1f3e27bb475..c3eee4df250 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3332,21 +3332,38 @@ "providers": ["ssh", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["ssh"], - "coverageNotes": "A macOS unit harness loads the installed patched node-pty WindowsTerminal prototype without constructing native state, proving pre-ready kill/destroy dispatch immediately. Relay tests simulate win32 shutdown, one-shot ownership, and retry after native failure. The same relay contract serves paired web/mobile clients; no live Windows ConPTY or Windows-over-SSH run is recorded yet.", + "coverageNotes": "A macOS unit harness loads the installed patched node-pty WindowsTerminal prototype without constructing native state, proving pre-ready kill/destroy dispatch immediately. A relay artifact harness reproduces vanilla npm node-pty retaining one deferred kill with zero native close calls, then proves both Windows relay artifacts hash, ship, apply, and idempotently verify the fix. Relay tests simulate win32 shutdown, one-shot ownership, and retry after native failure. The same relay contract serves paired web/mobile clients; no live Windows ConPTY or Windows-over-SSH run is recorded yet.", "motivatingLinks": [ "https://github.com/stablyai/orca/issues/8260", "https://github.com/stablyai/orca/pull/8279" ], "invariant": "A Windows relay PTY closed before its first output executes exactly one no-argument node-pty shutdown immediately; Orca does not delete ownership for a native kill failure, and a later retry can still target the same managed PTY.", - "oracle": "Load the installed patched WindowsTerminal prototype in a pre-ready state and assert kill and destroy each close the terminal and native agent once with zero deferred work, while signal arguments still throw. In the relay, assert immediate, graceful, fallback, and handler-dispose paths issue at most one no-argument kill per request; injected native failures retain one managed PTY, and a successful retry clears it.", + "oracle": "Load the installed patched WindowsTerminal prototype in a pre-ready state and assert kill and destroy each close the terminal and native agent once with zero deferred work, while signal arguments still throw. Build both Windows SSH relay artifacts, reproduce vanilla npm node-pty with close=0/native-kill=0/deferred=1, apply the shipped patch, and assert close=1/native-kill=1/deferred=0 plus an artifact version derived from the patch bytes. Assert remote npm install executes patch enforcement before its runtime probe and never finalizes a failed patch. In the relay, assert immediate, graceful, fallback, and handler-dispose paths issue at most one no-argument kill per request; injected native failures retain one managed PTY, and a successful retry clears it.", "commands": [ - "pnpm exec vitest run --config config/vitest.config.ts src/relay/node-pty-windows-pre-ready-shutdown.test.ts src/relay/pty-handler.test.ts" + "pnpm exec vitest run --config config/vitest.config.ts config/scripts/build-relay-windows-node-pty-patch.test.mjs src/main/ssh/ssh-relay-native-deps-install.test.ts src/relay/node-pty-windows-pre-ready-shutdown.test.ts src/relay/pty-handler.test.ts" ], "testFiles": [ + "config/scripts/build-relay-windows-node-pty-patch.test.mjs", + "src/main/ssh/ssh-relay-native-deps-install.test.ts", "src/relay/node-pty-windows-pre-ready-shutdown.test.ts", "src/relay/pty-handler.test.ts" ], "assertionRefs": [ + { + "file": "config/scripts/build-relay-windows-node-pty-patch.test.mjs", + "assertions": [ + "both Windows artifacts contain the strict patcher and derive their version hash from its bytes", + "vanilla npm node-pty defers pre-ready kill, while the shipped patch executes one native close immediately and clears deferred work", + "patch application and dependency verification are idempotent" + ] + }, + { + "file": "src/main/ssh/ssh-relay-native-deps-install.test.ts", + "assertions": [ + "Windows remote install applies the shipped patch after npm and before probing node-pty", + "patch failure releases install ownership without writing the completion marker" + ] + }, { "file": "src/relay/node-pty-windows-pre-ready-shutdown.test.ts", "assertions": [ @@ -3367,10 +3384,10 @@ "date": "2026-07-12", "runner": "local", "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/relay/node-pty-windows-pre-ready-shutdown.test.ts src/relay/pty-handler.test.ts", + "command": "pnpm exec vitest run --config config/vitest.config.ts config/scripts/build-relay-windows-node-pty-patch.test.mjs src/main/ssh/ssh-relay-native-deps-install.test.ts src/relay/node-pty-windows-pre-ready-shutdown.test.ts src/relay/pty-handler.test.ts", "result": "passed", - "durationSeconds": 1, - "summary": "78 tests passed, including three installed-dependency pre-ready assertions and relay shutdown/retry call-count coverage." + "durationSeconds": 2, + "summary": "97 tests passed, including the built Windows relay artifact red/green harness, strict remote install ordering/failure/upgrade-repair handling, three installed-dependency pre-ready assertions, and relay shutdown/retry call-count coverage." } ], "runtimeBudget": { @@ -3383,7 +3400,7 @@ }, "redGreenEvidence": { "status": "partial", - "evidence": "Without the node-pty patch, the pre-ready dependency test observes one deferred callback and zero native kill calls. Without relay one-shot ownership, graceful fallback and dispose issue duplicate kills." + "evidence": "The built relay artifact harness records the vanilla npm dependency at close=0, native-kill=0, deferred=1 and the shipped patched artifact at close=1, native-kill=1, deferred=0. Without relay one-shot ownership, graceful fallback and dispose issue duplicate kills." }, "performanceBudget": { "required": true, @@ -3391,12 +3408,11 @@ }, "promotionCriteria": [ "Run the deterministic command across the required CI platforms without flakes.", - "Attach a live Windows ConPTY close-before-first-output run for local and SSH relay shutdown.", - "Record saved red/green evidence from an unpatched installed dependency." + "Attach a live Windows ConPTY close-before-first-output run for local and SSH relay shutdown." ], "knownGaps": [ - "No live Windows ConPTY, Windows SSH relay, WSL, or packaged Electron run exercises close-before-first-output.", - "The installed-dependency harness bypasses native construction and proves dispatch/ownership shape, not Windows native handle release." + "No live Windows ConPTY, Windows SSH relay, or WSL run exercises close-before-first-output.", + "The installed-dependency and packaged-relay harnesses bypass native construction and prove dispatch/ownership shape, not Windows native handle release." ], "demotionRule": "Keep non-blocking or quarantine if the dependency harness becomes platform-sensitive or flakes without a product defect." } diff --git a/config/scripts/build-relay-windows-node-pty-patch.test.mjs b/config/scripts/build-relay-windows-node-pty-patch.test.mjs new file mode 100644 index 00000000000..af4be74c1e1 --- /dev/null +++ b/config/scripts/build-relay-windows-node-pty-patch.test.mjs @@ -0,0 +1,137 @@ +import { createHash } from 'node:crypto' +import { spawnSync } from 'node:child_process' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const projectRoot = resolve(import.meta.dirname, '../..') +const require = createRequire(import.meta.url) +const patchName = 'node-pty-windows-pre-ready-shutdown-patch.cjs' +const vanillaWindowsTerminal = `'use strict'; +function WindowsTerminal() {} + WindowsTerminal.prototype.destroy = function () { + var _this = this; + this._deferNoArgs(function () { + _this.kill(); + }); + }; + WindowsTerminal.prototype.kill = function (signal) { + var _this = this; + this._deferNoArgs(function () { + if (signal) { + throw new Error('Signals not supported on windows.'); + } + _this._close(); + _this._agent.kill(); + }); + }; +WindowsTerminal.prototype._deferNoArgs = function (deferredFn) { + var _this = this; + if (this._isReady) { + deferredFn.call(this); + return; + } + this._deferreds.push({ run: function () { return deferredFn.call(_this); } }); +}; +exports.WindowsTerminal = WindowsTerminal; +` + +function loadHarness(modulePath) { + delete require.cache[require.resolve(modulePath)] + const { WindowsTerminal } = require(modulePath) + const terminal = Object.create(WindowsTerminal.prototype) + let closeCount = 0 + let nativeKillCount = 0 + terminal._isReady = false + terminal._deferreds = [] + terminal._close = () => closeCount++ + terminal._agent = { kill: () => nativeKillCount++ } + terminal.kill() + return { + closeCount, + nativeKillCount, + deferredCount: terminal._deferreds.length + } +} + +describe('packaged Windows SSH relay node-pty patch', () => { + it('hashes, ships, and executes the pre-ready shutdown fix', () => { + const build = spawnSync(process.execPath, ['config/scripts/build-relay.mjs'], { + cwd: projectRoot, + encoding: 'utf8' + }) + expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + + for (const platform of ['win32-x64', 'win32-arm64']) { + const relayDir = join(projectRoot, 'out', 'relay', platform) + const relayContent = readFileSync(join(relayDir, 'relay.js')) + const patchContent = readFileSync(join(relayDir, patchName)) + const expectedHash = createHash('sha256') + .update(relayContent) + .update(patchContent) + .digest('hex') + .slice(0, 12) + expect(readFileSync(join(relayDir, '.version'), 'utf8')).toBe(`0.1.0+${expectedHash}`) + } + + for (const platform of ['linux-x64', 'darwin-arm64']) { + expect(existsSync(join(projectRoot, 'out', 'relay', platform, patchName))).toBe(false) + } + + const packageConfig = require('../electron-builder.config.cjs') + for (const platform of ['win', 'mac', 'linux']) { + expect(packageConfig[platform].extraResources).toContainEqual({ + from: 'out/relay', + to: 'relay' + }) + } + + const tempRoot = mkdtempSync(join(tmpdir(), 'orca-relay-node-pty-')) + try { + const modulePath = join(tempRoot, 'node_modules', 'node-pty', 'lib', 'windowsTerminal.js') + const watcherPath = join(tempRoot, 'node_modules', '@parcel', 'watcher', 'index.js') + mkdirSync(dirname(modulePath), { recursive: true }) + mkdirSync(dirname(watcherPath), { recursive: true }) + writeFileSync(modulePath, vanillaWindowsTerminal, 'utf8') + writeFileSync(watcherPath, 'module.exports = {}\n', 'utf8') + + expect(loadHarness(modulePath)).toEqual({ + closeCount: 0, + nativeKillCount: 0, + deferredCount: 1 + }) + + const patch = spawnSync( + process.execPath, + [join(projectRoot, 'out', 'relay', 'win32-x64', patchName), '--apply', tempRoot], + { cwd: tempRoot, encoding: 'utf8' } + ) + expect(patch.status, patch.stderr).toBe(0) + expect(patch.stdout).toContain('ORCA-NPTY-WINDOWS-PATCH-OK') + expect(loadHarness(modulePath)).toEqual({ + closeCount: 1, + nativeKillCount: 1, + deferredCount: 0 + }) + + const secondPatch = spawnSync( + process.execPath, + [join(projectRoot, 'out', 'relay', 'win32-x64', patchName), '--apply', tempRoot], + { cwd: tempRoot, encoding: 'utf8' } + ) + expect(secondPatch.status, secondPatch.stderr).toBe(0) + + const check = spawnSync( + process.execPath, + [join(projectRoot, 'out', 'relay', 'win32-x64', patchName), '--check', tempRoot], + { cwd: tempRoot, encoding: 'utf8' } + ) + expect(check.status, check.stderr).toBe(0) + expect(check.stdout).toContain('ORCA-NATIVE-DEPS-OK') + } finally { + rmSync(tempRoot, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/build-relay.mjs b/config/scripts/build-relay.mjs index c9cabcea838..1fba1da82f2 100644 --- a/config/scripts/build-relay.mjs +++ b/config/scripts/build-relay.mjs @@ -10,13 +10,20 @@ */ import { build } from 'esbuild' import { createHash } from 'node:crypto' -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' const __dirname = import.meta.dirname // Why: the script lives under config/scripts, so go two levels up to reach the repo root. const ROOT = join(__dirname, '..', '..') const RELAY_ENTRY = join(ROOT, 'src', 'relay', 'relay.ts') +const WINDOWS_NODE_PTY_PATCH_SOURCE = join( + ROOT, + 'config', + 'scripts', + 'relay-node-pty-windows-pre-ready-shutdown-patch.cjs' +) +const WINDOWS_NODE_PTY_PATCH_NAME = 'node-pty-windows-pre-ready-shutdown-patch.cjs' const PLATFORMS = [ 'linux-x64', @@ -53,7 +60,17 @@ for (const platform of PLATFORMS) { // Why: include a content hash so the deploy check detects code changes // even when RELAY_VERSION hasn't been bumped (common during development). const relayContent = readFileSync(join(outDir, 'relay.js')) - const hash = createHash('sha256').update(relayContent).digest('hex').slice(0, 12) + const artifactHash = createHash('sha256').update(relayContent) + if (platform.startsWith('win32-')) { + // Why: SSH relays install node-pty with npm on the remote, outside pnpm's + // patchedDependencies. Hashing the patch forces safe upgrade redeployment. + const patchTarget = join(outDir, WINDOWS_NODE_PTY_PATCH_NAME) + copyFileSync(WINDOWS_NODE_PTY_PATCH_SOURCE, patchTarget) + artifactHash.update(readFileSync(patchTarget)) + } else { + rmSync(join(outDir, WINDOWS_NODE_PTY_PATCH_NAME), { force: true }) + } + const hash = artifactHash.digest('hex').slice(0, 12) writeFileSync(join(outDir, '.version'), `${RELAY_VERSION}+${hash}`) console.log(`Built relay for ${platform} → ${outDir}/relay.js`) diff --git a/config/scripts/relay-node-pty-windows-pre-ready-shutdown-patch.cjs b/config/scripts/relay-node-pty-windows-pre-ready-shutdown-patch.cjs new file mode 100644 index 00000000000..3a4a41af34c --- /dev/null +++ b/config/scripts/relay-node-pty-windows-pre-ready-shutdown-patch.cjs @@ -0,0 +1,94 @@ +const { readFileSync, renameSync, rmSync, writeFileSync } = require('node:fs') +const { join } = require('node:path') + +const PATCH_MARKER = 'Orca: Windows pre-ready shutdown is synchronous.' +const VANILLA_DESTROY = ` WindowsTerminal.prototype.destroy = function () { + var _this = this; + this._deferNoArgs(function () { + _this.kill(); + }); + };` +const PATCHED_DESTROY = ` WindowsTerminal.prototype.destroy = function () { + this.kill(); + };` +const VANILLA_KILL = ` WindowsTerminal.prototype.kill = function (signal) { + var _this = this; + this._deferNoArgs(function () { + if (signal) { + throw new Error('Signals not supported on windows.'); + } + _this._close(); + _this._agent.kill(); + }); + };` +const PATCHED_KILL = ` WindowsTerminal.prototype.kill = function (signal) { + if (signal) { + throw new Error('Signals not supported on windows.'); + } + // ${PATCH_MARKER} + this._deferreds = []; + this._close(); + this._agent.kill(); + };` + +function replaceExactlyOnce(source, before, after, label) { + const first = source.indexOf(before) + if (first < 0 || source.includes(before, first + before.length)) { + throw new Error(`node-pty@1.1.0 ${label} implementation did not match exactly once`) + } + return source.slice(0, first) + after + source.slice(first + before.length) +} + +function patchWindowsTerminal(source) { + if (source.includes(PATCH_MARKER)) { + if (!source.includes(PATCHED_DESTROY) || !source.includes(PATCHED_KILL)) { + throw new Error('node-pty Windows shutdown patch marker exists on an incomplete patch') + } + return source + } + + const withDestroy = replaceExactlyOnce(source, VANILLA_DESTROY, PATCHED_DESTROY, 'destroy()') + return replaceExactlyOnce(withDestroy, VANILLA_KILL, PATCHED_KILL, 'kill()') +} + +function applyPatch(rootDir = process.cwd()) { + const target = join(rootDir, 'node_modules', 'node-pty', 'lib', 'windowsTerminal.js') + const source = readFileSync(target, 'utf8') + const patched = patchWindowsTerminal(source) + if (patched !== source) { + const pendingTarget = `${target}.orca-patch-pending` + try { + writeFileSync(pendingTarget, patched, 'utf8') + renameSync(pendingTarget, target) + } finally { + rmSync(pendingTarget, { force: true }) + } + } + return target +} + +function checkNativeDeps(rootDir = process.cwd()) { + const target = join(rootDir, 'node_modules', 'node-pty', 'lib', 'windowsTerminal.js') + const source = readFileSync(target, 'utf8') + if (!source.includes(PATCHED_DESTROY) || !source.includes(PATCHED_KILL)) { + throw new Error('node-pty Windows pre-ready shutdown patch is missing') + } + require.resolve('@parcel/watcher', { paths: [rootDir] }) + return target +} + +if (require.main === module) { + const mode = process.argv[2] ?? '--apply' + const rootDir = process.argv[3] ?? process.cwd() + if (mode === '--check') { + checkNativeDeps(rootDir) + process.stdout.write('ORCA-NATIVE-DEPS-OK\n') + } else if (mode === '--apply') { + const target = applyPatch(rootDir) + process.stdout.write(`ORCA-NPTY-WINDOWS-PATCH-OK ${target}\n`) + } else { + throw new Error(`Unsupported node-pty patch mode: ${mode}`) + } +} + +module.exports = { PATCH_MARKER, applyPatch, checkNativeDeps, patchWindowsTerminal } diff --git a/src/main/ssh/ssh-relay-deploy.ts b/src/main/ssh/ssh-relay-deploy.ts index 26501a8371b..1df09487bdd 100644 --- a/src/main/ssh/ssh-relay-deploy.ts +++ b/src/main/ssh/ssh-relay-deploy.ts @@ -437,6 +437,7 @@ const RELAY_NATIVE_DEPS = { 'node-pty': '1.1.0', '@parcel/watcher': '2.5.6' } as const +const WINDOWS_NODE_PTY_PATCH_NAME = 'node-pty-windows-pre-ready-shutdown-patch.cjs' async function hasRequiredNativeDeps( conn: SshConnection, @@ -451,7 +452,7 @@ async function hasRequiredNativeDeps( hostPlatform, nodePath, remoteDir, - `try { & ${powerShellLiteral(nodePath)} -e ${powerShellNativeArg('require.resolve("node-pty"); require.resolve("@parcel/watcher"); console.log("ORCA-NATIVE-DEPS-OK")')} } catch { 'MISSING' }` + `& ${powerShellLiteral(nodePath)} ${powerShellLiteral(joinRemotePath(hostPlatform, remoteDir, WINDOWS_NODE_PTY_PATCH_NAME))} --check` ) : commandWithNodePath( hostPlatform, @@ -576,6 +577,27 @@ async function installNativeDeps( throw err } + if (isWindowsRemoteHost(hostPlatform)) { + const patchPath = joinRemotePath(hostPlatform, remoteDir, WINDOWS_NODE_PTY_PATCH_NAME) + try { + await execHostCommand( + conn, + hostPlatform, + commandWithNodePath( + hostPlatform, + nodePath, + remoteDir, + `& ${powerShellLiteral(nodePath)} ${powerShellLiteral(patchPath)} --apply` + ) + ) + } catch (err) { + console.warn( + `[ssh-relay][NPTY-PATCH-FAIL] failed to enforce Windows pre-ready shutdown at ${remoteDir} (${platform}): ${(err as Error).message}` + ) + throw err + } + } + // SFTP doesn't preserve execute bits; node-pty's spawn-helper prebuild // must be +x for posix_spawnp. if (!isWindowsRemoteHost(hostPlatform)) { diff --git a/src/main/ssh/ssh-relay-native-deps-install.test.ts b/src/main/ssh/ssh-relay-native-deps-install.test.ts index 4d2f92eb947..591c1d8548c 100644 --- a/src/main/ssh/ssh-relay-native-deps-install.test.ts +++ b/src/main/ssh/ssh-relay-native-deps-install.test.ts @@ -534,6 +534,7 @@ describe('installNativeDeps (via deployAndLaunchRelay)', () => { 'C:\\Users\\u', '', // mkdir remoteDir '', // npm install native deps + '', // enforce Windows node-pty shutdown patch 'MISSING\n', // native process exit normalized by PowerShell command '', // remove probe stderr file '', // no persisted active pipe marker @@ -555,12 +556,45 @@ describe('installNativeDeps (via deployAndLaunchRelay)', () => { expect(probeScript).toContain('$LASTEXITCODE -ne 0') expect(probeScript).toContain("'MISSING'") + const decodedCommands = vi + .mocked(execCommand) + .mock.calls.map(([, command]) => decodePowerShellCommand(command) ?? '') + const npmIdx = decodedCommands.findIndex((command) => command.includes('npm install')) + const patchIdx = decodedCommands.findIndex((command) => + command.includes('node-pty-windows-pre-ready-shutdown-patch.cjs') + ) + const probeIdx = decodedCommands.findIndex((command) => + command.includes('require(\\"node-pty\\")') + ) + expect(patchIdx).toBeGreaterThan(npmIdx) + expect(probeIdx).toBeGreaterThan(patchIdx) + const warnMessages = warnSpy.mock.calls.map((args) => String(args[0] ?? '')) expect(warnMessages.some((m) => m.includes('[ssh-relay][NPTY-MISSING]'))).toBe(true) expect(vi.mocked(finalizeInstall)).toHaveBeenCalledTimes(1) expect(vi.mocked(abandonInstall)).not.toHaveBeenCalled() }) + it('does not finalize a Windows relay when node-pty patch enforcement fails', async () => { + vi.mocked(parseUnameToRelayPlatform).mockReturnValueOnce('win32-x64') + vi.mocked(resolveRemoteNodePath).mockResolvedValueOnce('C:/Program Files/nodejs/node.exe') + const conn = makeMockConnection(sftpCapture) + feed([ + '__ORCA_REMOTE_PLATFORM__ Windows AMD64', + 'C:\\Users\\u', + '', // mkdir remoteDir + '', // npm install native deps + { reject: 'node-pty@1.1.0 kill() implementation did not match exactly once' } + ]) + + await expect(deployAndLaunchRelay(conn)).rejects.toThrow(/kill\(\) implementation/) + + expect(vi.mocked(finalizeInstall)).not.toHaveBeenCalled() + expect(vi.mocked(abandonInstall)).toHaveBeenCalledTimes(1) + const warnings = warnSpy.mock.calls.map((args) => String(args[0] ?? '')) + expect(warnings.some((message) => message.includes('[ssh-relay][NPTY-PATCH-FAIL]'))).toBe(true) + }) + it('includes the platform tuple in NPTY-MISSING and native install failure logs', async () => { // Platform tuple lets bug reports be triaged for prebuild availability // without asking the user to dig out their arch. @@ -629,6 +663,38 @@ describe('installNativeDeps (via deployAndLaunchRelay)', () => { ).toBe(true) }) + it('repairs an existing Windows relay whose npm node-pty is still vanilla', async () => { + vi.mocked(parseUnameToRelayPlatform).mockReturnValueOnce('win32-x64') + vi.mocked(resolveRemoteNodePath).mockResolvedValueOnce('C:/Program Files/nodejs/node.exe') + vi.mocked(isRelayAlreadyInstalled).mockResolvedValue(true) + const conn = makeMockConnection(sftpCapture) + feed([ + '__ORCA_REMOTE_PLATFORM__ Windows AMD64', + 'C:\\Users\\u', + 'MISSING', // pre-lock patch/dependency check + 'MISSING', // under-lock patch/dependency check + '', // npm install native deps + '', // enforce Windows node-pty shutdown patch + 'ORCA-NPTY-PROBE-OK\n', + '', // remove probe stderr + '', // no persisted active pipe marker + 'WAITING', + '', // WMI relay launch + 'READY', + '' // persist active pipe marker + ]) + + await deployAndLaunchRelay(conn) + + expect(vi.mocked(acquireInstallLock)).toHaveBeenCalledTimes(1) + expect(vi.mocked(finalizeInstall)).toHaveBeenCalledTimes(1) + const decodedCommands = vi + .mocked(execCommand) + .mock.calls.map(([, command]) => decodePowerShellCommand(command) ?? '') + expect(decodedCommands.filter((command) => command.includes('--check'))).toHaveLength(2) + expect(decodedCommands.filter((command) => command.includes('--apply'))).toHaveLength(1) + }) + it('does not mutate an existing relay dir when required native deps are present', async () => { vi.mocked(isRelayAlreadyInstalled).mockResolvedValue(true) const conn = makeMockConnection(sftpCapture)