cmux/cmuxTests/CLIProcessRunnerTests.swift
2026-03-16 23:57:48 -07:00

441 lines
19 KiB
Swift

import XCTest
#if canImport(cmux)
@testable import cmux
final class CLIProcessRunnerTests: XCTestCase {
private func writeExecutable(_ contents: String, to url: URL) throws {
try contents.write(to: url, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
}
func testRunProcessTimesOutHungChild() {
let startedAt = Date()
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", "sleep 5"],
timeout: 0.2
)
XCTAssertTrue(result.timedOut)
XCTAssertEqual(result.status, 124)
XCTAssertLessThan(Date().timeIntervalSince(startedAt), 2.0)
}
func testInteractiveRemoteShellCommandHonorsZDOTDIRFromRealZshenv() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-zdotdir-\(UUID().uuidString)")
let userZdotdir = home.appendingPathComponent("user-zdotdir")
let relayDir = home.appendingPathComponent(".cmux/relay")
let binDir = home.appendingPathComponent(".cmux/bin")
try fileManager.createDirectory(at: userZdotdir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: relayDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "export ZDOTDIR=\"$HOME/user-zdotdir\"\n"
.write(to: home.appendingPathComponent(".zshenv"), atomically: true, encoding: .utf8)
try """
precmd() {
print -r -- "REAL=$CMUX_REAL_ZDOTDIR ZDOTDIR=$ZDOTDIR SOCKET=$CMUX_SOCKET_PATH PATH=$PATH"
exit
}
"""
.write(to: userZdotdir.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
try "#!/bin/sh\nexit 0\n"
.write(to: binDir.appendingPathComponent("cmux"), atomically: true, encoding: .utf8)
try "".write(
to: relayDir.appendingPathComponent("64003.auth"),
atomically: true,
encoding: .utf8
)
try fileManager.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: binDir.appendingPathComponent("cmux").path
)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(remoteRelayPort: 64003, shellFeatures: "")
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("REAL=\(userZdotdir.path)"), result.stdout)
XCTAssertTrue(result.stdout.contains("SOCKET=127.0.0.1:64003"), result.stdout)
XCTAssertTrue(result.stdout.contains("PATH=\(binDir.path):"), result.stdout)
XCTAssertTrue(result.stdout.contains("ZDOTDIR=\(relayDir.appendingPathComponent("64003.shell").path)"), result.stdout)
}
func testInteractiveRemoteShellCommandKeepsDefaultZDOTDIRWithoutRecursing() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-zdotdir-default-\(UUID().uuidString)")
let relayDir = home.appendingPathComponent(".cmux/relay")
let binDir = home.appendingPathComponent(".cmux/bin")
try fileManager.createDirectory(at: relayDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "precmd() { print -r -- \"REAL=$CMUX_REAL_ZDOTDIR ZDOTDIR=$ZDOTDIR\"; exit }\n"
.write(to: home.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
try "#!/bin/sh\nexit 0\n"
.write(to: binDir.appendingPathComponent("cmux"), atomically: true, encoding: .utf8)
try "".write(
to: relayDir.appendingPathComponent("64004.auth"),
atomically: true,
encoding: .utf8
)
try fileManager.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: binDir.appendingPathComponent("cmux").path
)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(remoteRelayPort: 64004, shellFeatures: "")
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertFalse(result.stderr.contains("too many open files"), result.stderr)
XCTAssertTrue(result.stdout.contains("REAL=\(home.path)"), result.stdout)
XCTAssertTrue(result.stdout.contains("ZDOTDIR=\(relayDir.appendingPathComponent("64004.shell").path)"), result.stdout)
}
func testInteractiveRemoteShellCommandDoesNotWaitForRelayReadinessBeforeLaunchingShell() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-no-relay-wait-\(UUID().uuidString)")
try fileManager.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "precmd() { print -r -- \"READY SOCKET=$CMUX_SOCKET_PATH\"; exit }\n"
.write(to: home.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(remoteRelayPort: 64006, shellFeatures: "")
let startedAt = Date()
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 2
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("READY SOCKET=127.0.0.1:64006"), result.stdout)
XCTAssertLessThan(Date().timeIntervalSince(startedAt), 1.5, "interactive shell startup should not wait for relay readiness")
}
func testInteractiveRemoteShellCommandDefaultsToXterm256ColorWithoutPreparedGhosttyTerminfo() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-term-fallback-\(UUID().uuidString)")
try fileManager.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "precmd() { print -r -- \"TERM=$TERM\"; exit }\n"
.write(to: home.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(remoteRelayPort: 0, shellFeatures: "")
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("TERM=xterm-256color"), result.stdout)
}
func testInteractiveRemoteShellCommandSourcesZprofileBeforeLaunchingInteractiveZsh() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-zprofile-\(UUID().uuidString)")
let brewBin = home.appendingPathComponent("testbrew/bin")
try fileManager.createDirectory(at: brewBin, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "export PATH=\"$HOME/testbrew/bin:$PATH\"\n"
.write(to: home.appendingPathComponent(".zprofile"), atomically: true, encoding: .utf8)
try "precmd() { print -r -- \"PATH=$PATH\"; exit }\n"
.write(to: home.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(remoteRelayPort: 0, shellFeatures: "")
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("PATH=\(brewBin.path):"), result.stdout)
}
func testInteractiveRemoteShellCommandWithInlineTerminfoParsesAndLaunchesZsh() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-inline-terminfo-\(UUID().uuidString)")
try fileManager.createDirectory(at: home, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try "precmd() { print -r -- \"READY TERM=$TERM\"; exit }\n"
.write(to: home.appendingPathComponent(".zshrc"), atomically: true, encoding: .utf8)
let cli = CMUXCLI(args: [])
let command = cli.buildInteractiveRemoteShellCommand(
remoteRelayPort: 0,
shellFeatures: "",
terminfoSource: "xterm-ghostty|ghostty,clear=\\E[H\\E[2J"
)
let result = CLIProcessRunner.runProcess(
executablePath: "/bin/sh",
arguments: ["-c", command],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("READY TERM="), result.stdout)
XCTAssertFalse(result.stderr.contains("unexpected end of file"), result.stderr)
}
func testRemoteCLIWrapperPrefersRelaySpecificDaemonMapping() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-wrapper-\(UUID().uuidString)")
let relayDir = home.appendingPathComponent(".cmux/relay")
let binDir = home.appendingPathComponent(".cmux/bin")
let wrapperURL = binDir.appendingPathComponent("cmux")
let currentDaemonURL = binDir.appendingPathComponent("cmuxd-remote-current")
let mappedDaemonURL = binDir.appendingPathComponent("cmuxd-remote-64005")
let daemonPathURL = relayDir.appendingPathComponent("64005.daemon_path")
try fileManager.createDirectory(at: relayDir, withIntermediateDirectories: true)
try fileManager.createDirectory(at: binDir, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try writeExecutable("#!/bin/sh\necho current \"$@\"\n", to: currentDaemonURL)
try writeExecutable("#!/bin/sh\necho mapped \"$@\"\n", to: mappedDaemonURL)
try writeExecutable(Workspace.remoteCLIWrapperScript(), to: wrapperURL)
try mappedDaemonURL.path.write(to: daemonPathURL, atomically: true, encoding: .utf8)
let result = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/env",
arguments: [
"HOME=\(home.path)",
"CMUX_SOCKET_PATH=127.0.0.1:64005",
wrapperURL.path,
"ping",
],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "mapped ping")
}
func testRemoteCLIWrapperInstallScriptDoesNotClobberLegacySymlinkedDaemonTarget() throws {
let fileManager = FileManager.default
let home = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-wrapper-install-\(UUID().uuidString)")
let binDir = home.appendingPathComponent(".cmux/bin")
let daemonDir = binDir.appendingPathComponent("cmuxd-remote/0.62.1/darwin-arm64")
let daemonURL = daemonDir.appendingPathComponent("cmuxd-remote")
let currentDaemonURL = binDir.appendingPathComponent("cmuxd-remote-current")
let wrapperURL = binDir.appendingPathComponent("cmux")
try fileManager.createDirectory(at: daemonDir, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: home) }
try writeExecutable("#!/bin/sh\necho daemon \"$@\"\n", to: daemonURL)
try fileManager.createSymbolicLink(atPath: currentDaemonURL.path, withDestinationPath: daemonURL.path)
try fileManager.createSymbolicLink(atPath: wrapperURL.path, withDestinationPath: currentDaemonURL.path)
let installScript = Workspace.remoteCLIWrapperInstallScript(
daemonRemotePath: ".cmux/bin/cmuxd-remote/0.62.1/darwin-arm64/cmuxd-remote"
)
let installResult = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/env",
arguments: [
"HOME=\(home.path)",
"/bin/sh",
"-c",
installScript,
],
timeout: 5
)
XCTAssertFalse(installResult.timedOut, installResult.stderr)
XCTAssertEqual(installResult.status, 0, installResult.stderr)
XCTAssertEqual(
try String(contentsOf: daemonURL, encoding: .utf8),
"#!/bin/sh\necho daemon \"$@\"\n"
)
XCTAssertEqual(
try fileManager.destinationOfSymbolicLink(atPath: currentDaemonURL.path),
daemonURL.path
)
let wrapperAttributes = try fileManager.attributesOfItem(atPath: wrapperURL.path)
XCTAssertEqual(wrapperAttributes[.type] as? FileAttributeType, .typeRegular)
let wrapperResult = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/env",
arguments: [
"HOME=\(home.path)",
wrapperURL.path,
"serve",
"--stdio",
],
timeout: 5
)
XCTAssertFalse(wrapperResult.timedOut, wrapperResult.stderr)
XCTAssertEqual(wrapperResult.status, 0, wrapperResult.stderr)
XCTAssertEqual(wrapperResult.stdout.trimmingCharacters(in: .whitespacesAndNewlines), "daemon serve --stdio")
}
func testSSHStartupCommandBootstrapsOverRemoteCommandWithoutStealingInteractiveInput() throws {
let fileManager = FileManager.default
let tempRoot = fileManager.temporaryDirectory.appendingPathComponent("cmux-cli-ssh-pty-\(UUID().uuidString)")
let fakeBin = tempRoot.appendingPathComponent("bin")
let argvURL = tempRoot.appendingPathComponent("ssh-argv.txt")
let remoteCommandURL = tempRoot.appendingPathComponent("ssh-remote-command.txt")
try fileManager.createDirectory(at: fakeBin, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: tempRoot) }
try writeExecutable(
"""
#!/bin/sh
printf '%s\\n' "$@" > '\(argvURL.path)'
remote_command=''
while [ "$#" -gt 0 ]; do
if [ "$1" = '-o' ] && [ "$#" -ge 2 ]; then
case "$2" in
RemoteCommand=*)
remote_command=${2#RemoteCommand=}
;;
esac
shift 2
continue
fi
shift
done
printf '%s' "$remote_command" > '\(remoteCommandURL.path)'
if [ -n "$remote_command" ]; then
exec /bin/sh -lc "$remote_command"
fi
exec /bin/sh
""",
to: fakeBin.appendingPathComponent("ssh")
)
let cli = CMUXCLI(args: [])
let sshCommand = cli.buildSSHCommandText(
CMUXCLI.SSHCommandOptions(
destination: "cmux-macmini",
port: nil,
identityFile: nil,
workspaceName: nil,
sshOptions: [],
extraArguments: [],
localSocketPath: "",
remoteRelayPort: 64007
),
remoteBootstrapScript: """
printf '%s\\n' 'BOOTSTRAPPED %{255}'
exec /bin/sh
"""
)
let startupCommand = try cli.buildSSHStartupCommand(
sshCommand: sshCommand,
shellFeatures: "cursor:blink,path,title",
remoteRelayPort: 64007
)
let currentPath = ProcessInfo.processInfo.environment["PATH"] ?? "/usr/bin:/bin:/usr/sbin:/sbin"
let result = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/env",
arguments: [
"PATH=\(fakeBin.path):\(currentPath)",
"STARTUP=\(startupCommand)",
"/usr/bin/python3",
"-c",
"""
import os, pty, select, subprocess, time
startup = os.environ["STARTUP"]
env = os.environ.copy()
master, slave = pty.openpty()
proc = subprocess.Popen([startup], stdin=slave, stdout=slave, stderr=slave, env=env, close_fds=True)
os.close(slave)
time.sleep(0.4)
os.write(master, b"echo READY\\nexit\\n")
time.sleep(0.8)
out = b""
deadline = time.time() + 1.5
while time.time() < deadline:
r, _, _ = select.select([master], [], [], 0.2)
if not r:
break
try:
chunk = os.read(master, 65536)
except OSError:
break
if not chunk:
break
out += chunk
try:
proc.terminate()
except ProcessLookupError:
pass
try:
proc.wait(timeout=1)
except Exception:
proc.kill()
print(out.decode("utf-8", "replace"), end="")
""",
],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("BOOTSTRAPPED %{255}"), result.stdout)
XCTAssertTrue(result.stdout.contains("READY"), result.stdout)
let argv = try String(contentsOf: argvURL, encoding: .utf8)
XCTAssertTrue(argv.contains("RemoteCommand="), argv)
let remoteCommand = try String(contentsOf: remoteCommandURL, encoding: .utf8)
XCTAssertFalse(remoteCommand.contains("%{255}"), remoteCommand)
XCTAssertTrue(remoteCommand.contains("base64"), remoteCommand)
}
func testEncodedRemoteBootstrapCommandEscapesPercentsForSSHRemoteCommand() throws {
let cli = CMUXCLI(args: [])
let remoteCommand = cli.sshPercentEscapedRemoteCommand(
cli.encodedRemoteBootstrapCommand(
"""
printf '%s\\n' 'BOOTSTRAPPED %{255}'
exit 0
"""
)
)
let result = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/ssh",
arguments: [
"-G",
"-o",
"RemoteCommand=\(remoteCommand)",
"cmux-macmini",
],
timeout: 5
)
XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
XCTAssertTrue(result.stdout.contains("host cmux-macmini"), result.stdout)
}
}
#endif