mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
test(macos): block real process fallthrough (#113337)
* 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>
This commit is contained in:
committed by
GitHub
parent
0c36fabc61
commit
5d1311c19e
@@ -267,6 +267,12 @@ extension GatewayLaunchAgentManager {
|
||||
payload: Data(payload.utf8),
|
||||
message: nil)
|
||||
}
|
||||
if ProcessInfo.processInfo.isRunningTests {
|
||||
return CommandResult(
|
||||
success: false,
|
||||
payload: nil,
|
||||
message: "Gateway daemon commands require explicit interception during tests")
|
||||
}
|
||||
#endif
|
||||
let command = CommandResolver.openclawCommand(
|
||||
subcommand: "gateway",
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
extension ProcessInfo {
|
||||
/// SwiftPM loads test bundles into these helpers, so bundle inspection alone
|
||||
/// cannot identify every test process. Keep current and legacy runner names.
|
||||
private static let swiftPMTestHelperNames: Set<String> = [
|
||||
"swiftpm-testing-helper",
|
||||
"swiftpm-xctest-helper",
|
||||
]
|
||||
|
||||
var isPreview: Bool {
|
||||
guard let raw = getenv("XCODE_RUNNING_FOR_PREVIEWS") else { return false }
|
||||
return String(cString: raw) == "1"
|
||||
@@ -34,15 +41,29 @@ extension ProcessInfo {
|
||||
isAppBundle: isAppBundle)
|
||||
}
|
||||
|
||||
var isRunningTests: Bool {
|
||||
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
|
||||
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
|
||||
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
|
||||
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
|
||||
static func resolveIsRunningTests(
|
||||
environment: [String: String],
|
||||
processName: String,
|
||||
arguments: [String],
|
||||
bundleURLs: [URL]) -> Bool
|
||||
{
|
||||
if bundleURLs.contains(where: { $0.pathExtension == "xctest" }) { return true }
|
||||
if self.swiftPMTestHelperNames.contains(processName) { return true }
|
||||
if let executable = arguments.first.map({ URL(fileURLWithPath: $0).lastPathComponent }),
|
||||
self.swiftPMTestHelperNames.contains(executable)
|
||||
{
|
||||
return true
|
||||
}
|
||||
return environment["XCTestConfigurationFilePath"] != nil
|
||||
|| environment["XCTestBundlePath"] != nil
|
||||
|| environment["XCTestSessionIdentifier"] != nil
|
||||
}
|
||||
|
||||
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
|
||||
return self.environment["XCTestConfigurationFilePath"] != nil
|
||||
|| self.environment["XCTestBundlePath"] != nil
|
||||
|| self.environment["XCTestSessionIdentifier"] != nil
|
||||
var isRunningTests: Bool {
|
||||
Self.resolveIsRunningTests(
|
||||
environment: self.environment,
|
||||
processName: self.processName,
|
||||
arguments: self.arguments,
|
||||
bundleURLs: Bundle.allBundles.map(\.bundleURL) + [Bundle.main.bundleURL])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayLaunchAgentManagerTests {
|
||||
@Test func `reads Gateway service ownership command directly from launchd`() throws {
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
@@ -75,6 +76,22 @@ struct GatewayLaunchAgentManagerTests {
|
||||
#expect(GatewayLaunchAgentManager.testingDaemonCommandCallsSnapshot().isEmpty)
|
||||
}
|
||||
|
||||
@Test func `unintercepted daemon commands fail closed during tests`() async {
|
||||
let marker = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("openclaw-no-disable-marker-\(UUID().uuidString)")
|
||||
defer {
|
||||
GatewayLaunchAgentManager.setTestingDisableLaunchAgentMarkerURL(nil)
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(false)
|
||||
}
|
||||
|
||||
GatewayLaunchAgentManager.setTestingDisableLaunchAgentMarkerURL(marker)
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(false)
|
||||
|
||||
let error = await GatewayLaunchAgentManager.kickstart()
|
||||
|
||||
#expect(error == "Gateway daemon commands require explicit interception during tests")
|
||||
}
|
||||
|
||||
@Test func `launch agent plist snapshot parses args and env`() throws {
|
||||
let url = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-launchd-\(UUID().uuidString).plist")
|
||||
|
||||
@@ -337,55 +337,14 @@ struct LowCoverageHelperTests {
|
||||
#expect(siblingPlan.reap.isEmpty)
|
||||
}
|
||||
|
||||
@Test func `port guardian classifies a real orphaned tunnel process for reaping`() async throws {
|
||||
// Real ssh that hangs safely: ProxyCommand replaces the TCP transport, so no
|
||||
// network traffic happens and the -L port is never bound (forwards only bind
|
||||
// after auth). Spawned through sh so the parent exits and ssh reparents to
|
||||
// launchd — the exact orphan shape the reaper must detect.
|
||||
let port = 45871
|
||||
// Detach the child's stdio: the pipe must reach EOF when sh exits, not when ssh dies.
|
||||
let script = "/usr/bin/ssh -o BatchMode=yes -o ProxyCommand='sleep 60' " +
|
||||
"-N -L \(port):127.0.0.1:\(port) orphan-reap-test-host >/dev/null 2>&1 & echo $!"
|
||||
let spawn = Process()
|
||||
spawn.executableURL = URL(fileURLWithPath: "/bin/sh")
|
||||
spawn.arguments = ["-c", script]
|
||||
let out = Pipe()
|
||||
spawn.standardOutput = out
|
||||
try spawn.run()
|
||||
spawn.waitUntilExit()
|
||||
let pidText = String(data: out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
|
||||
let pid = try #require(Int32(pidText.trimmingCharacters(in: .whitespacesAndNewlines)))
|
||||
defer { kill(pid, SIGKILL) }
|
||||
@Test func `port guardian reads current process metadata without spawning children`() throws {
|
||||
let info = try #require(PortGuardian._testTunnelProcessInfo(pid: getpid()))
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
// Reparenting to launchd is immediate once sh exits, but give ps/sysctl a beat.
|
||||
var info: PortGuardian.TunnelProcessInfo?
|
||||
for _ in 0..<40 {
|
||||
info = PortGuardian._testTunnelProcessInfo(pid: pid)
|
||||
if info?.parentPid == 1, info?.fullCommand?.isEmpty == false { break }
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
let orphan = try #require(info)
|
||||
#expect(orphan.parentPid == 1)
|
||||
// Kernel start time must be sane so the pid-reuse gate can rely on it.
|
||||
#expect(abs(orphan.startedAt - Date().timeIntervalSince1970) < 60)
|
||||
let recordedAt = Date().timeIntervalSince1970
|
||||
let record = PortGuardian.Record(
|
||||
port: port, pid: pid, command: "/usr/bin/ssh", mode: "remote", timestamp: recordedAt)
|
||||
#expect(PortGuardian.classifyTunnelRecord(record, process: orphan) == .reap)
|
||||
|
||||
// Same process under a different recorded port must never be reap-eligible.
|
||||
let mismatched = PortGuardian.Record(
|
||||
port: port + 1, pid: pid, command: "/usr/bin/ssh", mode: "remote", timestamp: recordedAt)
|
||||
#expect(PortGuardian.classifyTunnelRecord(mismatched, process: orphan) == .drop)
|
||||
|
||||
// A record predating this process (reused pid) must drop, not reap.
|
||||
let predates = PortGuardian.Record(
|
||||
port: port,
|
||||
pid: pid,
|
||||
command: "/usr/bin/ssh",
|
||||
mode: "remote",
|
||||
timestamp: orphan.startedAt - 3600)
|
||||
#expect(PortGuardian.classifyTunnelRecord(predates, process: orphan) == .drop)
|
||||
#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 {
|
||||
|
||||
@@ -25,6 +25,41 @@ struct NixModeStableSuiteTests {
|
||||
#expect(resolved)
|
||||
}
|
||||
|
||||
@Test func `detects SwiftPM and XCTest runners`() {
|
||||
#expect(ProcessInfo.resolveIsRunningTests(
|
||||
environment: [:],
|
||||
processName: "swiftpm-testing-helper",
|
||||
arguments: [],
|
||||
bundleURLs: []))
|
||||
#expect(ProcessInfo.resolveIsRunningTests(
|
||||
environment: [:],
|
||||
processName: "swiftpm-xctest-helper",
|
||||
arguments: [],
|
||||
bundleURLs: []))
|
||||
for helper in ["swiftpm-testing-helper", "swiftpm-xctest-helper"] {
|
||||
#expect(ProcessInfo.resolveIsRunningTests(
|
||||
environment: [:],
|
||||
processName: "OpenClawTests",
|
||||
arguments: ["/Library/Developer/Toolchains/usr/libexec/swift/pm/\(helper)"],
|
||||
bundleURLs: []))
|
||||
}
|
||||
#expect(ProcessInfo.resolveIsRunningTests(
|
||||
environment: ["XCTestSessionIdentifier": "session"],
|
||||
processName: "OpenClawTests",
|
||||
arguments: [],
|
||||
bundleURLs: []))
|
||||
#expect(ProcessInfo.resolveIsRunningTests(
|
||||
environment: [:],
|
||||
processName: "OpenClawTests",
|
||||
arguments: [],
|
||||
bundleURLs: [URL(fileURLWithPath: "/tmp/OpenClawTests.xctest")]))
|
||||
#expect(!ProcessInfo.resolveIsRunningTests(
|
||||
environment: [:],
|
||||
processName: "OpenClaw",
|
||||
arguments: [],
|
||||
bundleURLs: []))
|
||||
}
|
||||
|
||||
@Test func `ignores stable suite outside app bundles`() throws {
|
||||
let suite = try #require(UserDefaults(suiteName: launchdLabel))
|
||||
let key = "openclaw.nixMode"
|
||||
|
||||
Reference in New Issue
Block a user