mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
148 lines
6.8 KiB
Swift
148 lines
6.8 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized) struct NodeServiceManagerTests {
|
|
@Test func `active profile performs no persistent node service work`() async {
|
|
let profile = AppProfile(environment: ["OPENCLAW_PROFILE": "work"])
|
|
NodeServiceManager._testResetPersistentServiceCalls()
|
|
|
|
#expect(await NodeServiceManager.start(profile: profile) == nil)
|
|
#expect(await NodeServiceManager.stop(profile: profile) == nil)
|
|
#expect(await NodeServiceManager.restart(profile: profile) == nil)
|
|
#expect(NodeServiceManager.launchdProgramArguments(profile: profile) == [])
|
|
#expect(await !(NodeServiceManager.waitUntilRunning(profile: profile)))
|
|
let snapshot = NodeServiceManager._testPersistentServiceCallSnapshot()
|
|
#expect(snapshot.commands.isEmpty)
|
|
#expect(snapshot.ownershipReads == 0)
|
|
}
|
|
|
|
@Test func `builds node service commands with current CLI shape`() async throws {
|
|
try await TestIsolation.withUserDefaultsValues(["openclaw.gatewayProjectRootPath": nil]) {
|
|
let tmp = try makeTempDirForTests()
|
|
CommandResolver.setProjectRoot(tmp.path)
|
|
|
|
let openclawPath = tmp.appendingPathComponent("node_modules/.bin/openclaw")
|
|
try makeExecutableForTests(at: openclawPath)
|
|
|
|
let start = await NodeServiceManager._testServiceCommand(["start"])
|
|
#expect(start == [openclawPath.path, "node", "start", "--json"])
|
|
|
|
let stop = await NodeServiceManager._testServiceCommand(["stop"])
|
|
#expect(stop == [openclawPath.path, "node", "stop", "--json"])
|
|
|
|
let restart = await NodeServiceManager._testServiceCommand(["restart"])
|
|
#expect(restart == [openclawPath.path, "node", "restart", "--json"])
|
|
}
|
|
}
|
|
|
|
@Test(arguments: ["start:stop", "stop:start", "restart:stop"])
|
|
func `node lifecycle commands finish in request order`(_ transition: String) async throws {
|
|
let actions = transition.split(separator: ":").map(String.init)
|
|
let previousAction = try #require(actions.first)
|
|
let nextAction = try #require(actions.last)
|
|
let root = try makeTempDirForTests()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
try await TestIsolation.withIsolatedState(
|
|
env: [
|
|
"OPENCLAW_NODE_SERVICE_TEST_ROOT": root.path,
|
|
"OPENCLAW_NODE_SERVICE_DELAYED_ACTION": previousAction,
|
|
],
|
|
defaults: ["openclaw.gatewayProjectRootPath": nil])
|
|
{
|
|
CommandResolver.setProjectRoot(root.path)
|
|
let executable = root.appendingPathComponent("node_modules/.bin/openclaw")
|
|
try makeExecutableForTests(at: executable)
|
|
let script = """
|
|
#!/bin/sh
|
|
action="$2"
|
|
: > "$OPENCLAW_NODE_SERVICE_TEST_ROOT/$action.entered"
|
|
if [ "$action" = "$OPENCLAW_NODE_SERVICE_DELAYED_ACTION" ]; then
|
|
while [ ! -f "$OPENCLAW_NODE_SERVICE_TEST_ROOT/release" ]; do
|
|
sleep 0.01
|
|
done
|
|
fi
|
|
if [ "$action" = "stop" ]; then
|
|
printf stopped > "$OPENCLAW_NODE_SERVICE_TEST_ROOT/state"
|
|
else
|
|
printf running > "$OPENCLAW_NODE_SERVICE_TEST_ROOT/state"
|
|
fi
|
|
printf '{"ok":true}'
|
|
"""
|
|
try script.write(to: executable, atomically: false, encoding: .utf8)
|
|
|
|
let release = root.appendingPathComponent("release")
|
|
defer { try? Data().write(to: release) }
|
|
let profile = AppProfile(environment: [:])
|
|
let previous = Task { await self.runNodeServiceAction(previousAction, profile: profile) }
|
|
let previousEntered = root.appendingPathComponent("\(previousAction).entered")
|
|
for _ in 0..<200 where !FileManager.default.fileExists(atPath: previousEntered.path) {
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
try #require(FileManager.default.fileExists(atPath: previousEntered.path))
|
|
|
|
let next = Task { await self.runNodeServiceAction(nextAction, profile: profile) }
|
|
let nextEntered = root.appendingPathComponent("\(nextAction).entered")
|
|
for _ in 0..<50 where !FileManager.default.fileExists(atPath: nextEntered.path) {
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
}
|
|
#expect(!FileManager.default.fileExists(atPath: nextEntered.path))
|
|
|
|
try Data().write(to: release)
|
|
#expect(await previous.value == nil)
|
|
#expect(await next.value == nil)
|
|
let observed = try String(contentsOf: root.appendingPathComponent("state"), encoding: .utf8)
|
|
#expect(observed == (nextAction == "stop" ? "stopped" : "running"))
|
|
}
|
|
}
|
|
|
|
@Test func `reads node service ownership command directly from launchd`() throws {
|
|
let url = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("openclaw-node-\(UUID().uuidString).plist")
|
|
defer { try? FileManager.default.removeItem(at: url) }
|
|
let arguments = [
|
|
"/Users/Test/.openclaw/tools/node/bin/node",
|
|
"/Users/Test/.openclaw/lib/node_modules/openclaw/dist/index.js",
|
|
"node",
|
|
"run",
|
|
]
|
|
let data = try PropertyListSerialization.data(
|
|
fromPropertyList: ["ProgramArguments": arguments],
|
|
format: .xml,
|
|
options: 0)
|
|
try data.write(to: url, options: .atomic)
|
|
|
|
#expect(NodeServiceManager._testLaunchdProgramArguments(plistURL: url) == arguments)
|
|
try Data("not a plist".utf8).write(to: url, options: .atomic)
|
|
#expect(NodeServiceManager._testLaunchdProgramArguments(plistURL: url) == nil)
|
|
try FileManager.default.removeItem(at: url)
|
|
#expect(NodeServiceManager._testLaunchdProgramArguments(plistURL: url) == [])
|
|
}
|
|
|
|
@Test func `node status requires loaded running service`() {
|
|
#expect(NodeServiceManager._testRuntimeIsRunning(fromJSON: """
|
|
{"service":{"loaded":true,"runtime":{"status":"running"}}}
|
|
"""))
|
|
#expect(!NodeServiceManager._testRuntimeIsRunning(fromJSON: """
|
|
{"service":{"loaded":false,"runtime":{"status":"running"}}}
|
|
"""))
|
|
#expect(!NodeServiceManager._testRuntimeIsRunning(fromJSON: """
|
|
{"service":{"loaded":true,"runtime":{"status":"stopped"}}}
|
|
"""))
|
|
}
|
|
|
|
private func runNodeServiceAction(_ action: String, profile: AppProfile) async -> String? {
|
|
switch action {
|
|
case "start":
|
|
await NodeServiceManager.start(profile: profile)
|
|
case "stop":
|
|
await NodeServiceManager.stop(profile: profile)
|
|
case "restart":
|
|
await NodeServiceManager.restart(profile: profile)
|
|
default:
|
|
"Unknown node service action"
|
|
}
|
|
}
|
|
}
|