mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
0dbdf994b3
* feat(macos): isolate named app profiles * refactor(macos): isolate profile launch ownership * fix(macos): avoid overlapping approvals socket access * fix(macos): declare profile defaults concurrency ownership * fix(macos): return profiled node launch arguments * chore(i18n): refresh macOS profile source inventory * fix(macos): gate profile startup before services * test(macos): evaluate profile state before assertions * fix(daemon): skip absent launchd deactivation * fix(macos): fail closed on profile port conflicts * chore(i18n): refresh profile conflict inventory * fix(macos): ignore non-gateway launch agent claims * test(macos): stabilize profile lifecycle timing * fix(macos): remove stale dashboard URL * chore(macos): refresh native source baseline
200 lines
9.2 KiB
Swift
200 lines
9.2 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
struct GatewayEnvironmentTests {
|
|
private final class LockedCounter: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var count = 0
|
|
|
|
func increment() {
|
|
self.lock.withLock {
|
|
self.count += 1
|
|
}
|
|
}
|
|
|
|
func value() -> Int {
|
|
self.lock.withLock { self.count }
|
|
}
|
|
}
|
|
|
|
@Test func `semver parses common forms`() {
|
|
#expect(Semver.parse("1.2.3") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse(" v1.2.3 \n") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse("v2.0.0") == Semver(major: 2, minor: 0, patch: 0))
|
|
#expect(Semver.parse("3.4.5-beta.1") == Semver(major: 3, minor: 4, patch: 5)) // prerelease suffix stripped
|
|
#expect(Semver.parse("2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11)) // build suffix stripped
|
|
#expect(Semver.parse("1.0.5+build.123") == Semver(major: 1, minor: 0, patch: 5)) // metadata suffix stripped
|
|
#expect(Semver.parse("v1.2.3+build.9") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse("1.2.3+build.123") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse("1.2.3-rc.1+build.7") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse("v1.2.3-rc.1") == Semver(major: 1, minor: 2, patch: 3))
|
|
#expect(Semver.parse("1.2.0") == Semver(major: 1, minor: 2, patch: 0))
|
|
#expect(Semver.parse(nil) == nil)
|
|
#expect(Semver.parse("invalid") == nil)
|
|
#expect(Semver.parse("1.2") == nil)
|
|
#expect(Semver.parse("1.2.x") == nil)
|
|
// Product-prefixed output from `openclaw --version` should NOT parse as semver
|
|
// (the prefix must be stripped by the caller, not the parser).
|
|
#expect(Semver.parse("OpenClaw 2026.3.23-1") == nil)
|
|
}
|
|
|
|
@Test func `gateway version output strips product prefix before parsing`() {
|
|
let normalized = GatewayEnvironment.normalizeGatewayVersionOutput(" OpenClaw 2026.3.23-1 \n")
|
|
#expect(normalized == "2026.3.23-1")
|
|
#expect(Semver.parse(normalized) == Semver(major: 2026, minor: 3, patch: 23))
|
|
}
|
|
|
|
@Test func `gateway version output strips trailing commit hash`() {
|
|
let normalized = GatewayEnvironment.normalizeGatewayVersionOutput("OpenClaw 2026.4.2 (d74a122)")
|
|
#expect(normalized == "2026.4.2")
|
|
#expect(Semver.parse(normalized) == Semver(major: 2026, minor: 4, patch: 2))
|
|
|
|
// Pre-release suffix + commit hash combined
|
|
let normalized2 = GatewayEnvironment.normalizeGatewayVersionOutput("OpenClaw 2026.4.2-1 (d74a122)")
|
|
#expect(normalized2 == "2026.4.2-1")
|
|
#expect(Semver.parse(normalized2) == Semver(major: 2026, minor: 4, patch: 2))
|
|
}
|
|
|
|
@Test func `failed global version probe falls back to local package`() async throws {
|
|
let projectRoot = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-gateway-environment-\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: projectRoot, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: projectRoot) }
|
|
try Data(#"{"version":"2026.7.29"}"#.utf8)
|
|
.write(to: projectRoot.appendingPathComponent("package.json"))
|
|
|
|
let version = await GatewayEnvironment.installedGatewayVersion(
|
|
gatewayBin: "/usr/bin/false",
|
|
projectRoot: projectRoot,
|
|
searchPaths: ["/usr/bin"])
|
|
|
|
#expect(version == "2026.7.29")
|
|
}
|
|
|
|
@Test func `gateway version probe tolerates loaded host delay`() async throws {
|
|
let root = try makeTempDirForTests()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let gateway = root.appendingPathComponent("openclaw")
|
|
try "#!/bin/sh\nsleep 2.1\necho OpenClaw 2026.7.30\n"
|
|
.write(to: gateway, atomically: true, encoding: .utf8)
|
|
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: gateway.path)
|
|
|
|
let version = await GatewayEnvironment.installedGatewayVersion(
|
|
gatewayBin: gateway.path,
|
|
projectRoot: root,
|
|
searchPaths: [root.path, "/usr/bin", "/bin"])
|
|
|
|
#expect(version == "2026.7.30")
|
|
}
|
|
|
|
@Test func `gateway launch resolution scans preferred paths once`() async throws {
|
|
let root = try makeTempDirForTests()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let bin = root.appendingPathComponent("bin", isDirectory: true)
|
|
let node = bin.appendingPathComponent("node")
|
|
let gateway = bin.appendingPathComponent("openclaw")
|
|
try makeExecutableForTests(at: node)
|
|
try makeExecutableForTests(at: gateway)
|
|
try "#!/bin/sh\necho v24.15.0\n".write(to: node, atomically: true, encoding: .utf8)
|
|
let expectedVersion = GatewayEnvironment.expectedGatewayVersionString()
|
|
let gatewayVersion = expectedVersion.flatMap { Semver.parse($0) == nil ? nil : $0 } ?? "2026.7.30"
|
|
try "#!/bin/sh\necho OpenClaw \(gatewayVersion)\n"
|
|
.write(to: gateway, atomically: true, encoding: .utf8)
|
|
|
|
let scans = LockedCounter()
|
|
let resolution = await GatewayEnvironment.resolveGatewayCommand(searchPathsProvider: {
|
|
scans.increment()
|
|
return [bin.path]
|
|
})
|
|
|
|
#expect(scans.value() == 1)
|
|
#expect(resolution.status.kind == .ok)
|
|
#expect(resolution.status.gatewayVersion == gatewayVersion)
|
|
#expect(resolution.command?.first == gateway.path)
|
|
}
|
|
|
|
@Test func `failed gateway launch resolution scans preferred paths once`() async {
|
|
let scans = LockedCounter()
|
|
let resolution = await GatewayEnvironment.resolveGatewayCommand(searchPathsProvider: {
|
|
scans.increment()
|
|
return ["/missing/\(UUID().uuidString)"]
|
|
})
|
|
|
|
#expect(scans.value() == 1)
|
|
#expect(resolution.status.kind == .missingNode)
|
|
#expect(resolution.command == nil)
|
|
}
|
|
|
|
@Test func `semver compatibility requires same major and not older`() {
|
|
let required = Semver(major: 2, minor: 1, patch: 0)
|
|
#expect(Semver(major: 2, minor: 1, patch: 0).compatible(with: required))
|
|
#expect(Semver(major: 2, minor: 2, patch: 0).compatible(with: required))
|
|
#expect(Semver(major: 2, minor: 1, patch: 1).compatible(with: required))
|
|
#expect(Semver(major: 2, minor: 0, patch: 9).compatible(with: required) == false)
|
|
#expect(Semver(major: 3, minor: 0, patch: 0).compatible(with: required) == false)
|
|
#expect(Semver(major: 1, minor: 9, patch: 9).compatible(with: required) == false)
|
|
}
|
|
|
|
@Test func `gateway port defaults and respects override`() async {
|
|
let configPath = TestIsolation.tempConfigPath()
|
|
await TestIsolation.withIsolatedState(
|
|
env: ["OPENCLAW_CONFIG_PATH": configPath],
|
|
defaults: ["gatewayPort": nil])
|
|
{
|
|
let defaultPort = GatewayEnvironment.gatewayPort()
|
|
#expect(defaultPort == 18789)
|
|
|
|
UserDefaults.standard.set(19999, forKey: "gatewayPort")
|
|
defer { UserDefaults.standard.removeObject(forKey: "gatewayPort") }
|
|
#expect(GatewayEnvironment.gatewayPort() == 19999)
|
|
}
|
|
}
|
|
|
|
@Test func `named profiles derive stable distinct gateway ports after explicit precedence`() {
|
|
let work = AppProfile(environment: ["OPENCLAW_PROFILE": "work"])
|
|
let personal = AppProfile(environment: ["OPENCLAW_PROFILE": "personal"])
|
|
let workPort = GatewayEnvironment.resolvedGatewayPort(
|
|
environment: [:],
|
|
configPort: nil,
|
|
storedPort: 0,
|
|
profile: work)
|
|
#expect((20000..<60000).contains(workPort))
|
|
#expect(workPort == work.defaultGatewayPort)
|
|
#expect(workPort != personal.defaultGatewayPort)
|
|
#expect(GatewayEnvironment.resolvedGatewayPort(
|
|
environment: ["OPENCLAW_GATEWAY_PORT": "21001"],
|
|
configPort: 22001,
|
|
storedPort: 23001,
|
|
profile: work) == 21001)
|
|
#expect(GatewayEnvironment.resolvedGatewayPort(
|
|
environment: [:],
|
|
configPort: 22001,
|
|
storedPort: 23001,
|
|
profile: work) == 22001)
|
|
#expect(GatewayEnvironment.resolvedGatewayPort(
|
|
environment: [:],
|
|
configPort: nil,
|
|
storedPort: 23001,
|
|
profile: work) == 23001)
|
|
#expect(AppProfile(environment: [:]).defaultGatewayPort == 18789)
|
|
#expect(GatewayEnvironment.gatewayCommand(
|
|
prefix: ["/opt/openclaw"],
|
|
port: workPort,
|
|
bind: "loopback",
|
|
profile: work) == [
|
|
"/opt/openclaw", "--profile", "work", "gateway", "--port", "\(workPort)",
|
|
"--bind", "loopback",
|
|
])
|
|
}
|
|
|
|
@Test func `expected gateway version from string uses parser`() {
|
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2))
|
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver(
|
|
major: 2026,
|
|
minor: 1,
|
|
patch: 11))
|
|
#expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil)
|
|
}
|
|
}
|