Files
adolf/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

416 lines
16 KiB
Swift

import Darwin
import Foundation
import Testing
@testable import OpenClaw
@Suite(.serialized) struct CommandResolverTests {
private func makeDefaults() -> UserDefaults {
// Use a unique suite to avoid cross-suite concurrency on UserDefaults.standard.
UserDefaults(suiteName: "CommandResolverTests.\(UUID().uuidString)")!
}
private func makeLocalDefaults() -> UserDefaults {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.local.rawValue, forKey: connectionModeKey)
return defaults
}
private func makeProjectRootWithPnpm() throws -> (tmp: URL, pnpmPath: URL) {
let tmp = try makeTempDirForTests()
let pnpmPath = tmp.appendingPathComponent("node_modules/.bin/pnpm")
try makeExecutableForTests(at: pnpmPath)
return (tmp, pnpmPath)
}
@Test func `prefers open claw binary`() throws {
let defaults = self.makeLocalDefaults()
let tmp = try makeTempDirForTests()
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
try makeExecutableForTests(at: openclawPath)
let searchPaths = [tmp.appendingPathComponent("node_modules/.bin").path]
let cmd = CommandResolver.openclawCommand(
subcommand: "gateway",
defaults: defaults,
configRoot: [:],
searchPaths: searchPaths,
projectRoot: tmp)
#expect(cmd.prefix(2).elementsEqual([openclawPath.path, "gateway"]))
}
@Test func `falls back to node and script`() throws {
let defaults = self.makeLocalDefaults()
let tmp = try makeTempDirForTests()
let nodePath = tmp.appendingPathComponent("node_modules/.bin/node")
let scriptPath = tmp.appendingPathComponent("bin/openclaw.js")
try makeExecutableForTests(at: nodePath)
try "#!/bin/sh\necho v22.19.0\n".write(to: nodePath, atomically: true, encoding: .utf8)
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path)
try makeExecutableForTests(at: scriptPath)
let cmd = CommandResolver.openclawCommand(
subcommand: "rpc",
defaults: defaults,
configRoot: [:],
searchPaths: [tmp.appendingPathComponent("node_modules/.bin").path],
projectRoot: tmp)
#expect(cmd.count >= 3)
if cmd.count >= 3 {
#expect(cmd[0] == nodePath.path)
#expect(cmd[1] == scriptPath.path)
#expect(cmd[2] == "rpc")
}
}
@Test func `prefers open claw binary over pnpm`() throws {
let defaults = self.makeLocalDefaults()
let tmp = try makeTempDirForTests()
let binDir = tmp.appendingPathComponent("bin")
let openclawPath = binDir.appendingPathComponent("openclaw")
let pnpmPath = binDir.appendingPathComponent("pnpm")
try makeExecutableForTests(at: openclawPath)
try makeExecutableForTests(at: pnpmPath)
let cmd = CommandResolver.openclawCommand(
subcommand: "rpc",
defaults: defaults,
configRoot: [:],
searchPaths: [binDir.path],
projectRoot: tmp)
#expect(cmd.prefix(2).elementsEqual([openclawPath.path, "rpc"]))
}
@Test func `uses open claw binary without node runtime`() throws {
let defaults = self.makeLocalDefaults()
let tmp = try makeTempDirForTests()
let binDir = tmp.appendingPathComponent("bin")
let openclawPath = binDir.appendingPathComponent("openclaw")
try makeExecutableForTests(at: openclawPath)
let cmd = CommandResolver.openclawCommand(
subcommand: "gateway",
defaults: defaults,
configRoot: [:],
searchPaths: [binDir.path],
projectRoot: tmp)
#expect(cmd.prefix(2).elementsEqual([openclawPath.path, "gateway"]))
}
@Test func `falls back to pnpm`() throws {
let defaults = self.makeLocalDefaults()
let (tmp, pnpmPath) = try self.makeProjectRootWithPnpm()
let cmd = CommandResolver.openclawCommand(
subcommand: "rpc",
defaults: defaults,
configRoot: [:],
searchPaths: [tmp.appendingPathComponent("node_modules/.bin").path],
projectRoot: tmp)
#expect(cmd.prefix(4).elementsEqual([pnpmPath.path, "--silent", "openclaw", "rpc"]))
}
@Test func `pnpm keeps extra args after subcommand`() throws {
let defaults = self.makeLocalDefaults()
let (tmp, pnpmPath) = try self.makeProjectRootWithPnpm()
let cmd = CommandResolver.openclawCommand(
subcommand: "health",
extraArgs: ["--json", "--timeout", "5"],
defaults: defaults,
configRoot: [:],
searchPaths: [tmp.appendingPathComponent("node_modules/.bin").path],
projectRoot: tmp)
#expect(cmd.prefix(5).elementsEqual([pnpmPath.path, "--silent", "openclaw", "health", "--json"]))
#expect(cmd.suffix(2).elementsEqual(["--timeout", "5"]))
}
@Test func `preferred paths start with project node bins`() throws {
let tmp = try makeTempDirForTests()
let first = CommandResolver.preferredPaths(
home: FileManager().homeDirectoryForCurrentUser,
current: [],
projectRoot: tmp).first
#expect(first == tmp.appendingPathComponent("node_modules/.bin").path)
}
@Test func `managed install only precedes external installs after validation`() throws {
let home = try makeTempDirForTests()
let managedBin = home.appendingPathComponent(".openclaw/bin")
try FileManager().createDirectory(at: managedBin, withIntermediateDirectories: true)
let managedExecutable = managedBin.appendingPathComponent("openclaw")
let fallbackPaths = CommandResolver.preferredPaths(
home: home,
current: [],
projectRoot: home)
let validatedPaths = CommandResolver.preferredPaths(
home: home,
current: [],
projectRoot: home,
validatedExecutable: managedExecutable.path)
let packageManagerPath = home.appendingPathComponent("Library/pnpm").path
let fallbackManagedIndex = try #require(fallbackPaths.firstIndex(of: managedBin.path))
let fallbackPackageManagerIndex = try #require(fallbackPaths.firstIndex(of: packageManagerPath))
let validatedManagedIndex = try #require(validatedPaths.firstIndex(of: managedBin.path))
let validatedPackageManagerIndex = try #require(validatedPaths.firstIndex(of: packageManagerPath))
#expect(fallbackManagedIndex > fallbackPackageManagerIndex)
#expect(validatedManagedIndex < validatedPackageManagerIndex)
}
@Test func `node manager runtimes precede system runtimes`() throws {
let home = try makeTempDirForTests()
let nodeManagerBin = home.appendingPathComponent(".nvm/versions/node/v22.19.0/bin")
try makeExecutableForTests(at: nodeManagerBin.appendingPathComponent("node"))
let paths = CommandResolver.preferredPaths(
home: home,
current: [],
projectRoot: home)
let managerIndex = try #require(paths.firstIndex(of: nodeManagerBin.path))
let systemIndex = try #require(paths.firstIndex(of: "/opt/homebrew/bin"))
#expect(managerIndex < systemIndex)
}
@Test func `preferred paths include local user bin after system bins`() throws {
let home = try makeTempDirForTests()
let localBin = home.appendingPathComponent(".local/bin").path
let paths = CommandResolver.preferredPaths(
home: home,
current: [],
projectRoot: home)
let localIndex = try #require(paths.firstIndex(of: localBin))
let systemIndex = try #require(paths.firstIndex(of: "/bin"))
#expect(localIndex > systemIndex)
#expect(paths.count(where: { $0 == localBin }) == 1)
}
@Test func `SSH environment replaces path without dropping inherited values`() {
let paths = ["/usr/bin", "/bin", "/Users/test/.local/bin", "/opt/homebrew/bin"]
let environment = CommandResolver.sshEnvironment(
base: [
"HOME": "/Users/test",
"PATH": "/stale/path",
"SSH_AUTH_SOCK": "/tmp/ssh-agent.sock",
],
searchPaths: paths)
#expect(environment["PATH"] == paths.joined(separator: ":"))
#expect(environment["HOME"] == "/Users/test")
#expect(environment["SSH_AUTH_SOCK"] == "/tmp/ssh-agent.sock")
}
@Test func `validated CLI preference expires when the app requires a newer version`() throws {
let defaults = self.makeDefaults()
let root = try makeTempDirForTests()
let executable = root.appendingPathComponent("openclaw")
FileManager().createFile(atPath: executable.path, contents: Data())
try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
defaults.set(executable.path, forKey: cliValidatedExecutableKey)
defaults.set("2026.7.3", forKey: cliValidatedVersionKey)
#expect(CommandResolver.validatedOpenClawExecutable(
defaults: defaults,
fileManager: .default,
requiredVersion: "2026.7.3") == executable.path)
#expect(CommandResolver.validatedOpenClawExecutable(
defaults: defaults,
fileManager: .default,
requiredVersion: "2026.8.0") == nil)
}
@Test func `builds SSH command for remote mode`() {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
defaults.set("openclaw@example.com:2222", forKey: remoteTargetKey)
defaults.set("/tmp/id_ed25519", forKey: remoteIdentityKey)
defaults.set("/srv/openclaw", forKey: remoteProjectRootKey)
let cmd = CommandResolver.openclawCommand(
subcommand: "status",
extraArgs: ["--json"],
defaults: defaults,
configRoot: [:])
#expect(cmd.first == "/usr/bin/ssh")
if let marker = cmd.firstIndex(of: "--") {
#expect(cmd[marker + 1] == "openclaw@example.com")
} else {
#expect(Bool(false))
}
#expect(cmd.contains("StrictHostKeyChecking=yes"))
#expect(!cmd.contains("StrictHostKeyChecking=accept-new"))
#expect(cmd.contains("UpdateHostKeys=yes"))
#expect(cmd.contains("ControlPath=none"))
#expect(cmd.contains("-i"))
#expect(cmd.contains("/tmp/id_ed25519"))
if let script = cmd.last {
#expect(script.contains("PRJ='/srv/openclaw'"))
#expect(script.contains("cd \"$PRJ\""))
#expect(script.contains("openclaw"))
#expect(script.contains("status"))
#expect(script.contains("--json"))
#expect(script.contains("CLI="))
}
}
@Test func `explicit SSH config host key policy omits strict override`() {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
defaults.set("gateway-alias", forKey: remoteTargetKey)
let cmd = CommandResolver.openclawCommand(
subcommand: "status",
defaults: defaults,
configRoot: [
"gateway": [
"mode": "remote",
"remote": [
"sshHostKeyPolicy": "openssh",
"sshTarget": "gateway-alias",
],
],
])
#expect(cmd.first == "/usr/bin/ssh")
#expect(!cmd.contains { $0.hasPrefix("StrictHostKeyChecking=") })
#expect(cmd.contains("ControlPath=none"))
}
@Test func `OpenSSH host key opt in does not transfer to a different effective target`() {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
defaults.set("new-gateway-alias", forKey: remoteTargetKey)
let cmd = CommandResolver.openclawCommand(
subcommand: "status",
defaults: defaults,
configRoot: [
"gateway": [
"mode": "remote",
"remote": [
"sshHostKeyPolicy": "openssh",
"sshTarget": "old-gateway-alias",
],
],
])
#expect(cmd.contains("StrictHostKeyChecking=yes"))
#expect(cmd.contains("UpdateHostKeys=yes"))
}
@Test func `invalid SSH host key policy fails closed`() {
let settings = CommandResolver.connectionSettings(configRoot: [
"gateway": [
"mode": "remote",
"remote": ["sshHostKeyPolicy": " OPENSSH "],
],
])
#expect(settings.sshHostKeyPolicy == .strict)
}
@Test func `remote gateway probe applies SSH host key policy`() throws {
let strict = try #require(RemoteGatewayProbe._testSSHCheckCommand(
target: "gateway-alias",
hostKeyPolicy: .strict))
let openssh = try #require(RemoteGatewayProbe._testSSHCheckCommand(
target: "gateway-alias",
hostKeyPolicy: .openssh))
#expect(strict.contains("StrictHostKeyChecking=yes"))
#expect(strict.contains("UpdateHostKeys=yes"))
#expect(strict.contains("ControlPath=none"))
#expect(!openssh.contains { $0.hasPrefix("StrictHostKeyChecking=") })
#expect(!openssh.contains { $0.hasPrefix("UpdateHostKeys=") })
#expect(openssh.contains("ControlPath=none"))
}
@Test func `empty remote defaults fall back to config remote values`() {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
defaults.set(" ", forKey: remoteTargetKey)
defaults.set("", forKey: remoteIdentityKey)
let settings = CommandResolver.connectionSettings(
defaults: defaults,
configRoot: [
"gateway": [
"mode": "remote",
"remote": [
"sshTarget": "alice@gateway.local",
"sshIdentity": "/tmp/config-id",
],
],
])
#expect(settings.target == "alice@gateway.local")
#expect(settings.identity == "/tmp/config-id")
}
@Test func `rejects unsafe SSH targets`() {
#expect(CommandResolver.parseSSHTarget("-oProxyCommand=calc") == nil)
#expect(CommandResolver.parseSSHTarget("host:-oProxyCommand=calc") == nil)
#expect(CommandResolver.parseSSHTarget("user@host:2222")?.port == 2222)
}
@Test func `config root local overrides remote defaults`() throws {
let defaults = self.makeDefaults()
defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey)
defaults.set("openclaw@example.com:2222", forKey: remoteTargetKey)
let tmp = try makeTempDirForTests()
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
try makeExecutableForTests(at: openclawPath)
let cmd = CommandResolver.openclawCommand(
subcommand: "daemon",
defaults: defaults,
configRoot: ["gateway": ["mode": "local"]],
searchPaths: [tmp.appendingPathComponent("node_modules/.bin").path],
projectRoot: tmp)
#expect(cmd.first == openclawPath.path)
#expect(cmd.count >= 2)
if cmd.count >= 2 {
#expect(cmd[1] == "daemon")
}
}
@Test func `remote settings fall back to config ssh target`() {
let defaults = self.makeDefaults()
let settings = CommandResolver.connectionSettings(
defaults: defaults,
configRoot: [
"gateway": [
"mode": "remote",
"remote": [
"sshTarget": "alice@gateway.example:2222",
"sshIdentity": "/tmp/id_ed25519",
],
],
])
#expect(settings.mode == .remote)
#expect(settings.target == "alice@gateway.example:2222")
#expect(settings.identity == "/tmp/id_ed25519")
}
}