Files
orca/src/main/ssh/ssh-system-fallback.test.ts
T
Neil 2ee507d744 fix(ssh): move Windows file writes off PowerShell 5.1 stdin onto sftp (#18596)
* fix(ssh): move Windows file writes off PowerShell 5.1 stdin onto sftp

#16432 was fixed by chunking writes to 32KB, on the belief that a
`DefaultShell=cmd.exe` host caps one stdin at roughly 50KB. Re-measured on
Windows 11 26200.9168 / OpenSSH_for_Windows_10.0p2, that premise is wrong in
both directions, and the chunking does not fix the hang.

The real constraint: a read on Windows PowerShell 5.1's redirected-stdin handle
over a non-pty ssh exec can die permanently when it finds the stream momentarily
empty, taking both the remaining data and the EOF with it. It is probabilistic
per such read — not a size threshold, and not certain on the first one. Measured
by swapping the copy loop for a counting reader:

  a 1.5s gap before any byte    -> 0 bytes received, 6 of 6
  1 byte, 1.5s gap, then 32767  -> exactly 1 byte
  32768, 1.5s gap, then 32768   -> exactly 32768
  a continuous 2MB              -> 167936 / 270336 / 372736

Those three 2MB figures are one payload run three times under the same
conditions, which is what rules out a threshold. Independently reproduced by a
second harness where one 1.9MB counted read completed through 39 reads and
another died after 11.

A payload that fits one burst usually presents only one read that can find the
stream empty, which is why 32KB mostly works — and it still failed 15 times in
120 under load, and 1 in 40 on a quiet host. Neither rate survives the 62 execs
a 1.9MB file needs: even 2.5% compounds to about four uploads in five failing.
No chunk size helps, because the defect is per blocking read, not per byte.
Three controls on the same host, same DefaultShell, rule out both a size limit
and cmd.exe: `findstr` took 2,016,000 bytes through one exec's stdin, sftp moved
1.9MB 5/5, and PowerShell 7 took 2MB in one exec.

Windows writes now go over the sftp subsystem, whose batch script is read by
the *local* client, so no remote process reads a pipe at all. PowerShell 7 is
the fallback where sftp is unavailable, and Windows PowerShell 5.1 is last,
still bounded, and now reports the host limitation and its remedy instead of a
bare timeout.

Measured on the same host, through this code: 1.9MB x20 all succeeded,
hash-verified, median 315ms, against 0/6 before. 32KB x120 zero hangs, against
15/120.

Also:
- Stage under a unique name per attempt. An abandoned write leaves a remote
  process that may still hold the staging file, and losing contact is not
  evidence it died (docs/reference/ssh-execution-boundary.md), so a retry must
  not reuse a name its predecessor may own. Sweep is best-effort and never
  treated as proof of anything.
- Create upload directories over sftp too; the JSON mkdir batch rode the same
  defective read.
- Cover makeWindowsWriteFileCommand and the publish command against the
  8000-char budget, which F11 flagged as untested.

* fix(ssh): replace the staged Windows write atomically, and translate ssh -l

Three review findings, all on the failure path that the success-path
measurements say nothing about.

CodeRabbit, Critical: the publish deleted the destination before moving the
staged file onto it, so a failed move destroyed the user's existing file and
left a window where a reader saw no file at all. That is worse than the
truncated partial the staging discipline exists to prevent. Now File.Replace
(Win32 ReplaceFile, atomic), falling back to a plain Move only when the
destination is absent — and that race is safe, because a destination appearing
in between makes Move throw with the staged file preserved. The exclusive
branch already had it right: Move throwing on an existing destination is the
exclusive contract. Append stays non-atomic and now says why.

buildSshArgs can emit '-l <username>' for a config alias no Host block claims,
and the translator threw on it. isSftpUnavailableError read that throw as 'this
host cannot do sftp', so those hosts fell back to the defective PowerShell 5.1
path and had the refusal cached against them for 30 minutes, silently. '-l' now
maps to '-o User=', with a test for the exact argument shape buildSshArgs
produces in that case.

CodeRabbit, minor: two assertions passed on an absent observation — an
unmatched regex yields '' and every() is true of an empty list. Both now assert
the positive form first, and the same audit was applied to the three other
some()/every() assertions in the file. The temp-file test now asserts mode 0600
rather than only that the file is cleaned up.

* fix(ssh): keep a path sftp cannot spell from becoming a verdict about the host

Audit of isSftpUnavailableError, prompted by the '-l' gap having the same
shape: a per-operation condition being written into a per-host cache that
holds for 30 minutes.

It had a second instance, and this one was mine. UnsupportedSftpPathError was
classified as 'this host cannot do sftp', but it is thrown for a UNC or
relative destination and for any path sftp's batch lexer cannot quote --
including a *local* filename containing a newline, which POSIX clients allow.
One such file would have routed every later Windows write to that host down
the defective PowerShell 5.1 path for the rest of the cache window.

The host verdict is now only the errors that really are host-scoped: a refused
subsystem, a client that will not start, and an untranslatable argument list.
A path refusal falls back for that one write and leaves the cache alone, in
both the file-write and directory-creation paths.

Revert-tested. Removing the operation-scoped catch fails all three new tests,
whether or not the predicate is also widened. Widening the predicate alone
does not fail them, correctly: with the catch in place the predicate no longer
gates that path, so keeping it narrow is defence-in-depth rather than the live
mechanism.

Flag audit at the same time: -F, -o, -T, -S, -p, -i, -J, -l and -- are now the
complete set buildSshArgs can emit, and all are handled.

* fix(ssh): make the atomic publish actually run, and unroll the mkdir batch

Two runtime defects that only a real host could surface. Both were invisible
to unit tests that assert the shape of the generated command string, because
both are PowerShell rejecting an argument at execution time.

File.Replace was passed a bare $null for destinationBackupFileName. PowerShell
coerces $null to an empty string when binding a .NET string parameter, and
Replace rejects that with 'The path is not of a legal form' -- so every
create-mode publish failed. The Critical fix was inert as shipped. Now
[NullString]::Value, which is the construct that exists for this.

Measured on awin, same staging-file lock, opposite outcomes:

  old publish  rc=1  destination MISSING          <- prior contents destroyed
  new publish  rc=1  destination PRESENT, sha 7f06b7e0... unchanged
  control, destination present, no lock  rc=0  replaced exactly
  control, destination absent, no lock   rc=0  Move fallback created it

End-to-end through the real uploader afterwards: 1.9MB x15 all hashes exact,
median 303ms; overwrite of an existing destination exact both times.

Separately, the PowerShell mkdir fallback could not create a tree of more than
one directory. '@($json | ConvertFrom-Json)' wraps the parsed array in another
array, so the loop variable binds to the whole thing and [string] of it is the
paths joined by spaces. It only ever worked for a one-element batch, where
stringifying a single-element array happens to yield the element -- which is
why no existing test caught it. Pre-existing on main; fixed here because this
PR puts that command on the fallback tier and claims the ladder works.
Both tiers now verified live against a three-directory tree.
2026-09-04 01:22:09 -07:00

949 lines
32 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { EventEmitter } from 'node:events'
import { PassThrough } from 'node:stream'
import { describe, expect, it, vi, beforeEach } from 'vitest'
const { existsSyncMock, spawnMock } = vi.hoisted(() => ({
existsSyncMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('fs', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
existsSync: existsSyncMock
}
})
vi.mock('child_process', () => ({
spawn: spawnMock
}))
import {
buildSshArgs,
downloadFileViaSystemSsh,
spawnSystemSsh,
spawnSystemSshCommand,
uploadDirectoryViaSystemSsh,
uploadFileViaSystemSsh,
writeBufferViaSystemSsh,
writeFileViaSystemSsh
} from './ssh-system-fallback'
import { spawnSystemSshPortForward } from './system-ssh-forward-process'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import type { SshTarget } from '../../shared/ssh-types'
import type { SystemSshResolvedConfig } from './ssh-control-socket'
const SYSTEM_SSH_PATH =
process.platform === 'win32' ? 'C:\\Windows\\System32\\OpenSSH\\ssh.exe' : '/usr/bin/ssh'
function decodePowerShellCommand(command: string): string {
const encoded = command.match(/-EncodedCommand\s+(\S+)/)?.[1]
return encoded ? Buffer.from(encoded, 'base64').toString('utf16le') : command
}
function mockSystemSshExists(): void {
existsSyncMock.mockImplementation((p: string) => p === SYSTEM_SSH_PATH)
}
function createTarget(overrides?: Partial<SshTarget>): SshTarget {
return {
id: 'target-1',
label: 'Test Server',
host: 'example.com',
port: 22,
username: 'deploy',
...overrides
}
}
function createResolvedConfig(
overrides?: Partial<SystemSshResolvedConfig>
): SystemSshResolvedConfig {
return {
hostname: 'example.com',
port: 22,
identityFile: [],
forwardAgent: false,
identitiesOnly: false,
proxyUseFdpass: false,
controlMaster: 'no',
controlPersist: 'no',
...overrides
}
}
function expectNoOrcaControlMasterArgs(args: string[]): void {
expect(args).not.toContain('ControlMaster=auto')
expect(args.some((arg) => arg.startsWith('ControlPath='))).toBe(false)
expect(args).not.toContain('ControlPersist=300')
}
function expectOrcaControlMasterArgs(args: string[]): void {
if (process.platform === 'win32') {
expectNoOrcaControlMasterArgs(args)
return
}
expect(args).toContain('ControlMaster=auto')
expect(args.some((arg) => arg.startsWith('ControlPath='))).toBe(true)
expect(args).toContain('ControlPersist=300')
}
type EventedProcess = EventEmitter & {
stdin: EventEmitter & {
write: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
}
stdout: EventEmitter
stderr: EventEmitter
pid: number
kill: ReturnType<typeof vi.fn>
exitCode: number | null
killed: boolean
}
// Windows writes read their source asynchronously before spawning, so a close emitted straight
// after the call can beat the listener. Emit it from the spawn instead.
function closeOnceSpawned(proc: EventedProcess): EventedProcess {
setImmediate(() => proc.emit('close', 0, null))
return proc
}
function createEventedProcess(): EventedProcess {
const proc = new EventEmitter() as EventedProcess
proc.stdin = Object.assign(new EventEmitter(), {
write: vi.fn((_chunk, _encoding, cb?: (err?: Error | null) => void) => cb?.()),
end: vi.fn()
})
proc.stdout = new EventEmitter()
proc.stderr = new EventEmitter()
proc.pid = 12345
proc.kill = vi.fn()
proc.exitCode = null
proc.killed = false
return proc
}
function createMockChildProcess(): EventEmitter & {
stdin: PassThrough
stdout: PassThrough
stderr: PassThrough
pid: number
kill: ReturnType<typeof vi.fn>
killed: boolean
exitCode: number | null
} {
const child = new EventEmitter() as EventEmitter & {
stdin: PassThrough
stdout: PassThrough
stderr: PassThrough
pid: number
kill: ReturnType<typeof vi.fn>
killed: boolean
exitCode: number | null
}
child.stdin = new PassThrough()
child.stdout = new PassThrough()
child.stderr = new PassThrough()
child.pid = 12345
child.killed = false
child.exitCode = null
child.kill = vi.fn(() => {
child.killed = true
return true
})
return child
}
describe('spawnSystemSsh', () => {
let mockProc: {
stdin: {
write: ReturnType<typeof vi.fn>
end: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
}
stdout: { on: ReturnType<typeof vi.fn> }
stderr: { on: ReturnType<typeof vi.fn> }
pid: number
on: ReturnType<typeof vi.fn>
kill: ReturnType<typeof vi.fn>
}
beforeEach(() => {
existsSyncMock.mockReset()
spawnMock.mockReset()
mockProc = {
stdin: { write: vi.fn(), end: vi.fn(), on: vi.fn() },
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
pid: 12345,
on: vi.fn(),
kill: vi.fn()
}
spawnMock.mockReturnValue(mockProc)
mockSystemSshExists()
})
it('spawns ssh with correct arguments for basic target', () => {
spawnSystemSsh(createTarget())
expect(spawnMock).toHaveBeenCalledWith(
SYSTEM_SSH_PATH,
expect.arrayContaining(['-T', 'deploy@example.com']),
expect.objectContaining({ stdio: ['pipe', 'pipe', 'pipe'] })
)
})
it('includes port flag when not 22', () => {
spawnSystemSsh(createTarget({ port: 2222 }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-p')
expect(args).toContain('2222')
})
it('does not include port flag when port is 22', () => {
spawnSystemSsh(createTarget({ port: 22 }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).not.toContain('-p')
})
it('includes identity file flag', () => {
spawnSystemSsh(createTarget({ identityFile: '/home/user/.ssh/id_ed25519' }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-i')
expect(args).toContain('/home/user/.ssh/id_ed25519')
})
it('includes identity agent option', () => {
spawnSystemSsh(createTarget({ identityAgent: '/home/user/.1password/agent.sock' }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-o')
expect(args).toContain('IdentityAgent=/home/user/.1password/agent.sock')
})
it('includes identities only option', () => {
spawnSystemSsh(createTarget({ identitiesOnly: true }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-o')
expect(args).toContain('IdentitiesOnly=yes')
})
it('includes jump host flag', () => {
spawnSystemSsh(createTarget({ jumpHost: 'bastion.example.com' }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-J')
expect(args).toContain('bastion.example.com')
})
it('includes proxy command flag', () => {
spawnSystemSsh(createTarget({ proxyCommand: 'ssh -W %h:%p bastion' }))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('-o')
expect(args).toContain('ProxyCommand=ssh -W %h:%p bastion')
})
it('uses configHost without resolved field overrides so OpenSSH sees the Host block', () => {
const args = buildSshArgs(
createTarget({
configHost: 'fdpass-host',
host: 'resolved.example.com',
port: 2222,
username: 'deploy',
identityFile: '/tmp/key',
identityAgent: '/tmp/agent.sock',
proxyCommand: 'ignored'
})
)
expect(args).toContain('fdpass-host')
expect(args).not.toContain('deploy@fdpass-host')
expect(args).not.toContain('resolved.example.com')
expect(args).not.toContain('-p')
expect(args).not.toContain('-i')
expect(args).not.toContain('IdentityAgent=/tmp/agent.sock')
expect(args).not.toContain('ProxyCommand=ignored')
})
it('states the stored endpoint when no Host block claims the alias', () => {
// A wildcard `Host *` supplies the proxy for every alias, so an alias whose own block is gone
// still reads as config-backed and gets dialled bare - the #11746 P1.
const args = buildSshArgs(
createTarget({
source: 'ssh-config',
configHost: 'prod',
host: '10.0.0.5',
port: 2222,
username: 'deploy'
}),
{ aliasClaimedByConfig: false }
)
expect(args).toContain('Hostname=10.0.0.5')
expect(args.slice(args.indexOf('-p'))).toContain('2222')
expect(args.slice(args.indexOf('-l'))).toContain('deploy')
// The alias is still the destination so OpenSSH keeps applying the wildcard's proxy.
expect(args.at(-1)).toBe('prod')
expect(args).not.toContain('deploy@prod')
expect(args).not.toContain('-i')
expect(args).not.toContain('-J')
})
it('stays a no-op when the stored endpoint matches the unclaimed alias', () => {
const args = buildSshArgs(
createTarget({ source: 'ssh-config', configHost: 'prod', host: 'prod', username: '' }),
{ aliasClaimedByConfig: false }
)
expect(args.some((arg) => arg.startsWith('Hostname='))).toBe(false)
expect(args).not.toContain('-p')
expect(args).not.toContain('-l')
expect(args.at(-1)).toBe('prod')
})
it('leaves a manual target alone even when nothing claims its alias', () => {
const args = buildSshArgs(
createTarget({ source: 'manual', configHost: 'prod', host: '10.0.0.5', port: 2222 }),
{ aliasClaimedByConfig: false }
)
expect(args.some((arg) => arg.startsWith('Hostname='))).toBe(false)
expect(args).toContain('deploy@prod')
})
it('passes an explicit main-owned OpenSSH config as one argument', () => {
const args = buildSshArgs(createTarget({ configHost: 'isolated-host', source: 'ssh-config' }), {
configFile: '/tmp/orca isolated/ssh_config'
})
expect(args.slice(0, 2)).toEqual(['-F', '/tmp/orca isolated/ssh_config'])
expect(args).toContain('isolated-host')
})
it('passes explicit options for manual targets with implicit configHost', () => {
const args = buildSshArgs(
createTarget({
source: 'manual',
configHost: '127.0.0.1',
host: '127.0.0.1',
port: 2222,
identityFile: '/tmp/orca-docker-key',
identitiesOnly: true
})
)
expect(args).toEqual(expect.arrayContaining(['-p', '2222', '-i', '/tmp/orca-docker-key']))
expect(args).toContain('IdentitiesOnly=yes')
expect(args).toContain('deploy@127.0.0.1')
})
it('requests GSSAPI authentication explicitly for manual targets', () => {
const args = buildSshArgs(
createTarget({ source: 'manual', configHost: 'krb.example.com', gssapiAuthentication: true })
)
expect(args).toContain('GSSAPIAuthentication=yes')
})
it('restricts Kerberos probes to non-interactive GSSAPI authentication', () => {
spawnSystemSshCommand(
createTarget({
configHost: 'krb-host; touch /tmp/not-run',
source: 'ssh-config',
gssapiAuthentication: true
}),
'echo ready',
{ gssapiOnly: true, wrapCommand: false }
)
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toEqual(
expect.arrayContaining([
'-o',
'BatchMode=yes',
'-o',
'GSSAPIAuthentication=yes',
'-o',
'PreferredAuthentications=gssapi-with-mic'
])
)
expect(args).not.toContain('BatchMode=no')
const standaloneControlIdx = args.indexOf('-S')
expect(standaloneControlIdx).toBeGreaterThan(-1)
expect(args[standaloneControlIdx + 1]).toBe('none')
expect(args.at(-2)).toBe('krb-host; touch /tmp/not-run')
expect(args.at(-1)).toBe('echo ready')
})
it('leaves GSSAPI to the Host block for ssh-config targets', () => {
const args = buildSshArgs(
createTarget({ configHost: 'krb-host', source: 'ssh-config', gssapiAuthentication: true })
)
expect(args).not.toContain('GSSAPIAuthentication=yes')
expect(args).toContain('krb-host')
})
it('does not inject Orca ControlMaster flags when ssh config already owns muxing', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }), {
resolvedConfig: createResolvedConfig({
controlMaster: 'auto',
controlPath: '/Users/me/.ssh/cm/%r@%h:%p',
controlPersist: '10m'
})
})
expectNoOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
expect(args).toContain('workbox')
})
it('injects Orca ControlMaster flags when ssh config only sets ControlPersist', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }), {
resolvedConfig: createResolvedConfig({
controlMaster: 'no',
controlPersist: '10m'
})
})
expectOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
})
it('injects Orca ControlMaster flags when ssh config only sets ControlPath', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }), {
resolvedConfig: createResolvedConfig({
controlMaster: 'no',
controlPath: '/Users/me/.ssh/cm/%r@%h:%p'
})
})
expectOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
})
it('injects Orca ControlMaster flags when ssh config omits ControlPath', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }), {
resolvedConfig: createResolvedConfig({
controlMaster: 'auto'
})
})
expectOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
})
it('does not inject Orca ControlMaster flags for unresolved ssh-config targets', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }))
expectNoOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
expect(args).toContain('workbox')
})
it('does not inject Orca ControlMaster flags for unresolved legacy config aliases', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', host: 'resolved.example.com' }))
expectNoOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
expect(args).toContain('workbox')
})
it('can inject Orca ControlMaster flags for ssh-config targets with resolved config', () => {
const args = buildSshArgs(createTarget({ configHost: 'workbox', source: 'ssh-config' }), {
resolvedConfig: createResolvedConfig()
})
expectOrcaControlMasterArgs(args)
expect(args).not.toContain('-S')
})
it('forces standalone SSH when target connection reuse is disabled', () => {
const args = buildSshArgs(createTarget({ systemSshConnectionReuse: false }))
const standaloneControlIdx = args.indexOf('-S')
expect(standaloneControlIdx).toBeGreaterThan(-1)
expect(args[standaloneControlIdx + 1]).toBe('none')
expectNoOrcaControlMasterArgs(args)
})
it('adds keepalive options to Orca-owned ControlMaster connections', () => {
const args = buildSshArgs(createTarget(), { resolvedConfig: createResolvedConfig() })
expectOrcaControlMasterArgs(args)
if (process.platform !== 'win32') {
expect(args).toContain('ServerAliveInterval=15')
expect(args).toContain('ServerAliveCountMax=3')
}
})
it('spawns a remote command through the system ssh target', () => {
spawnSystemSshCommand(createTarget({ configHost: 'fdpass-host' }), 'echo hello')
// Why: the remote command stays on one line so csh/tcsh login shells cannot
// split it before /bin/sh receives it.
const args = spawnMock.mock.calls[0][1] as string[]
expect(args).toContain('--')
expect(args).toContain('fdpass-host')
const wrapped = args.at(-1)!
expect(wrapped).not.toContain('\n')
expect(wrapped).toContain('printf %b "$@"')
expect(wrapped).not.toContain('base64')
expect(spawnMock).toHaveBeenCalledWith(
SYSTEM_SSH_PATH,
expect.any(Array),
expect.objectContaining({ stdio: ['pipe', 'pipe', 'pipe'] })
)
})
it('spawns port forwards before the ssh destination terminator', () => {
spawnSystemSshPortForward(createTarget({ configHost: 'fdpass-host' }), 5173, '127.0.0.1', 3000)
const args = spawnMock.mock.calls[0][1] as string[]
const terminatorIdx = args.indexOf('--')
const forwardFlagIdx = args.indexOf('-N')
const localForwardIdx = args.indexOf('-L')
const exitOnForwardFailureIdx = args.indexOf('ExitOnForwardFailure=yes')
const standaloneControlIdx = args.indexOf('-S')
expect(terminatorIdx).toBeGreaterThan(-1)
expect(forwardFlagIdx).toBeGreaterThan(-1)
expect(localForwardIdx).toBeGreaterThan(-1)
expect(exitOnForwardFailureIdx).toBeGreaterThan(-1)
// Why: -N and -L must appear before -- or OpenSSH treats them as remote command args.
expect(forwardFlagIdx).toBeLessThan(terminatorIdx)
expect(localForwardIdx).toBeLessThan(terminatorIdx)
expect(args[exitOnForwardFailureIdx - 1]).toBe('-o')
expect(exitOnForwardFailureIdx).toBeLessThan(terminatorIdx)
expect(standaloneControlIdx).toBe(-1)
expectNoOrcaControlMasterArgs(args)
expect(args).toContain('127.0.0.1:5173:127.0.0.1:3000')
expect(args[terminatorIdx + 1]).toBe('fdpass-host')
expect(spawnMock).toHaveBeenCalledWith(
SYSTEM_SSH_PATH,
expect.any(Array),
expect.objectContaining({ stdio: ['ignore', 'ignore', 'pipe'] })
)
})
it('can spawn a native remote command without the POSIX shell wrapper', () => {
spawnSystemSshCommand(createTarget({ configHost: 'fdpass-host' }), 'echo hello', {
wrapCommand: false
})
expect(spawnMock).toHaveBeenCalledWith(
SYSTEM_SSH_PATH,
expect.arrayContaining(['--', 'fdpass-host', 'echo hello']),
expect.objectContaining({ stdio: ['pipe', 'pipe', 'pipe'] })
)
})
it('exposes child stdin so remote commands receive EOF', () => {
const channel = spawnSystemSshCommand(createTarget(), 'cat > /tmp/file')
channel.stdin.end('contents')
expect(mockProc.stdin.end).toHaveBeenCalledWith('contents')
})
it('marks a system command channel when local teardown is requested', () => {
const channel = spawnSystemSshCommand(createTarget(), 'npm install')
channel.close()
expect(channel._closeRequested).toBe(true)
expect(mockProc.kill).toHaveBeenCalledWith('SIGTERM')
})
it('removes wrapped process listeners after command close', () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const channel = spawnSystemSshCommand(createTarget(), 'echo hello')
const onClose = vi.fn()
channel.on('close', onClose)
proc.emit('close', 0, null)
expect(onClose).toHaveBeenCalledWith(0, null)
expect(proc.stdout.listenerCount('data')).toBe(0)
expect(proc.stdout.listenerCount('end')).toBe(0)
expect(proc.stdout.listenerCount('error')).toBe(0)
expect(proc.stdin.listenerCount('error')).toBe(0)
expect(proc.listenerCount('exit')).toBe(0)
expect(proc.listenerCount('close')).toBe(0)
expect(proc.listenerCount('error')).toBe(0)
})
it('pauses command stdout under backpressure and resumes when the channel reads', async () => {
const proc = createMockChildProcess()
const pause = vi.spyOn(proc.stdout, 'pause')
const resume = vi.spyOn(proc.stdout, 'resume')
spawnMock.mockReturnValue(proc)
const channel = spawnSystemSshCommand(createTarget(), 'cat /tmp/large-file')
resume.mockClear()
proc.stdout.write(Buffer.alloc(128 * 1024))
expect(pause).toHaveBeenCalled()
resume.mockClear()
channel.read()
await new Promise<void>((resolve) => setImmediate(resolve))
expect(resume).toHaveBeenCalled()
})
it('removes write command wait listeners after close', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const promise = writeFileViaSystemSsh(createTarget(), '/tmp/file', 'contents')
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
expect(proc.stdin.end).toHaveBeenCalledWith(Buffer.from('contents'))
expect(proc.stderr.listenerCount('data')).toBe(0)
})
it('writes binary buffers to POSIX system SSH targets with exclusive create', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const promise = writeBufferViaSystemSsh(createTarget(), '/tmp/file', Buffer.from('png'), {
exclusive: true
})
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
const args = spawnMock.mock.calls[0][1] as string[]
expect(args.at(-1)).toContain('set -C; cat >')
expect(args.at(-1)).toContain('/tmp/file')
expect(proc.stdin.end).toHaveBeenCalledWith(Buffer.from('png'))
})
it('streams a local file through one POSIX system SSH command', async () => {
const proc = createMockChildProcess()
const received: Buffer[] = []
proc.stdin.on('data', (chunk: Buffer) => received.push(chunk))
spawnMock.mockReturnValue(proc)
const dir = mkdtempSync(join(tmpdir(), 'orca-system-ssh-upload-'))
const source = join(dir, 'payload.bin')
writeFileSync(source, Buffer.from('payload'))
try {
const promise = uploadFileViaSystemSsh(createTarget(), source, '/remote/payload.bin', {
exclusive: true
})
await new Promise<void>((resolve) => proc.stdin.once('finish', resolve))
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
expect(Buffer.concat(received)).toEqual(Buffer.from('payload'))
expect(spawnMock).toHaveBeenCalledTimes(1)
const args = spawnMock.mock.calls[0][1] as string[]
expect(args.at(-1)).toContain('set -C; cat >')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('appends binary buffers to POSIX system SSH targets', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const promise = writeBufferViaSystemSsh(createTarget(), '/tmp/file', Buffer.from('more'), {
append: true
})
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
const args = spawnMock.mock.calls[0][1] as string[]
expect(args.at(-1)).toContain('cat >>')
expect(args.at(-1)).toContain('/tmp/file')
expect(args.at(-1)).not.toContain('set -C')
})
it('downloads files from POSIX system SSH targets', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const dir = mkdtempSync(join(tmpdir(), 'orca-system-ssh-download-'))
const dest = join(dir, 'payload.bin')
try {
const promise = downloadFileViaSystemSsh(createTarget(), '/remote/payload.bin', dest)
proc.stdout.emit('data', Buffer.from('payload'))
proc.stdout.emit('end')
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
expect(readFileSync(dest)).toEqual(Buffer.from('payload'))
const args = spawnMock.mock.calls[0][1] as string[]
expect(args.at(-1)).toContain('cat')
expect(args.at(-1)).toContain('/remote/payload.bin')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('forces standalone SSH for POSIX file writes when requested', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const promise = writeFileViaSystemSsh(createTarget(), '/tmp/file', 'contents', {
disableControlMaster: true
})
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
const args = spawnMock.mock.calls[0][1] as string[]
const standaloneControlIdx = args.indexOf('-S')
expect(standaloneControlIdx).toBeGreaterThan(-1)
expect(args[standaloneControlIdx + 1]).toBe('none')
})
it('sends Windows file writes over sftp, not through a remote PowerShell stdin', async () => {
const spawned: EventedProcess[] = []
spawnMock.mockImplementation(() => {
const proc = createEventedProcess()
spawned.push(proc)
return closeOnceSpawned(proc)
})
const hostPlatform = getRemoteHostPlatform('win32-x64')
const promise = writeFileViaSystemSsh(
createTarget(),
'C:/Users/me/.orca-remote/relay/.version',
'0.1.0',
{ hostPlatform }
)
await expect(promise).resolves.toBeUndefined()
// #16432, re-measured: Windows PowerShell 5.1 can lose a redirected stdin for good when a read
// finds it momentarily empty, so the bytes must not travel that way at all.
const batch = String(spawned[0]!.stdin.end.mock.calls[0]?.[0] ?? '')
expect(batch).toContain('put ')
expect(batch).toContain('/C:/Users/me/.orca-remote/relay/.version.orca-partial-')
const sftpArgs = spawnMock.mock.calls[0][1] as string[]
expect(sftpArgs).toContain('-b')
// The rename that publishes it reads the staged file, never a pipe.
const publish = (spawnMock.mock.calls[1][1] as string[]).at(-1) ?? ''
expect(publish).toContain('powershell.exe')
expect(decodePowerShellCommand(publish)).toContain(
'[System.IO.File]::Replace($staging, $path, [NullString]::Value)'
)
expect(publish).not.toContain('/bin/sh')
})
it('enforces an exclusive Windows buffer write at the rename, where it is atomic', async () => {
spawnMock.mockImplementation(() => closeOnceSpawned(createEventedProcess()))
const hostPlatform = getRemoteHostPlatform('win32-x64')
const promise = writeBufferViaSystemSsh(
createTarget(),
'C:/Users/me/logo.png',
Buffer.from('png'),
{ hostPlatform, exclusive: true }
)
await expect(promise).resolves.toBeUndefined()
const publish = decodePowerShellCommand((spawnMock.mock.calls[1][1] as string[]).at(-1) ?? '')
// `File::Move` raising on an existing destination is what carries the exclusive contract now;
// a `CreateNew` on the staged file would only refuse a leftover of our own.
expect(publish).toContain('[System.IO.File]::Move($staging, $path)')
expect(publish).not.toContain('[System.IO.File]::Delete($path)')
})
it('downloads files from Windows system SSH targets with PowerShell stdout bytes', async () => {
const proc = createEventedProcess()
spawnMock.mockReturnValue(proc)
const hostPlatform = getRemoteHostPlatform('win32-x64')
const dir = mkdtempSync(join(tmpdir(), 'orca-system-ssh-download-'))
const dest = join(dir, 'payload.bin')
try {
const promise = downloadFileViaSystemSsh(createTarget(), 'C:/Users/me/payload.bin', dest, {
hostPlatform
})
proc.stdout.emit('data', Buffer.from('payload'))
proc.stdout.emit('end')
proc.emit('close', 0, null)
await expect(promise).resolves.toBeUndefined()
expect(readFileSync(dest)).toEqual(Buffer.from('payload'))
const args = spawnMock.mock.calls[0][1] as string[]
const remoteCommand = args.at(-1) ?? ''
expect(remoteCommand).toContain('powershell.exe')
expect(decodePowerShellCommand(remoteCommand)).toContain('OpenRead')
expect(decodePowerShellCommand(remoteCommand)).toContain('CopyTo')
expect(remoteCommand).not.toContain('/bin/sh')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('forces standalone SSH for Windows file writes when requested', async () => {
spawnMock.mockImplementation(() => closeOnceSpawned(createEventedProcess()))
const hostPlatform = getRemoteHostPlatform('win32-x64')
const promise = writeFileViaSystemSsh(
createTarget(),
'C:/Users/me/.orca-remote/relay/.version',
'0.1.0',
{ hostPlatform, disableControlMaster: true }
)
await expect(promise).resolves.toBeUndefined()
const sftpArgs = spawnMock.mock.calls[0][1] as string[]
// sftp's own `-S` names a program to run, so the same request has to be spelled as an option.
expect(sftpArgs).not.toContain('-S')
expect(sftpArgs).toContain('ControlPath=none')
const publishArgs = spawnMock.mock.calls[1][1] as string[]
const standaloneControlIdx = publishArgs.indexOf('-S')
expect(standaloneControlIdx).toBeGreaterThan(-1)
expect(publishArgs[standaloneControlIdx + 1]).toBe('none')
})
it('uploads a Windows directory as a mkdir batch plus per-file writes, never one blob', async () => {
const localDir = mkdtempSync(join(tmpdir(), 'orca-system-ssh-upload-'))
writeFileSync(join(localDir, 'relay.js'), 'console.log("relay")')
const spawned: EventedProcess[] = []
spawnMock.mockImplementation(() => {
const proc = createEventedProcess()
spawned.push(proc)
queueMicrotask(() => proc.emit('close', 0, null))
return proc
})
try {
await uploadDirectoryViaSystemSsh(
createTarget(),
localDir,
'C:/Users/me/.orca-remote/relay',
{ hostPlatform: getRemoteHostPlatform('win32-x64') }
)
} finally {
rmSync(localDir, { recursive: true, force: true })
}
// #16432: directories first, then the file — but both over sftp now, so the only PowerShell
// left is the rename that publishes the staged file, which reads a file rather than a pipe.
const mkdirBatch = String(spawned[0]!.stdin.end.mock.calls[0]?.[0] ?? '')
expect(mkdirBatch).toBe('-mkdir "/C:/Users/me/.orca-remote/relay"\n')
const putBatch = String(spawned[1]!.stdin.end.mock.calls[0]?.[0] ?? '')
expect(putBatch).toContain('put ')
expect(putBatch).toContain('/C:/Users/me/.orca-remote/relay/relay.js.orca-partial-')
const commands = spawnMock.mock.calls.map((call) => (call[1] as string[]).at(-1) ?? '')
expect(commands.every((command) => !command.includes('/bin/sh'))).toBe(true)
expect(commands.join('\n')).not.toContain('tar -xzf')
// Nothing base64s the bundle into one PowerShell string any more, and nothing reads one.
expect(
commands.some((command) => decodePowerShellCommand(command).includes('OpenStandardInput'))
).toBe(false)
})
it('forces standalone SSH for Windows upload packages when requested', async () => {
const localDir = mkdtempSync(join(tmpdir(), 'orca-system-ssh-upload-'))
writeFileSync(join(localDir, 'relay.js'), 'console.log("relay")')
spawnMock.mockImplementation(() => {
const proc = createEventedProcess()
queueMicrotask(() => proc.emit('close', 0, null))
return proc
})
try {
await uploadDirectoryViaSystemSsh(
createTarget(),
localDir,
'C:/Users/me/.orca-remote/relay',
{ hostPlatform: getRemoteHostPlatform('win32-x64'), disableControlMaster: true }
)
} finally {
rmSync(localDir, { recursive: true, force: true })
}
const args = spawnMock.mock.calls[0][1] as string[]
// The first spawn is the sftp client, whose own `-S` names a program to run.
expect(args).not.toContain('-S')
expect(args).toContain('ControlPath=none')
})
it('throws when no system ssh is found', () => {
existsSyncMock.mockReturnValue(false)
vi.stubEnv('PATH', '')
expect(() => spawnSystemSsh(createTarget())).toThrow('No system ssh binary found')
})
it('returns a process wrapper with kill and onExit', () => {
const result = spawnSystemSsh(createTarget())
expect(result.pid).toBe(12345)
expect(typeof result.kill).toBe('function')
expect(typeof result.onExit).toBe('function')
})
})
describe('system SSH operation aborts', () => {
beforeEach(() => {
existsSyncMock.mockReset()
spawnMock.mockReset()
mockSystemSshExists()
})
it('rejects directory uploads when aborted even if child processes do not close', async () => {
const tarCreate = createMockChildProcess()
const sshExtract = createMockChildProcess()
spawnMock.mockReturnValueOnce(tarCreate).mockReturnValueOnce(sshExtract)
const controller = new AbortController()
const uploadPromise = uploadDirectoryViaSystemSsh(
createTarget(),
'/tmp/local-relay',
'/tmp/remote-relay',
{ signal: controller.signal }
)
controller.abort()
const outcome = await Promise.race([
uploadPromise.then(
() => 'resolved',
(error: Error) => error.name
),
new Promise<string>((resolve) => setTimeout(() => resolve('pending'), 0))
])
expect(outcome).toBe('AbortError')
expect(tarCreate.kill).toHaveBeenCalledTimes(1)
expect(sshExtract.kill).toHaveBeenCalledTimes(1)
})
it('rejects remote file writes when aborted even if ssh never closes', async () => {
const sshProcess = createMockChildProcess()
spawnMock.mockReturnValueOnce(sshProcess)
const controller = new AbortController()
const writePromise = writeFileViaSystemSsh(createTarget(), '/tmp/remote-file', 'contents', {
signal: controller.signal
})
controller.abort()
const outcome = await Promise.race([
writePromise.then(
() => 'resolved',
(error: Error) => error.name
),
new Promise<string>((resolve) => setTimeout(() => resolve('pending'), 0))
])
expect(outcome).toBe('AbortError')
expect(sshProcess.kill).toHaveBeenCalledTimes(1)
})
})