refactor(cli): unify on clawdis CLI + node permissions

This commit is contained in:
Peter Steinberger
2025-12-20 02:08:04 +00:00
parent 479720c169
commit 849446ae17
49 changed files with 1205 additions and 2735 deletions

View File

@@ -1,173 +0,0 @@
import ClawdisIPC
import Foundation
import Testing
@testable import Clawdis
@Suite(.serialized)
struct ControlRequestHandlerTests {
private static func withDefaultOverride<T>(
_ key: String,
value: Any?,
operation: () async throws -> T) async rethrows -> T
{
let defaults = UserDefaults.standard
let previous = defaults.object(forKey: key)
if let value {
defaults.set(value, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
defer {
if let previous {
defaults.set(previous, forKey: key)
} else {
defaults.removeObject(forKey: key)
}
}
return try await operation()
}
@Test
func statusReturnsReadyWhenNotPaused() async throws {
let defaults = UserDefaults.standard
let previous = defaults.object(forKey: pauseDefaultsKey)
defaults.set(false, forKey: pauseDefaultsKey)
defer {
if let previous {
defaults.set(previous, forKey: pauseDefaultsKey)
} else {
defaults.removeObject(forKey: pauseDefaultsKey)
}
}
let res = try await ControlRequestHandler.process(request: .status)
#expect(res.ok == true)
#expect(res.message == "ready")
}
@Test
func statusReturnsPausedWhenPaused() async throws {
let defaults = UserDefaults.standard
let previous = defaults.object(forKey: pauseDefaultsKey)
defaults.set(true, forKey: pauseDefaultsKey)
defer {
if let previous {
defaults.set(previous, forKey: pauseDefaultsKey)
} else {
defaults.removeObject(forKey: pauseDefaultsKey)
}
}
let res = try await ControlRequestHandler.process(request: .status)
#expect(res.ok == false)
#expect(res.message == "clawdis paused")
}
@Test
func nonStatusRequestsShortCircuitWhenPaused() async throws {
let defaults = UserDefaults.standard
let previous = defaults.object(forKey: pauseDefaultsKey)
defaults.set(true, forKey: pauseDefaultsKey)
defer {
if let previous {
defaults.set(previous, forKey: pauseDefaultsKey)
} else {
defaults.removeObject(forKey: pauseDefaultsKey)
}
}
let res = try await ControlRequestHandler.process(request: .rpcStatus)
#expect(res.ok == false)
#expect(res.message == "clawdis paused")
}
@Test
func agentRejectsEmptyMessage() async throws {
let res = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await ControlRequestHandler.process(request: .agent(
message: " ",
thinking: nil,
session: nil,
deliver: false,
to: nil))
}
#expect(res.ok == false)
#expect(res.message == "message empty")
}
@Test
func runShellEchoReturnsPayload() async throws {
let res = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await ControlRequestHandler.process(request: .runShell(
command: ["echo", "hello"],
cwd: nil,
env: nil,
timeoutSec: nil,
needsScreenRecording: false))
}
#expect(res.ok == true)
#expect(String(data: res.payload ?? Data(), encoding: .utf8) == "hello\n")
}
@Test
func cameraRequestsReturnDisabledWhenCameraDisabled() async throws {
let snap = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(cameraEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .cameraSnap(
facing: nil,
maxWidth: nil,
quality: nil,
outPath: nil))
}
}
#expect(snap.ok == false)
#expect(snap.message == "Camera disabled by user")
let clip = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(cameraEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .cameraClip(
facing: nil,
durationMs: nil,
includeAudio: true,
outPath: nil))
}
}
#expect(clip.ok == false)
#expect(clip.message == "Camera disabled by user")
}
@Test
func canvasRequestsReturnDisabledWhenCanvasDisabled() async throws {
let show = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(canvasEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .canvasPresent(session: "s", path: nil, placement: nil))
}
}
#expect(show.ok == false)
#expect(show.message == "Canvas disabled by user")
let eval = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(canvasEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .canvasEval(session: "s", javaScript: "1+1"))
}
}
#expect(eval.ok == false)
#expect(eval.message == "Canvas disabled by user")
let snap = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(canvasEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .canvasSnapshot(session: "s", outPath: nil))
}
}
#expect(snap.ok == false)
#expect(snap.message == "Canvas disabled by user")
let a2ui = try await Self.withDefaultOverride(pauseDefaultsKey, value: false) {
try await Self.withDefaultOverride(canvasEnabledKey, value: false) {
try await ControlRequestHandler.process(request: .canvasA2UI(session: "s", command: .reset, jsonl: nil))
}
}
#expect(a2ui.ok == false)
#expect(a2ui.message == "Canvas disabled by user")
}
}

View File

@@ -1,50 +0,0 @@
import Foundation
import Testing
@testable import Clawdis
@Suite struct ControlSocketServerTests {
private static func codesignTeamIdentifier(executablePath: String) -> String? {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/usr/bin/codesign")
proc.arguments = ["-dv", "--verbose=4", executablePath]
proc.standardOutput = Pipe()
let stderr = Pipe()
proc.standardError = stderr
do {
try proc.run()
proc.waitUntilExit()
} catch {
return nil
}
guard proc.terminationStatus == 0 else {
return nil
}
let data = stderr.fileHandleForReading.readToEndSafely()
guard let text = String(data: data, encoding: .utf8) else { return nil }
for line in text.split(separator: "\n") {
if line.hasPrefix("TeamIdentifier=") {
let raw = String(line.dropFirst("TeamIdentifier=".count))
.trimmingCharacters(in: .whitespacesAndNewlines)
return raw == "not set" ? nil : raw
}
}
return nil
}
@Test func teamIdentifierLookupMatchesCodesign() async {
let pid = getpid()
let execPath = CommandLine.arguments.first ?? ""
let expected = Self.codesignTeamIdentifier(executablePath: execPath)
let actual = ControlSocketServer._testTeamIdentifier(pid: pid)
if let expected, !expected.isEmpty {
#expect(actual == expected)
} else {
#expect(actual == nil || actual?.isEmpty == true)
}
}
}

View File

@@ -1,45 +0,0 @@
import Testing
@testable import Clawdis
@Suite struct NodeListTests {
@Test func nodeListMapsGatewayPayloadIncludingHardwareAndCaps() async {
let payload = ControlRequestHandler.GatewayNodeListPayload(
ts: 123,
nodes: [
ControlRequestHandler.GatewayNodeListPayload.Node(
nodeId: "n1",
displayName: "Node",
platform: "iOS",
version: "1.0",
deviceFamily: "iPad",
modelIdentifier: "iPad14,5",
remoteIp: "192.168.0.88",
connected: true,
paired: true,
caps: ["canvas", "camera"]),
ControlRequestHandler.GatewayNodeListPayload.Node(
nodeId: "n2",
displayName: "Offline",
platform: "iOS",
version: "1.0",
deviceFamily: "iPhone",
modelIdentifier: "iPhone14,2",
remoteIp: nil,
connected: false,
paired: true,
caps: nil),
])
let res = ControlRequestHandler.buildNodeListResult(payload: payload)
#expect(res.ts == 123)
#expect(res.pairedNodeIds.sorted() == ["n1", "n2"])
#expect(res.connectedNodeIds == ["n1"])
let node = res.nodes.first { $0.nodeId == "n1" }
#expect(node?.remoteAddress == "192.168.0.88")
#expect(node?.deviceFamily == "iPad")
#expect(node?.modelIdentifier == "iPad14,5")
#expect(node?.capabilities?.sorted() == ["camera", "canvas"])
}
}