refactor(macos): inject main-actor services into node runtime
This commit is contained in:
@@ -93,34 +93,22 @@ struct LowCoverageHelperTests {
|
||||
_ = PresenceReporter._testPrimaryIPv4Address()
|
||||
}
|
||||
|
||||
@Test func gatewayLaunchAgentHelpers() {
|
||||
let keyBind = "CLAWDBOT_GATEWAY_BIND"
|
||||
let keyToken = "CLAWDBOT_GATEWAY_TOKEN"
|
||||
let previousBind = ProcessInfo.processInfo.environment[keyBind]
|
||||
let previousToken = ProcessInfo.processInfo.environment[keyToken]
|
||||
defer {
|
||||
if let previousBind {
|
||||
setenv(keyBind, previousBind, 1)
|
||||
} else {
|
||||
unsetenv(keyBind)
|
||||
}
|
||||
if let previousToken {
|
||||
setenv(keyToken, previousToken, 1)
|
||||
} else {
|
||||
unsetenv(keyToken)
|
||||
}
|
||||
@Test func gatewayLaunchAgentHelpers() async throws {
|
||||
try await TestIsolation.withEnvValues(
|
||||
[
|
||||
"CLAWDBOT_GATEWAY_BIND": "Lan",
|
||||
"CLAWDBOT_GATEWAY_TOKEN": " secret ",
|
||||
])
|
||||
{
|
||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan")
|
||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret")
|
||||
#expect(
|
||||
GatewayLaunchAgentManager._testEscapePlistValue("a&b<c>\"'") ==
|
||||
"a&b<c>"'")
|
||||
|
||||
#expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdbot")
|
||||
#expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay")
|
||||
}
|
||||
|
||||
setenv(keyBind, "Lan", 1)
|
||||
setenv(keyToken, " secret ", 1)
|
||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan")
|
||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret")
|
||||
#expect(
|
||||
GatewayLaunchAgentManager._testEscapePlistValue("a&b<c>\"'") ==
|
||||
"a&b<c>"'")
|
||||
|
||||
#expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdbot")
|
||||
#expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay")
|
||||
}
|
||||
|
||||
@Test func portGuardianParsesListenersAndBuildsReports() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import ClawdbotKit
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite(.serialized)
|
||||
struct MacNodeRuntimeTests {
|
||||
@Test func handleInvokeRejectsUnknownCommand() async {
|
||||
let runtime = MacNodeRuntime()
|
||||
@@ -31,21 +31,58 @@ struct MacNodeRuntimeTests {
|
||||
}
|
||||
|
||||
@Test func handleInvokeCameraListRequiresEnabledCamera() async {
|
||||
let defaults = UserDefaults.standard
|
||||
let previous = defaults.object(forKey: cameraEnabledKey)
|
||||
defaults.set(false, forKey: cameraEnabledKey)
|
||||
defer {
|
||||
if let previous {
|
||||
defaults.set(previous, forKey: cameraEnabledKey)
|
||||
} else {
|
||||
defaults.removeObject(forKey: cameraEnabledKey)
|
||||
await TestIsolation.withUserDefaultsValues([cameraEnabledKey: false]) {
|
||||
let runtime = MacNodeRuntime()
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(id: "req-4", command: ClawdbotCameraCommand.list.rawValue))
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.message.contains("CAMERA_DISABLED") == true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func handleInvokeScreenRecordUsesInjectedServices() async throws {
|
||||
@MainActor
|
||||
final class FakeMainActorServices: MacNodeRuntimeMainActorServices, @unchecked Sendable {
|
||||
func recordScreen(
|
||||
screenIndex: Int?,
|
||||
durationMs: Int?,
|
||||
fps: Double?,
|
||||
includeAudio: Bool?,
|
||||
outPath: String?) async throws -> (path: String, hasAudio: Bool)
|
||||
{
|
||||
let url = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("clawdbot-test-screen-record-\(UUID().uuidString).mp4")
|
||||
try Data("ok".utf8).write(to: url)
|
||||
return (path: url.path, hasAudio: false)
|
||||
}
|
||||
|
||||
func locationAuthorizationStatus() -> CLAuthorizationStatus { .authorizedAlways }
|
||||
func locationAccuracyAuthorization() -> CLAccuracyAuthorization { .fullAccuracy }
|
||||
func currentLocation(
|
||||
desiredAccuracy: ClawdbotLocationAccuracy,
|
||||
maxAgeMs: Int?,
|
||||
timeoutMs: Int?) async throws -> CLLocation
|
||||
{
|
||||
CLLocation(latitude: 0, longitude: 0)
|
||||
}
|
||||
}
|
||||
|
||||
let runtime = MacNodeRuntime()
|
||||
let services = await MainActor.run { FakeMainActorServices() }
|
||||
let runtime = MacNodeRuntime(makeMainActorServices: { services })
|
||||
|
||||
let params = MacNodeScreenRecordParams(durationMs: 250)
|
||||
let json = String(data: try JSONEncoder().encode(params), encoding: .utf8)
|
||||
let response = await runtime.handleInvoke(
|
||||
BridgeInvokeRequest(id: "req-4", command: ClawdbotCameraCommand.list.rawValue))
|
||||
#expect(response.ok == false)
|
||||
#expect(response.error?.message.contains("CAMERA_DISABLED") == true)
|
||||
BridgeInvokeRequest(id: "req-5", command: MacNodeScreenCommand.record.rawValue, paramsJSON: json))
|
||||
#expect(response.ok == true)
|
||||
let payloadJSON = try #require(response.payloadJSON)
|
||||
|
||||
struct Payload: Decodable {
|
||||
var format: String
|
||||
var base64: String
|
||||
}
|
||||
let payload = try JSONDecoder().decode(Payload.self, from: Data(payloadJSON.utf8))
|
||||
#expect(payload.format == "mp4")
|
||||
#expect(!payload.base64.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ actor TestIsolationLock {
|
||||
next.resume()
|
||||
}
|
||||
|
||||
func withLock<T>(_ body: () async throws -> T) async rethrows -> T {
|
||||
func withLock<T: Sendable>(_ body: @Sendable () async throws -> T) async rethrows -> T {
|
||||
await self.lock()
|
||||
defer { self.unlock() }
|
||||
return try await body()
|
||||
@@ -34,10 +34,10 @@ actor TestIsolationLock {
|
||||
}
|
||||
|
||||
enum TestIsolation {
|
||||
static func withIsolatedState<T>(
|
||||
static func withIsolatedState<T: Sendable>(
|
||||
env: [String: String?] = [:],
|
||||
defaults: [String: Any?] = [:],
|
||||
_ body: () async throws -> T) async rethrows -> T
|
||||
_ body: @Sendable () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await TestIsolationLock.shared.withLock {
|
||||
var previousEnv: [String: String?] = [:]
|
||||
@@ -82,16 +82,16 @@ enum TestIsolation {
|
||||
}
|
||||
}
|
||||
|
||||
static func withEnvValues<T>(
|
||||
static func withEnvValues<T: Sendable>(
|
||||
_ values: [String: String?],
|
||||
_ body: () async throws -> T) async rethrows -> T
|
||||
_ body: @Sendable () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await Self.withIsolatedState(env: values, defaults: [:], body)
|
||||
}
|
||||
|
||||
static func withUserDefaultsValues<T>(
|
||||
static func withUserDefaultsValues<T: Sendable>(
|
||||
_ values: [String: Any?],
|
||||
_ body: () async throws -> T) async rethrows -> T
|
||||
_ body: @Sendable () async throws -> T) async rethrows -> T
|
||||
{
|
||||
try await Self.withIsolatedState(env: [:], defaults: values, body)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user