diff --git a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift index ad4f8cfab..4fe454bec 100644 --- a/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift +++ b/apps/macos/Sources/Clawdis/CanvasSchemeHandler.swift @@ -240,3 +240,20 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { } } } + +#if DEBUG +extension CanvasSchemeHandler { + func _testResponse(for url: URL) -> (mime: String, data: Data) { + let response = self.response(for: url) + return (response.mime, response.data) + } + + func _testResolveFileURL(sessionRoot: URL, requestPath: String) -> URL? { + self.resolveFileURL(sessionRoot: sessionRoot, requestPath: requestPath) + } + + func _testTextEncodingName(for mimeType: String) -> String? { + self.textEncodingName(forMimeType: mimeType) + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/CanvasWindow.swift b/apps/macos/Sources/Clawdis/CanvasWindow.swift index 0a111e9d4..dcc994a6c 100644 --- a/apps/macos/Sources/Clawdis/CanvasWindow.swift +++ b/apps/macos/Sources/Clawdis/CanvasWindow.swift @@ -748,7 +748,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan } } - private static func isLocalNetworkCanvasURL(_ url: URL) -> Bool { + fileprivate static func isLocalNetworkCanvasURL(_ url: URL) -> Bool { guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return false } @@ -766,7 +766,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan return false } - private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { + fileprivate static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { let parts = host.split(separator: ".", omittingEmptySubsequences: false) guard parts.count == 4 else { return nil } let bytes: [UInt8] = parts.compactMap { UInt8($0) } @@ -774,7 +774,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan return (bytes[0], bytes[1], bytes[2], bytes[3]) } - private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { + fileprivate static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { let (a, b, _, _) = ip if a == 10 { return true } if a == 172, (16...31).contains(Int(b)) { return true } @@ -1012,3 +1012,40 @@ private final class HoverChromeContainerView: NSView { } } } + +#if DEBUG +extension CanvasWindowController { + static func _testSanitizeSessionKey(_ key: String) -> String { + self.sanitizeSessionKey(key) + } + + static func _testJSStringLiteral(_ value: String) -> String { + self.jsStringLiteral(value) + } + + static func _testJSOptionalStringLiteral(_ value: String?) -> String { + self.jsOptionalStringLiteral(value) + } + + static func _testStoredFrameKey(sessionKey: String) -> String { + self.storedFrameDefaultsKey(sessionKey: sessionKey) + } + + static func _testStoreAndLoadFrame(sessionKey: String, frame: NSRect) -> NSRect? { + self.storeRestoredFrame(frame, sessionKey: sessionKey) + return self.loadRestoredFrame(sessionKey: sessionKey) + } + + static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? { + CanvasA2UIActionMessageHandler.parseIPv4(host) + } + + static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool { + CanvasA2UIActionMessageHandler.isLocalNetworkIPv4(ip) + } + + static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool { + CanvasA2UIActionMessageHandler.isLocalNetworkCanvasURL(url) + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/DebugSettings.swift b/apps/macos/Sources/Clawdis/DebugSettings.swift index d69602b19..35f4a40f3 100644 --- a/apps/macos/Sources/Clawdis/DebugSettings.swift +++ b/apps/macos/Sources/Clawdis/DebugSettings.swift @@ -874,4 +874,56 @@ struct DebugSettings_Previews: PreviewProvider { .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight) } } + +@MainActor +extension DebugSettings { + static func exerciseForTesting() async { + var view = DebugSettings() + view.modelsCount = 3 + view.modelsLoading = false + view.modelsError = "Failed to load models" + view.gatewayRootInput = "/tmp/clawdis" + view.sessionStorePath = "/tmp/sessions.json" + view.sessionStoreSaveError = "Save failed" + view.debugSendInFlight = true + view.debugSendStatus = "Sent" + view.debugSendError = "Failed" + view.portCheckInFlight = true + view.portReports = [ + DebugActions.PortReport( + port: 18789, + expected: "Gateway websocket (node/tsx)", + status: .missing("Missing"), + listeners: []), + ] + view.portKillStatus = "Killed" + view.pendingKill = DebugActions.PortListener( + pid: 1, + command: "node", + fullCommand: "node", + user: nil, + expected: true) + view.canvasSessionKey = "main" + view.canvasStatus = "Canvas ok" + view.canvasError = "Canvas error" + view.canvasEvalJS = "document.title" + view.canvasEvalResult = "Canvas" + view.canvasSnapshotPath = "/tmp/snapshot.png" + + _ = view.body + _ = view.header + _ = view.appInfoSection + _ = view.gatewaySection + _ = view.logsSection + _ = view.portsSection + _ = view.pathsSection + _ = view.quickActionsSection + _ = view.canvasSection + _ = view.experimentsSection + _ = view.gridLabel("Test") + + view.loadSessionStorePath() + await view.reloadModels() + } +} #endif diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift index 4de7a9f01..60b4afb0f 100644 --- a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift @@ -159,3 +159,23 @@ enum GatewayLaunchAgentManager { }.value } } + +#if DEBUG +extension GatewayLaunchAgentManager { + static func _testGatewayExecutablePath(bundlePath: String) -> String { + self.gatewayExecutablePath(bundlePath: bundlePath) + } + + static func _testRelayDir(bundlePath: String) -> String { + self.relayDir(bundlePath: bundlePath) + } + + static func _testPreferredGatewayBind() -> String? { + self.preferredGatewayBind() + } + + static func _testPreferredGatewayToken() -> String? { + self.preferredGatewayToken() + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/MenuContextCardInjector.swift b/apps/macos/Sources/Clawdis/MenuContextCardInjector.swift index 24094c602..f469ca348 100644 --- a/apps/macos/Sources/Clawdis/MenuContextCardInjector.swift +++ b/apps/macos/Sources/Clawdis/MenuContextCardInjector.swift @@ -204,3 +204,25 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate { self.lastKnownMenuWidth = max(300, targetWidth) } } + +#if DEBUG +extension MenuContextCardInjector { + func _testSetCache(rows: [SessionRow], errorText: String?, updatedAt: Date?) { + self.cachedRows = rows + self.cacheErrorText = errorText + self.cacheUpdatedAt = updatedAt + } + + func _testFindInsertIndex(in menu: NSMenu) -> Int? { + self.findInsertIndex(in: menu) + } + + func _testInitialCardWidth(for menu: NSMenu) -> CGFloat { + self.initialCardWidth(for: menu) + } + + func _testCachedView() -> AnyView { + self.cachedView() + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/PortGuardian.swift b/apps/macos/Sources/Clawdis/PortGuardian.swift index ecbaf8310..0cdbb5d15 100644 --- a/apps/macos/Sources/Clawdis/PortGuardian.swift +++ b/apps/macos/Sources/Clawdis/PortGuardian.swift @@ -153,58 +153,7 @@ actor PortGuardian { for port in ports { let listeners = await self.listeners(on: port) - let expectedDesc: String - let okPredicate: (Listener) -> Bool - let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"] - - switch mode { - case .remote: - expectedDesc = "SSH tunnel to remote gateway" - okPredicate = { $0.command.lowercased().contains("ssh") } - case .local: - expectedDesc = "Gateway websocket (node/tsx)" - okPredicate = { listener in - let c = listener.command.lowercased() - return expectedCommands.contains { c.contains($0) } - } - case .unconfigured: - expectedDesc = "Gateway not configured" - okPredicate = { _ in false } - } - - if listeners.isEmpty { - let text = "Nothing is listening on \(port) (\(expectedDesc))." - reports.append(.init(port: port, expected: expectedDesc, status: .missing(text), listeners: [])) - continue - } - - let reportListeners = listeners.map { listener in - ReportListener( - pid: listener.pid, - command: listener.command, - fullCommand: listener.fullCommand, - user: listener.user, - expected: okPredicate(listener)) - } - - let offenders = reportListeners.filter { !$0.expected } - if offenders.isEmpty { - let list = listeners.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") - let okText = "Port \(port) is served by \(list)." - reports.append(.init( - port: port, - expected: expectedDesc, - status: .ok(okText), - listeners: reportListeners)) - } else { - let list = offenders.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") - let reason = "Port \(port) is held by \(list), expected \(expectedDesc)." - reports.append(.init( - port: port, - expected: expectedDesc, - status: .interference(reason, offenders: offenders), - listeners: reportListeners)) - } + reports.append(Self.buildReport(port: port, listeners: listeners, mode: mode)) } return reports @@ -218,6 +167,29 @@ actor PortGuardian { timeout: 5) guard res.ok, let data = res.payload, !data.isEmpty else { return [] } let text = String(data: data, encoding: .utf8) ?? "" + return Self.parseListeners(from: text) + } + + private static func readFullCommand(pid: Int32) -> String? { + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/bin/ps") + proc.arguments = ["-p", "\(pid)", "-o", "command="] + let pipe = Pipe() + proc.standardOutput = pipe + proc.standardError = Pipe() + do { + try proc.run() + proc.waitUntilExit() + } catch { + return nil + } + let data = pipe.fileHandleForReading.readToEndSafely() + guard !data.isEmpty else { return nil } + return String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + private static func parseListeners(from text: String) -> [Listener] { var listeners: [Listener] = [] var currentPid: Int32? var currentCmd: String? @@ -252,23 +224,62 @@ actor PortGuardian { return listeners } - private static func readFullCommand(pid: Int32) -> String? { - let proc = Process() - proc.executableURL = URL(fileURLWithPath: "/bin/ps") - proc.arguments = ["-p", "\(pid)", "-o", "command="] - let pipe = Pipe() - proc.standardOutput = pipe - proc.standardError = Pipe() - do { - try proc.run() - proc.waitUntilExit() - } catch { - return nil + private static func buildReport( + port: Int, + listeners: [Listener], + mode: AppState.ConnectionMode) -> PortReport + { + let expectedDesc: String + let okPredicate: (Listener) -> Bool + let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"] + + switch mode { + case .remote: + expectedDesc = "SSH tunnel to remote gateway" + okPredicate = { $0.command.lowercased().contains("ssh") } + case .local: + expectedDesc = "Gateway websocket (node/tsx)" + okPredicate = { listener in + let c = listener.command.lowercased() + return expectedCommands.contains { c.contains($0) } + } + case .unconfigured: + expectedDesc = "Gateway not configured" + okPredicate = { _ in false } } - let data = pipe.fileHandleForReading.readToEndSafely() - guard !data.isEmpty else { return nil } - return String(data: data, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) + + if listeners.isEmpty { + let text = "Nothing is listening on \(port) (\(expectedDesc))." + return .init(port: port, expected: expectedDesc, status: .missing(text), listeners: []) + } + + let reportListeners = listeners.map { listener in + ReportListener( + pid: listener.pid, + command: listener.command, + fullCommand: listener.fullCommand, + user: listener.user, + expected: okPredicate(listener)) + } + + let offenders = reportListeners.filter { !$0.expected } + if offenders.isEmpty { + let list = listeners.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") + let okText = "Port \(port) is served by \(list)." + return .init( + port: port, + expected: expectedDesc, + status: .ok(okText), + listeners: reportListeners) + } + + let list = offenders.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ") + let reason = "Port \(port) is held by \(list), expected \(expectedDesc)." + return .init( + port: port, + expected: expectedDesc, + status: .interference(reason, offenders: offenders), + listeners: reportListeners) } private static func executablePath(for pid: Int32) -> String? { @@ -319,3 +330,20 @@ actor PortGuardian { try? data.write(to: Self.recordPath, options: [.atomic]) } } + +#if DEBUG +extension PortGuardian { + static func _testParseListeners(_ text: String) -> [(pid: Int32, command: String, fullCommand: String, user: String?)] { + Self.parseListeners(from: text).map { ($0.pid, $0.command, $0.fullCommand, $0.user) } + } + + static func _testBuildReport( + port: Int, + mode: AppState.ConnectionMode, + listeners: [(pid: Int32, command: String, fullCommand: String, user: String?)]) -> PortReport + { + let mapped = listeners.map { Listener(pid: $0.pid, command: $0.command, fullCommand: $0.fullCommand, user: $0.user) } + return Self.buildReport(port: port, listeners: mapped, mode: mode) + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/PresenceReporter.swift b/apps/macos/Sources/Clawdis/PresenceReporter.swift index 4a03edd34..2ec0c9d89 100644 --- a/apps/macos/Sources/Clawdis/PresenceReporter.swift +++ b/apps/macos/Sources/Clawdis/PresenceReporter.swift @@ -132,3 +132,27 @@ final class PresenceReporter { return en0 ?? fallback } } + +#if DEBUG +extension PresenceReporter { + static func _testComposePresenceSummary(mode: String, reason: String) -> String { + self.composePresenceSummary(mode: mode, reason: reason) + } + + static func _testAppVersionString() -> String { + self.appVersionString() + } + + static func _testPlatformString() -> String { + self.platformString() + } + + static func _testLastInputSeconds() -> Int? { + self.lastInputSeconds() + } + + static func _testPrimaryIPv4Address() -> String? { + self.primaryIPv4Address() + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/ViewMetrics.swift b/apps/macos/Sources/Clawdis/ViewMetrics.swift index 9b9615c8f..dfd7180de 100644 --- a/apps/macos/Sources/Clawdis/ViewMetrics.swift +++ b/apps/macos/Sources/Clawdis/ViewMetrics.swift @@ -17,3 +17,13 @@ extension View { .onPreferenceChange(ViewWidthPreferenceKey.self, perform: onChange) } } + +#if DEBUG +enum ViewMetricsTesting { + static func reduceWidth(current: CGFloat, next: CGFloat) -> CGFloat { + var value = current + ViewWidthPreferenceKey.reduce(value: &value, nextValue: { next }) + return value + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift index 0e57e41ac..ee694f6ad 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeSettings.swift @@ -510,4 +510,33 @@ struct VoiceWakeSettings_Previews: PreviewProvider { .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight) } } + +@MainActor +extension VoiceWakeSettings { + static func exerciseForTesting() { + let state = AppState(preview: true) + state.swabbleEnabled = true + state.voicePushToTalkEnabled = true + state.swabbleTriggerWords = ["Claude", "Hey"] + + var view = VoiceWakeSettings(state: state) + view.availableMics = [AudioInputDevice(uid: "mic-1", name: "Built-in")] + view.availableLocales = [Locale(identifier: "en_US")] + view.meterLevel = 0.42 + view.meterError = "No input" + view.testState = .detected("ok") + view.isTesting = true + + _ = view.body + _ = view.localePicker + _ = view.micPicker + _ = view.levelMeter + _ = view.triggerTable + _ = view.chimeSection + + view.addWord() + _ = view.binding(for: 0).wrappedValue + view.removeWord(at: 0) + } +} #endif diff --git a/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift b/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift new file mode 100644 index 000000000..92a7b891c --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift @@ -0,0 +1,224 @@ +import AppKit +import Foundation +import Testing + +@testable import Clawdis + +@Suite(.serialized) +struct LowCoverageHelperTests { + @Test func anyCodableHelperAccessors() throws { + let payload: [String: AnyCodable] = [ + "title": AnyCodable("Hello"), + "flag": AnyCodable(true), + "count": AnyCodable(3), + "ratio": AnyCodable(1.25), + "list": AnyCodable([AnyCodable("a"), AnyCodable(2)]), + ] + let any = AnyCodable(payload) + let dict = try #require(any.dictionaryValue) + #expect(dict["title"]?.stringValue == "Hello") + #expect(dict["flag"]?.boolValue == true) + #expect(dict["count"]?.intValue == 3) + #expect(dict["ratio"]?.doubleValue == 1.25) + #expect(dict["list"]?.arrayValue?.count == 2) + + let foundation = any.foundationValue as? [String: Any] + #expect(foundation?["title"] as? String == "Hello") + } + + @Test func attributedStringStripsForegroundColor() { + let text = NSMutableAttributedString(string: "Test") + text.addAttribute(.foregroundColor, value: NSColor.red, range: NSRange(location: 0, length: 4)) + let stripped = text.strippingForegroundColor() + let color = stripped.attribute(.foregroundColor, at: 0, effectiveRange: nil) + #expect(color == nil) + } + + @Test func viewMetricsReduceWidth() { + let value = ViewMetricsTesting.reduceWidth(current: 120, next: 180) + #expect(value == 180) + } + + @Test func shellExecutorHandlesEmptyCommand() async { + let result = await ShellExecutor.runDetailed(command: [], cwd: nil, env: nil, timeout: nil) + #expect(result.success == false) + #expect(result.errorMessage != nil) + } + + @Test func shellExecutorRunsCommand() async { + let result = await ShellExecutor.runDetailed(command: ["/bin/echo", "ok"], cwd: nil, env: nil, timeout: 2) + #expect(result.success == true) + #expect(result.stdout.contains("ok") || result.stderr.contains("ok")) + } + + @Test func shellExecutorTimesOut() async { + let result = await ShellExecutor.runDetailed(command: ["/bin/sleep", "1"], cwd: nil, env: nil, timeout: 0.05) + #expect(result.timedOut == true) + } + + @Test func pairedNodesStorePersists() async throws { + let dir = FileManager.default.temporaryDirectory + .appendingPathComponent("paired-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + let url = dir.appendingPathComponent("nodes.json") + let store = PairedNodesStore(fileURL: url) + await store.load() + #expect(await store.all().isEmpty) + + let node = PairedNode( + nodeId: "node-1", + displayName: "Node One", + platform: "macOS", + version: "1.0", + deviceFamily: "Mac", + modelIdentifier: "MacBookPro", + token: "token", + createdAtMs: 1, + lastSeenAtMs: nil) + try await store.upsert(node) + #expect(await store.find(nodeId: "node-1")?.displayName == "Node One") + + try await store.touchSeen(nodeId: "node-1") + let updated = await store.find(nodeId: "node-1") + #expect(updated?.lastSeenAtMs != nil) + } + + @Test @MainActor func presenceReporterHelpers() { + let summary = PresenceReporter._testComposePresenceSummary(mode: "local", reason: "test") + #expect(summary.contains("mode local")) + #expect(!PresenceReporter._testAppVersionString().isEmpty) + #expect(!PresenceReporter._testPlatformString().isEmpty) + _ = PresenceReporter._testLastInputSeconds() + _ = PresenceReporter._testPrimaryIPv4Address() + } + + @Test func gatewayLaunchAgentHelpers() { + let keyBind = "CLAWDIS_GATEWAY_BIND" + let keyToken = "CLAWDIS_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) + } + } + + setenv(keyBind, "Lan", 1) + setenv(keyToken, " secret ", 1) + #expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan") + #expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret") + + #expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis") + #expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay") + } + + @Test func portGuardianParsesListenersAndBuildsReports() { + let output = """ + p123 + cnode + uuser + p456 + cssh + uroot + """ + let listeners = PortGuardian._testParseListeners(output) + #expect(listeners.count == 2) + #expect(listeners[0].command == "node") + #expect(listeners[1].command == "ssh") + + let okReport = PortGuardian._testBuildReport( + port: 18789, + mode: .local, + listeners: [(pid: 1, command: "node", fullCommand: "node", user: "me")]) + #expect(okReport.offenders.isEmpty) + + let badReport = PortGuardian._testBuildReport( + port: 18789, + mode: .local, + listeners: [(pid: 2, command: "python", fullCommand: "python", user: "me")]) + #expect(!badReport.offenders.isEmpty) + + let emptyReport = PortGuardian._testBuildReport(port: 18789, mode: .local, listeners: []) + #expect(emptyReport.summary.contains("Nothing is listening")) + } + + @Test @MainActor func canvasSchemeHandlerResolvesFilesAndErrors() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: root) } + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + let session = root.appendingPathComponent("main", isDirectory: true) + try FileManager.default.createDirectory(at: session, withIntermediateDirectories: true) + + let index = session.appendingPathComponent("index.html") + try "

Hello

".write(to: index, atomically: true, encoding: .utf8) + + let handler = CanvasSchemeHandler(root: root) + let url = try #require(CanvasScheme.makeURL(session: "main", path: "index.html")) + let response = handler._testResponse(for: url) + #expect(response.mime == "text/html") + #expect(String(data: response.data, encoding: .utf8)?.contains("Hello") == true) + + let invalid = URL(string: "https://example.com")! + let invalidResponse = handler._testResponse(for: invalid) + #expect(invalidResponse.mime == "text/html") + + let missing = try #require(CanvasScheme.makeURL(session: "missing", path: "/")) + let missingResponse = handler._testResponse(for: missing) + #expect(missingResponse.mime == "text/html") + + #expect(handler._testTextEncodingName(for: "text/html") == "utf-8") + #expect(handler._testTextEncodingName(for: "application/octet-stream") == nil) + } + + @Test @MainActor func menuContextCardInjectorInsertsAndFindsIndex() { + let injector = MenuContextCardInjector() + let menu = NSMenu() + menu.minimumWidth = 280 + menu.addItem(NSMenuItem(title: "Active", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "Quit", action: nil, keyEquivalent: "q")) + + let idx = injector._testFindInsertIndex(in: menu) + #expect(idx == 1) + #expect(injector._testInitialCardWidth(for: menu) >= 300) + + injector._testSetCache(rows: [SessionRow.previewRows[0]], errorText: nil, updatedAt: Date()) + injector.menuWillOpen(menu) + injector.menuDidClose(menu) + + let fallbackMenu = NSMenu() + fallbackMenu.addItem(NSMenuItem(title: "First", action: nil, keyEquivalent: "")) + #expect(injector._testFindInsertIndex(in: fallbackMenu) == 1) + } + + @Test @MainActor func canvasWindowHelperFunctions() { + #expect(CanvasWindowController._testSanitizeSessionKey(" main ") == "main") + #expect(CanvasWindowController._testSanitizeSessionKey("bad/..") == "bad___") + #expect(CanvasWindowController._testJSOptionalStringLiteral(nil) == "null") + + let rect = NSRect(x: 10, y: 12, width: 400, height: 420) + let key = CanvasWindowController._testStoredFrameKey(sessionKey: "test") + let loaded = CanvasWindowController._testStoreAndLoadFrame(sessionKey: "test", frame: rect) + UserDefaults.standard.removeObject(forKey: key) + #expect(loaded?.size.width == rect.size.width) + + let parsed = CanvasWindowController._testParseIPv4("192.168.1.2") + #expect(parsed != nil) + if let parsed { + #expect(CanvasWindowController._testIsLocalNetworkIPv4(parsed)) + } + + let url = URL(string: "http://192.168.1.2")! + #expect(CanvasWindowController._testIsLocalNetworkCanvasURL(url)) + #expect(CanvasWindowController._testParseIPv4("not-an-ip") == nil) + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/LowCoverageViewSmokeTests.swift b/apps/macos/Tests/ClawdisIPCTests/LowCoverageViewSmokeTests.swift new file mode 100644 index 000000000..7fb60fcf8 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/LowCoverageViewSmokeTests.swift @@ -0,0 +1,98 @@ +import AppKit +import SwiftUI +import Testing + +@testable import Clawdis + +@Suite(.serialized) +@MainActor +struct LowCoverageViewSmokeTests { + @Test func contextMenuCardBuildsBody() { + let loading = ContextMenuCardView(rows: [], statusText: "Loading…", isLoading: true) + _ = loading.body + + let empty = ContextMenuCardView(rows: [], statusText: nil, isLoading: false) + _ = empty.body + + let withRows = ContextMenuCardView(rows: SessionRow.previewRows, statusText: nil, isLoading: false) + _ = withRows.body + } + + @Test func settingsToggleRowBuildsBody() { + var flag = false + let binding = Binding(get: { flag }, set: { flag = $0 }) + let view = SettingsToggleRow(title: "Enable", subtitle: "Detail", binding: binding) + _ = view.body + } + + @Test func voiceWakeTestCardBuildsBodyAcrossStates() { + var state = VoiceWakeTestState.idle + var isTesting = false + let stateBinding = Binding(get: { state }, set: { state = $0 }) + let testingBinding = Binding(get: { isTesting }, set: { isTesting = $0 }) + + _ = VoiceWakeTestCard(testState: stateBinding, isTesting: testingBinding, onToggle: {}).body + + state = .hearing("hello") + _ = VoiceWakeTestCard(testState: stateBinding, isTesting: testingBinding, onToggle: {}).body + + state = .detected("command") + isTesting = true + _ = VoiceWakeTestCard(testState: stateBinding, isTesting: testingBinding, onToggle: {}).body + + state = .failed("No mic") + _ = VoiceWakeTestCard(testState: stateBinding, isTesting: testingBinding, onToggle: {}).body + } + + @Test func agentEventsWindowBuildsBodyWithEvent() { + AgentEventStore.shared.clear() + let sample = ControlAgentEvent( + runId: "run-1", + seq: 1, + stream: "tool", + ts: Date().timeIntervalSince1970 * 1000, + data: ["phase": AnyCodable("start"), "name": AnyCodable("test")], + summary: nil) + AgentEventStore.shared.append(sample) + _ = AgentEventsWindow().body + AgentEventStore.shared.clear() + } + + @Test func notifyOverlayPresentsAndDismisses() async { + let controller = NotifyOverlayController() + controller.present(title: "Hello", body: "World", autoDismissAfter: 0) + controller.present(title: "Updated", body: "Again", autoDismissAfter: 0) + controller.dismiss() + try? await Task.sleep(nanoseconds: 250_000_000) + } + + @Test func visualEffectViewHostsInNSHostingView() { + let hosting = NSHostingView(rootView: VisualEffectView(material: .sidebar)) + _ = hosting.fittingSize + hosting.rootView = VisualEffectView(material: .popover, emphasized: true) + _ = hosting.fittingSize + } + + @Test func menuHostedItemHostsContent() { + let view = MenuHostedItem(width: 240, rootView: AnyView(Text("Menu"))) + let hosting = NSHostingView(rootView: view) + _ = hosting.fittingSize + hosting.rootView = MenuHostedItem(width: 320, rootView: AnyView(Text("Updated"))) + _ = hosting.fittingSize + } + + @Test func dockIconManagerUpdatesVisibility() { + _ = NSApplication.shared + UserDefaults.standard.set(false, forKey: showDockIconKey) + DockIconManager.shared.updateDockVisibility() + DockIconManager.shared.temporarilyShowDock() + } + + @Test func voiceWakeSettingsExercisesHelpers() { + VoiceWakeSettings.exerciseForTesting() + } + + @Test func debugSettingsExercisesHelpers() async { + await DebugSettings.exerciseForTesting() + } +}