test: expand menu and node coverage
This commit is contained in:
@@ -503,3 +503,40 @@ enum BridgePairingApprover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension BridgeServer {
|
||||
func exerciseForTesting() async {
|
||||
let conn = NWConnection(to: .hostPort(host: "127.0.0.1", port: 22), using: .tcp)
|
||||
let handler = BridgeConnectionHandler(connection: conn, logger: self.logger)
|
||||
self.connections["node-1"] = handler
|
||||
self.nodeInfoById["node-1"] = BridgeNodeInfo(
|
||||
nodeId: "node-1",
|
||||
displayName: "Node One",
|
||||
platform: "macOS",
|
||||
version: "1.0.0",
|
||||
deviceFamily: "Mac",
|
||||
modelIdentifier: "MacBookPro18,1",
|
||||
remoteAddress: "127.0.0.1",
|
||||
caps: ["chat", "voice"])
|
||||
|
||||
_ = self.connectedNodeIds()
|
||||
_ = self.connectedNodes()
|
||||
|
||||
self.handleListenerState(.ready)
|
||||
self.handleListenerState(.failed(NWError.posix(.ECONNREFUSED)))
|
||||
self.handleListenerState(.waiting(NWError.posix(.ETIMEDOUT)))
|
||||
self.handleListenerState(.cancelled)
|
||||
self.handleListenerState(.setup)
|
||||
|
||||
let subscribe = BridgeEventFrame(event: "chat.subscribe", payloadJSON: "{\"sessionKey\":\"main\"}")
|
||||
await self.handleEvent(nodeId: "node-1", evt: subscribe)
|
||||
|
||||
let unsubscribe = BridgeEventFrame(event: "chat.unsubscribe", payloadJSON: "{\"sessionKey\":\"main\"}")
|
||||
await self.handleEvent(nodeId: "node-1", evt: unsubscribe)
|
||||
|
||||
let invalid = BridgeRPCRequest(id: "req-1", method: "invalid.method", paramsJSON: nil)
|
||||
_ = await self.handleRequest(nodeId: "node-1", req: invalid)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -19,6 +19,9 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
private var cachedErrorText: String?
|
||||
private var cacheUpdatedAt: Date?
|
||||
private let refreshIntervalSeconds: TimeInterval = 12
|
||||
#if DEBUG
|
||||
private var testControlChannelConnected: Bool?
|
||||
#endif
|
||||
|
||||
func install(into statusItem: NSStatusItem) {
|
||||
self.statusItem = statusItem
|
||||
@@ -157,6 +160,9 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
}
|
||||
|
||||
private var isControlChannelConnected: Bool {
|
||||
#if DEBUG
|
||||
if let override = self.testControlChannelConnected { return override }
|
||||
#endif
|
||||
if case .connected = ControlChannel.shared.state { return true }
|
||||
return false
|
||||
}
|
||||
@@ -467,6 +473,24 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension MenuSessionsInjector {
|
||||
func setTestingControlChannelConnected(_ connected: Bool?) {
|
||||
self.testControlChannelConnected = connected
|
||||
}
|
||||
|
||||
func setTestingSnapshot(_ snapshot: SessionStoreSnapshot?, errorText: String? = nil) {
|
||||
self.cachedSnapshot = snapshot
|
||||
self.cachedErrorText = errorText
|
||||
self.cacheUpdatedAt = Date()
|
||||
}
|
||||
|
||||
func injectForTesting(into menu: NSMenu) {
|
||||
self.inject(into: menu)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private final class HighlightedMenuItemHostView: NSView {
|
||||
private let baseView: AnyView
|
||||
private let hosting: NSHostingView<AnyView>
|
||||
|
||||
@@ -653,3 +653,40 @@ final class NodePairingApprovalPrompter {
|
||||
self.updateReconcileLoop()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@MainActor
|
||||
extension NodePairingApprovalPrompter {
|
||||
static func exerciseForTesting() async {
|
||||
let prompter = NodePairingApprovalPrompter()
|
||||
let pending = PendingRequest(
|
||||
requestId: "req-1",
|
||||
nodeId: "node-1",
|
||||
displayName: "Node One",
|
||||
platform: "macos",
|
||||
version: "1.0.0",
|
||||
remoteIp: "127.0.0.1",
|
||||
isRepair: false,
|
||||
silent: true,
|
||||
ts: 1_700_000_000_000)
|
||||
let paired = PairedNode(
|
||||
nodeId: "node-1",
|
||||
approvedAtMs: 1_700_000_000_000,
|
||||
displayName: "Node One",
|
||||
platform: "macOS",
|
||||
version: "1.0.0",
|
||||
remoteIp: "127.0.0.1")
|
||||
let list = PairingList(pending: [pending], paired: [paired])
|
||||
|
||||
_ = Self.describe(pending)
|
||||
_ = Self.prettyIP(pending.remoteIp)
|
||||
_ = Self.prettyPlatform(pending.platform)
|
||||
_ = prompter.inferResolution(for: pending, list: list)
|
||||
|
||||
prompter.queue = [pending]
|
||||
_ = prompter.shouldPoll
|
||||
_ = await prompter.trySilentApproveIfPossible(pending)
|
||||
prompter.queue.removeAll()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
10
apps/macos/Tests/ClawdisIPCTests/BridgeServerTests.swift
Normal file
10
apps/macos/Tests/ClawdisIPCTests/BridgeServerTests.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
|
||||
@Suite(.serialized)
|
||||
struct BridgeServerTests {
|
||||
@Test func bridgeServerExercisesPaths() async {
|
||||
let server = BridgeServer()
|
||||
await server.exerciseForTesting()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
32
apps/macos/Tests/ClawdisIPCTests/MacNodeRuntimeTests.swift
Normal file
32
apps/macos/Tests/ClawdisIPCTests/MacNodeRuntimeTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Testing
|
||||
@testable import Clawdis
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct NodePairingApprovalPrompterTests {
|
||||
@Test func nodePairingApprovalPrompterExercises() async {
|
||||
await NodePairingApprovalPrompter.exerciseForTesting()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user