test: expand menu and node coverage

This commit is contained in:
Peter Steinberger
2025-12-24 17:42:24 +01:00
parent deec315f6a
commit 49e466dd40
9 changed files with 272 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
import Testing
@testable import Clawdis
@Suite(.serialized)
struct BridgeServerTests {
@Test func bridgeServerExercisesPaths() async {
let server = BridgeServer()
await server.exerciseForTesting()
}
}

View File

@@ -1,3 +1,4 @@
import Darwin
import Foundation
import Network
import Testing
@@ -39,17 +40,7 @@ import Testing
@MainActor
@Test func probeEndpointFailsForClosedPort() async throws {
let listener = try NWListener(using: .tcp, on: .any)
listener.start(queue: DispatchQueue(label: "com.steipete.clawdis.tests.bridge-listener-close"))
try await waitForListenerReady(listener, timeoutSeconds: 1.0)
let port = listener.port
listener.cancel()
try await Task.sleep(nanoseconds: 150_000_000)
guard let port else {
throw TestError(message: "listener port missing")
}
let port = try reserveEphemeralPort()
let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: port)
let ok = await MacNodeModeCoordinator.probeEndpoint(endpoint, timeoutSeconds: 0.4)
#expect(ok == false)
@@ -118,3 +109,43 @@ private func withEnv(_ key: String, value: String?, _ body: () -> Void) {
}
body()
}
private func reserveEphemeralPort() throws -> NWEndpoint.Port {
let fd = socket(AF_INET, SOCK_STREAM, 0)
if fd < 0 {
throw TestError(message: "socket failed")
}
defer { close(fd) }
var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = in_port_t(0)
addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
let bindResult = withUnsafePointer(to: &addr) { pointer -> Int32 in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
Darwin.bind(fd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
if bindResult != 0 {
throw TestError(message: "bind failed")
}
var resolved = sockaddr_in()
var length = socklen_t(MemoryLayout<sockaddr_in>.size)
let nameResult = withUnsafeMutablePointer(to: &resolved) { pointer -> Int32 in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
getsockname(fd, $0, &length)
}
}
if nameResult != 0 {
throw TestError(message: "getsockname failed")
}
let port = UInt16(bigEndian: resolved.sin_port)
guard let endpointPort = NWEndpoint.Port(rawValue: port), endpointPort.rawValue != 0 else {
throw TestError(message: "ephemeral port missing")
}
return endpointPort
}

View File

@@ -0,0 +1,32 @@
import ClawdisKit
import Foundation
import Testing
@testable import Clawdis
@Suite(.serialized)
struct MacNodeRuntimeTests {
@Test func handleInvokeRejectsUnknownCommand() async {
let runtime = MacNodeRuntime()
let response = await runtime.handleInvoke(
BridgeInvokeRequest(id: "req-1", command: "unknown.command"))
#expect(response.ok == false)
}
@Test func handleInvokeRejectsEmptySystemRun() async throws {
let runtime = MacNodeRuntime()
let params = ClawdisSystemRunParams(command: [])
let json = String(data: try JSONEncoder().encode(params), encoding: .utf8)
let response = await runtime.handleInvoke(
BridgeInvokeRequest(id: "req-2", command: ClawdisSystemCommand.run.rawValue, paramsJSON: json))
#expect(response.ok == false)
}
@Test func handleInvokeRejectsEmptyNotification() async throws {
let runtime = MacNodeRuntime()
let params = ClawdisSystemNotifyParams(title: "", body: "")
let json = String(data: try JSONEncoder().encode(params), encoding: .utf8)
let response = await runtime.handleInvoke(
BridgeInvokeRequest(id: "req-3", command: ClawdisSystemCommand.notify.rawValue, paramsJSON: json))
#expect(response.ok == false)
}
}

View File

@@ -25,4 +25,17 @@ struct MenuContentSmokeTests {
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
@Test func menuContentBuildsBodyWithDebugAndCanvas() {
let state = AppState(preview: true)
state.connectionMode = .local
state.debugPaneEnabled = true
state.canvasEnabled = true
state.canvasPanelVisible = true
state.swabbleEnabled = true
state.voicePushToTalkEnabled = true
state.heartbeatsEnabled = true
let view = MenuContent(state: state, updater: nil)
_ = view.body
}
}

View File

@@ -0,0 +1,67 @@
import AppKit
import Testing
@testable import Clawdis
@Suite(.serialized)
@MainActor
struct MenuSessionsInjectorTests {
@Test func injectsDisconnectedMessage() {
let injector = MenuSessionsInjector()
injector.setTestingControlChannelConnected(false)
injector.setTestingSnapshot(nil, errorText: nil)
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
injector.injectForTesting(into: menu)
#expect(menu.items.contains { $0.tag == 9_415_557 })
}
@Test func injectsSessionRows() {
let injector = MenuSessionsInjector()
injector.setTestingControlChannelConnected(true)
let defaults = SessionDefaults(model: "claude-opus-4-5", contextTokens: 200_000)
let rows = [
SessionRow(
id: "main",
key: "main",
kind: .direct,
updatedAt: Date(),
sessionId: "s1",
thinkingLevel: "low",
verboseLevel: nil,
systemSent: false,
abortedLastRun: false,
tokens: SessionTokenStats(input: 10, output: 20, total: 30, contextTokens: 200_000),
model: "claude-opus-4-5"),
SessionRow(
id: "group:alpha",
key: "group:alpha",
kind: .group,
updatedAt: Date(timeIntervalSinceNow: -60),
sessionId: "s2",
thinkingLevel: "high",
verboseLevel: "debug",
systemSent: true,
abortedLastRun: true,
tokens: SessionTokenStats(input: 50, output: 50, total: 100, contextTokens: 200_000),
model: "claude-opus-4-5"),
]
let snapshot = SessionStoreSnapshot(
storePath: "/tmp/sessions.json",
defaults: defaults,
rows: rows)
injector.setTestingSnapshot(snapshot, errorText: nil)
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
menu.addItem(.separator())
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
injector.injectForTesting(into: menu)
#expect(menu.items.contains { $0.tag == 9_415_557 })
}
}

View File

@@ -0,0 +1,10 @@
import Testing
@testable import Clawdis
@Suite(.serialized)
@MainActor
struct NodePairingApprovalPrompterTests {
@Test func nodePairingApprovalPrompterExercises() async {
await NodePairingApprovalPrompter.exerciseForTesting()
}
}