mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 07:41:36 +00:00
* test(macos): block real process fallthrough * test(macos): cover SwiftPM helper executable paths Co-authored-by: Peter Steinberger <steipete@gmail.com> * test(macos): avoid process-age timing assumption Co-authored-by: Peter Steinberger <steipete@gmail.com>
421 lines
18 KiB
Swift
421 lines
18 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import OpenClawProtocol
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
struct LowCoverageHelperTests {
|
|
private typealias ProtoAnyCodable = OpenClawProtocol.AnyCodable
|
|
|
|
@Test func `any codable helper accessors`() throws {
|
|
let payload: [String: ProtoAnyCodable] = [
|
|
"title": ProtoAnyCodable("Hello"),
|
|
"flag": ProtoAnyCodable(true),
|
|
"count": ProtoAnyCodable(3),
|
|
"ratio": ProtoAnyCodable(1.25),
|
|
"list": ProtoAnyCodable([ProtoAnyCodable("a"), ProtoAnyCodable(2)]),
|
|
]
|
|
let any = ProtoAnyCodable(payload)
|
|
let dict = try #require(any.dictionaryValue)
|
|
#expect(dict["title"]?.stringValue == "Hello")
|
|
#expect(dict["flag"]?.boolValue == true)
|
|
#expect(dict["count"]?.intValue == 3)
|
|
#expect(dict["ratio"]?.doubleValue == 1.25)
|
|
#expect(dict["list"]?.arrayValue?.count == 2)
|
|
|
|
let foundation = any.foundationValue as? [String: Any]
|
|
#expect((foundation?["title"] as? String) == "Hello")
|
|
}
|
|
|
|
@Test func `attributed string strips foreground color`() {
|
|
let text = NSMutableAttributedString(string: "Test")
|
|
text.addAttribute(.foregroundColor, value: NSColor.red, range: NSRange(location: 0, length: 4))
|
|
let stripped = text.strippingForegroundColor()
|
|
let color = stripped.attribute(.foregroundColor, at: 0, effectiveRange: nil)
|
|
#expect(color == nil)
|
|
}
|
|
|
|
@Test func `shell executor handles empty command`() async {
|
|
let result = await ShellExecutor.runDetailed(command: [], cwd: nil, env: nil, timeout: nil)
|
|
#expect(result.success == false)
|
|
#expect(result.errorMessage != nil)
|
|
}
|
|
|
|
@Test func `shell executor runs command`() async {
|
|
let result = await ShellExecutor.runDetailed(command: ["/bin/echo", "ok"], cwd: nil, env: nil, timeout: 2)
|
|
#expect(result.success == true)
|
|
#expect(result.stdout.contains("ok") || result.stderr.contains("ok"))
|
|
}
|
|
|
|
@Test func `shell executor times out`() async {
|
|
for _ in 0..<10 {
|
|
let result = await ShellExecutor.runDetailed(
|
|
command: ["/bin/sleep", "1"],
|
|
cwd: nil,
|
|
env: nil,
|
|
timeout: 0.01)
|
|
#expect(result.timedOut == true)
|
|
}
|
|
}
|
|
|
|
@Test func `shell executor drains stdout and stderr`() async {
|
|
let script = """
|
|
i=0
|
|
while [ $i -lt 2000 ]; do
|
|
echo "stdout-$i"
|
|
echo "stderr-$i" 1>&2
|
|
i=$((i+1))
|
|
done
|
|
"""
|
|
let result = await ShellExecutor.runDetailed(
|
|
command: ["/bin/sh", "-c", script],
|
|
cwd: nil,
|
|
env: nil,
|
|
timeout: 2)
|
|
#expect(result.success == true)
|
|
#expect(result.stdout.contains("stdout-1999"))
|
|
#expect(result.stderr.contains("stderr-1999"))
|
|
}
|
|
|
|
@Test func `shell executor finishes when a descendant retains output handles`() async {
|
|
let startedAt = ContinuousClock.now
|
|
let result = await ShellExecutor.runDetailed(
|
|
command: ["/bin/sh", "-c", "sleep 5 & echo ready"],
|
|
cwd: nil,
|
|
env: nil,
|
|
timeout: 1)
|
|
|
|
#expect(result.success == true)
|
|
#expect(result.stdout.contains("ready"))
|
|
#expect(ContinuousClock.now - startedAt < .seconds(2))
|
|
}
|
|
|
|
@Test func `node info codable round trip`() throws {
|
|
let info = NodeInfo(
|
|
nodeId: "node-1",
|
|
displayName: "Node One",
|
|
platform: "macOS",
|
|
version: "1.0",
|
|
coreVersion: "1.0-core",
|
|
uiVersion: "1.0-ui",
|
|
deviceFamily: "Mac",
|
|
modelIdentifier: "MacBookPro",
|
|
remoteIp: "192.168.1.2",
|
|
caps: ["chat"],
|
|
commands: ["send"],
|
|
permissions: ["send": true],
|
|
paired: true,
|
|
connected: false)
|
|
let data = try JSONEncoder().encode(info)
|
|
let decoded = try JSONDecoder().decode(NodeInfo.self, from: data)
|
|
#expect(decoded.nodeId == "node-1")
|
|
#expect(decoded.isPaired == true)
|
|
#expect(decoded.isConnected == false)
|
|
}
|
|
|
|
@Test @MainActor func `presence reporter helpers`() {
|
|
let summary = PresenceReporter._testComposePresenceSummary(mode: "local", reason: "test")
|
|
#expect(summary.contains("mode local"))
|
|
#expect(!summary.contains("last input"))
|
|
#expect(!PresenceReporter._testAppVersionString().isEmpty)
|
|
#expect(!PresenceReporter._testPlatformString().isEmpty)
|
|
_ = PresenceReporter._testPrimaryIPv4Address()
|
|
let privacyParameters = PresenceReporter._testActivityPrivacyParameters()
|
|
#expect(privacyParameters["lastInputSeconds"]?.base as? Int == 2_592_000)
|
|
#expect(
|
|
privacyParameters["tags"]?.base as? [String] ==
|
|
["system-presence-clear-last-input"])
|
|
}
|
|
|
|
@Test func `port guardian parses listeners and builds reports`() {
|
|
let output = """
|
|
p123
|
|
cnode
|
|
uuser
|
|
p456
|
|
cssh
|
|
uroot
|
|
"""
|
|
let listeners = PortGuardian._testParseListeners(output)
|
|
#expect(listeners.count == 2)
|
|
#expect(listeners[0].command == "node")
|
|
#expect(listeners[1].command == "ssh")
|
|
|
|
let okReport = PortGuardian._testBuildReport(
|
|
port: 18789,
|
|
mode: .local,
|
|
listeners: [(pid: 1, command: "node", fullCommand: "node", user: "me")])
|
|
#expect(okReport.offenders.isEmpty)
|
|
|
|
let badReport = PortGuardian._testBuildReport(
|
|
port: 18789,
|
|
mode: .local,
|
|
listeners: [(pid: 2, command: "python", fullCommand: "python", user: "me")])
|
|
#expect(!badReport.offenders.isEmpty)
|
|
|
|
let emptyReport = PortGuardian._testBuildReport(port: 18789, mode: .local, listeners: [])
|
|
#expect(emptyReport.summary.contains("Nothing is listening"))
|
|
}
|
|
|
|
@Test func `port guardian remote mode does not kill docker`() {
|
|
let port = GatewayEnvironment.gatewayPort()
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "com.docker.backend",
|
|
fullCommand: "com.docker.backend",
|
|
port: port,
|
|
mode: .remote) == true)
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "ssh",
|
|
fullCommand: "ssh -L \(port):localhost:\(port) user@host",
|
|
port: port,
|
|
mode: .remote) == true)
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "podman",
|
|
fullCommand: "podman",
|
|
port: port,
|
|
mode: .remote) == true)
|
|
}
|
|
|
|
@Test func `port guardian local mode still rejects unexpected`() {
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "com.docker.backend",
|
|
fullCommand: "com.docker.backend",
|
|
port: 18789,
|
|
mode: .local) == false)
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "python",
|
|
fullCommand: "python server.py",
|
|
port: 18789,
|
|
mode: .local) == false)
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "node",
|
|
fullCommand: "openclaw-gateway",
|
|
port: 18789,
|
|
mode: .local) == true)
|
|
|
|
#expect(PortGuardian._testIsExpected(
|
|
command: "node",
|
|
fullCommand: "node /path/to/gateway-daemon",
|
|
port: 18789,
|
|
mode: .local) == true)
|
|
}
|
|
|
|
@Test func `port guardian remote mode report accepts any listener`() {
|
|
let dockerReport = PortGuardian._testBuildReport(
|
|
port: 18789,
|
|
mode: .remote,
|
|
listeners: [(
|
|
pid: 99,
|
|
command: "com.docker.backend",
|
|
fullCommand: "com.docker.backend",
|
|
user: "me")])
|
|
#expect(dockerReport.offenders.isEmpty)
|
|
|
|
let localDockerReport = PortGuardian._testBuildReport(
|
|
port: 18789,
|
|
mode: .local,
|
|
listeners: [(
|
|
pid: 99,
|
|
command: "com.docker.backend",
|
|
fullCommand: "com.docker.backend",
|
|
user: "me")])
|
|
#expect(!localDockerReport.offenders.isEmpty)
|
|
}
|
|
|
|
@Test func `port guardian matches only its own ssh tunnel command`() {
|
|
let full = "/usr/bin/ssh -o BatchMode=yes -o ControlMaster=no -o ControlPath=none "
|
|
+ "-o ExitOnForwardFailure=yes -n -N -L 18789:127.0.0.1:18789 -- user@host"
|
|
#expect(PortGuardian.isTunnelCommand(full, localPort: 18789))
|
|
#expect(PortGuardian.isTunnelCommand("ssh -N -L18789:127.0.0.1:18789 user@host", localPort: 18789))
|
|
|
|
// Wrong local port, missing -N (interactive forward), other binaries, or a
|
|
// forward to a non-loopback host are not ours to kill.
|
|
#expect(!PortGuardian.isTunnelCommand(full, localPort: 18790))
|
|
#expect(!PortGuardian.isTunnelCommand("ssh -L 18789:127.0.0.1:18789 user@host", localPort: 18789))
|
|
#expect(!PortGuardian.isTunnelCommand("ssh -N -L 18789:example.com:80 user@host", localPort: 18789))
|
|
#expect(!PortGuardian.isTunnelCommand("sshd: user [priv]", localPort: 18789))
|
|
#expect(!PortGuardian.isTunnelCommand("node server.js -N -L 18789:127.0.0.1:18789", localPort: 18789))
|
|
#expect(!PortGuardian.isTunnelCommand("", localPort: 18789))
|
|
}
|
|
|
|
@Test func `port guardian classifies tunnel records for reaping`() {
|
|
let recordedAt: TimeInterval = 1_000_000
|
|
let record = PortGuardian.Record(
|
|
port: 18789, pid: 4242, command: "/usr/bin/ssh", mode: "remote", timestamp: recordedAt)
|
|
let tunnel = "/usr/bin/ssh -o BatchMode=yes -n -N -L 18789:127.0.0.1:18789 -- user@host"
|
|
let spawnedBeforeRecord = recordedAt - 2
|
|
|
|
// Process gone → stale record.
|
|
#expect(PortGuardian.classifyTunnelRecord(record, process: nil) == .drop)
|
|
// Pid reused by an unrelated process → drop the record, never kill.
|
|
#expect(PortGuardian.classifyTunnelRecord(
|
|
record,
|
|
process: .init(parentPid: 1, startedAt: spawnedBeforeRecord, fullCommand: "node server.js"))
|
|
== .drop)
|
|
// Pid reused by a look-alike tunnel started after the record was written →
|
|
// different process, drop without killing.
|
|
#expect(PortGuardian.classifyTunnelRecord(
|
|
record,
|
|
process: .init(parentPid: 1, startedAt: recordedAt + 3600, fullCommand: tunnel)) == .drop)
|
|
// Reparented to launchd → owning app instance died → reap.
|
|
#expect(PortGuardian.classifyTunnelRecord(
|
|
record,
|
|
process: .init(parentPid: 1, startedAt: spawnedBeforeRecord, fullCommand: tunnel)) == .reap)
|
|
// Parent alive (e.g. a concurrent OpenClaw instance) → hands off.
|
|
#expect(PortGuardian.classifyTunnelRecord(
|
|
record,
|
|
process: .init(parentPid: 987, startedAt: spawnedBeforeRecord, fullCommand: tunnel)) == .keep)
|
|
// Command unreadable → cannot prove ownership → keep.
|
|
#expect(PortGuardian.classifyTunnelRecord(
|
|
record,
|
|
process: .init(parentPid: 1, startedAt: spawnedBeforeRecord, fullCommand: nil)) == .keep)
|
|
}
|
|
|
|
@Test func `port guardian reap plan merges disk records and drops stale ones`() {
|
|
func record(pid: Int32, port: Int, timestamp: TimeInterval) -> PortGuardian.Record {
|
|
PortGuardian.Record(
|
|
port: port, pid: pid, command: "/usr/bin/ssh", mode: "remote", timestamp: timestamp)
|
|
}
|
|
func tunnel(port: Int) -> String {
|
|
"/usr/bin/ssh -o BatchMode=yes -n -N -L \(port):127.0.0.1:18789 -- user@host"
|
|
}
|
|
|
|
// pid 10: our exact live receipt. Disk-only records from a crashed sibling
|
|
// instance: pid 20 orphaned, pid 30 already gone.
|
|
let own = [record(pid: 10, port: 18790, timestamp: 300)]
|
|
let disk = [
|
|
record(pid: 20, port: 18789, timestamp: 100),
|
|
record(pid: 30, port: 18791, timestamp: 200),
|
|
own[0],
|
|
]
|
|
let currentAppPID: Int32 = 987
|
|
let plan = PortGuardian.planTunnelReap(
|
|
own: own,
|
|
disk: disk,
|
|
processInfo: { pid in
|
|
switch pid {
|
|
// Even a current-parent observation cannot reap a receipt this
|
|
// process still owns exactly.
|
|
case 10: .init(parentPid: currentAppPID, startedAt: 299, fullCommand: tunnel(port: 18790))
|
|
case 20: .init(parentPid: 1, startedAt: 99, fullCommand: tunnel(port: 18789))
|
|
default: nil
|
|
}
|
|
},
|
|
currentAppPID: currentAppPID)
|
|
#expect(plan.reap.map(\.pid) == [20])
|
|
#expect(plan.keep.map(\.pid) == [10])
|
|
#expect(plan.keep.first?.port == 18790)
|
|
// The dead pid 30 is reported as a drop; the exact owned pid-10 receipt is kept.
|
|
#expect(plan.drop.map(\.pid) == [30])
|
|
|
|
let replacement = record(pid: 10, port: 18792, timestamp: 400)
|
|
let replacementPlan = PortGuardian.planTunnelReap(
|
|
own: own,
|
|
disk: [replacement],
|
|
processInfo: { _ in
|
|
.init(parentPid: currentAppPID, startedAt: 399, fullCommand: tunnel(port: replacement.port))
|
|
},
|
|
currentAppPID: currentAppPID)
|
|
#expect(replacementPlan.reap == [replacement])
|
|
#expect(replacementPlan.keep.isEmpty)
|
|
|
|
let sibling = record(pid: 40, port: 18793, timestamp: 500)
|
|
let siblingPlan = PortGuardian.planTunnelReap(
|
|
own: [],
|
|
disk: [sibling],
|
|
processInfo: { _ in
|
|
.init(parentPid: 654, startedAt: 499, fullCommand: tunnel(port: sibling.port))
|
|
},
|
|
currentAppPID: currentAppPID)
|
|
#expect(siblingPlan.keep == [sibling])
|
|
#expect(siblingPlan.reap.isEmpty)
|
|
}
|
|
|
|
@Test func `port guardian reads current process metadata without spawning children`() throws {
|
|
let info = try #require(PortGuardian._testTunnelProcessInfo(pid: getpid()))
|
|
let now = Date().timeIntervalSince1970
|
|
|
|
#expect(info.parentPid > 0)
|
|
#expect(info.startedAt > now - ProcessInfo.processInfo.systemUptime - 1)
|
|
#expect(info.startedAt <= now + 1)
|
|
#expect(info.fullCommand?.isEmpty == false)
|
|
}
|
|
|
|
@Test @MainActor func `canvas scheme handler resolves files and errors`() throws {
|
|
let root = FileManager().temporaryDirectory
|
|
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
|
let session = root.appendingPathComponent("main", isDirectory: true)
|
|
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
|
|
|
|
let index = session.appendingPathComponent("index.html")
|
|
try "<h1>Hello</h1>".write(to: index, atomically: true, encoding: .utf8)
|
|
|
|
let handler = CanvasSchemeHandler(root: root)
|
|
let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html"))
|
|
let response = handler._testResponse(for: url)
|
|
#expect(response.mime == "text/html")
|
|
#expect(String(data: response.data, encoding: .utf8)?.contains("Hello") == true)
|
|
|
|
let invalid = try #require(URL(string: "https://example.com"))
|
|
let invalidResponse = handler._testResponse(for: invalid)
|
|
#expect(invalidResponse.mime == "text/html")
|
|
|
|
let missing = try #require(CanvasScheme.makeURL(session: "missing", path: "/"))
|
|
let missingResponse = handler._testResponse(for: missing)
|
|
#expect(missingResponse.mime == "text/html")
|
|
|
|
#expect(handler._testTextEncodingName(for: "text/html") == "utf-8")
|
|
#expect(handler._testTextEncodingName(for: "application/octet-stream") == nil)
|
|
}
|
|
|
|
@Test @MainActor func `canvas scheme handler blocks symlink escapes`() throws {
|
|
let root = FileManager().temporaryDirectory
|
|
.appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true)
|
|
defer { try? FileManager().removeItem(at: root) }
|
|
try FileManager().createDirectory(at: root, withIntermediateDirectories: true)
|
|
|
|
let session = root.appendingPathComponent("main", isDirectory: true)
|
|
try FileManager().createDirectory(at: session, withIntermediateDirectories: true)
|
|
|
|
let outside = root.deletingLastPathComponent().appendingPathComponent("canvas-secret-\(UUID().uuidString).txt")
|
|
defer { try? FileManager().removeItem(at: outside) }
|
|
try "top-secret".write(to: outside, atomically: true, encoding: .utf8)
|
|
|
|
let symlink = session.appendingPathComponent("index.html")
|
|
try FileManager().createSymbolicLink(at: symlink, withDestinationURL: outside)
|
|
|
|
let handler = CanvasSchemeHandler(root: root)
|
|
let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html"))
|
|
let response = handler._testResponse(for: url)
|
|
let body = String(data: response.data, encoding: .utf8) ?? ""
|
|
|
|
#expect(response.mime == "text/html")
|
|
#expect(body.contains("Forbidden"))
|
|
#expect(!body.contains("top-secret"))
|
|
}
|
|
|
|
@Test @MainActor func `canvas window helper functions`() throws {
|
|
#expect(CanvasWindowController._testSanitizeSessionKey(" main ") == "main")
|
|
#expect(CanvasWindowController._testSanitizeSessionKey("bad/..") == "bad___")
|
|
#expect(CanvasWindowController._testJSOptionalStringLiteral(nil) == "null")
|
|
|
|
let rect = NSRect(x: 10, y: 12, width: 400, height: 420)
|
|
let key = CanvasWindowController._testStoredFrameKey(sessionKey: "test")
|
|
let loaded = CanvasWindowController._testStoreAndLoadFrame(sessionKey: "test", frame: rect)
|
|
UserDefaults.standard.removeObject(forKey: key)
|
|
#expect(loaded?.size.width == rect.size.width)
|
|
|
|
let trusted = try #require(URL(string:
|
|
"http://127.0.0.1:18789/__openclaw__/cap/token/__openclaw__/a2ui/?platform=macos"))
|
|
#expect(CanvasA2UIActionMessageHandler.isTrustedSourceURL(trusted, expectedRemoteURL: trusted))
|
|
}
|
|
}
|