From 4ba86bbe007f87c557d5b67a2f252cf6a582da95 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 24 Dec 2025 18:07:38 +0100 Subject: [PATCH] test: cover bridge hello defaults --- .../Bridge/BridgeConnectionController.swift | 36 ++++ .../BridgeConnectionControllerTests.swift | 159 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 apps/ios/Tests/BridgeConnectionControllerTests.swift diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Bridge/BridgeConnectionController.swift index f3623e379..162e13858 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Bridge/BridgeConnectionController.swift @@ -231,3 +231,39 @@ final class BridgeConnectionController { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev" } } + +#if DEBUG +extension BridgeConnectionController { + func _test_makeHello(token: String) -> BridgeHello { + self.makeHello(token: token) + } + + func _test_resolvedDisplayName(defaults: UserDefaults) -> String { + self.resolvedDisplayName(defaults: defaults) + } + + func _test_currentCaps() -> [String] { + self.currentCaps() + } + + func _test_currentCommands() -> [String] { + self.currentCommands() + } + + func _test_platformString() -> String { + self.platformString() + } + + func _test_deviceFamily() -> String { + self.deviceFamily() + } + + func _test_modelIdentifier() -> String { + self.modelIdentifier() + } + + func _test_appVersion() -> String { + self.appVersion() + } +} +#endif diff --git a/apps/ios/Tests/BridgeConnectionControllerTests.swift b/apps/ios/Tests/BridgeConnectionControllerTests.swift new file mode 100644 index 000000000..4ff359616 --- /dev/null +++ b/apps/ios/Tests/BridgeConnectionControllerTests.swift @@ -0,0 +1,159 @@ +import ClawdisKit +import Foundation +import Testing +import UIKit +@testable import Clawdis + +private struct KeychainEntry: Hashable { + let service: String + let account: String +} + +private let bridgeService = "com.steipete.clawdis.bridge" +private let nodeService = "com.steipete.clawdis.node" +private let instanceIdEntry = KeychainEntry(service: nodeService, account: "instanceId") +private let preferredBridgeEntry = KeychainEntry(service: bridgeService, account: "preferredStableID") +private let lastBridgeEntry = KeychainEntry(service: bridgeService, account: "lastDiscoveredStableID") + +private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> T) rethrows -> T { + let defaults = UserDefaults.standard + var snapshot: [String: Any?] = [:] + for key in updates.keys { + snapshot[key] = defaults.object(forKey: key) + } + for (key, value) in updates { + if let value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + defer { + for (key, value) in snapshot { + if let value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + } + return try body() +} + +private func withKeychainValues(_ updates: [KeychainEntry: String?], _ body: () throws -> T) rethrows -> T { + var snapshot: [KeychainEntry: String?] = [:] + for entry in updates.keys { + snapshot[entry] = KeychainStore.loadString(service: entry.service, account: entry.account) + } + for (entry, value) in updates { + if let value { + _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) + } else { + _ = KeychainStore.delete(service: entry.service, account: entry.account) + } + } + defer { + for (entry, value) in snapshot { + if let value { + _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) + } else { + _ = KeychainStore.delete(service: entry.service, account: entry.account) + } + } + } + return try body() +} + +@Suite(.serialized) struct BridgeConnectionControllerTests { + @Test @MainActor func resolvedDisplayNameSetsDefaultWhenMissing() { + let defaults = UserDefaults.standard + let displayKey = "node.displayName" + + withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { + withUserDefaults([displayKey: nil, "node.instanceId": "ios-test"]) { + let appModel = NodeAppModel() + let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) + + let resolved = controller._test_resolvedDisplayName(defaults: defaults) + #expect(!resolved.isEmpty) + #expect(defaults.string(forKey: displayKey) == resolved) + } + } + } + + @Test @MainActor func resolvedDisplayNamePreservesCustomValue() { + let defaults = UserDefaults.standard + let displayKey = "node.displayName" + + withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { + withUserDefaults([displayKey: "My iOS Node", "node.instanceId": "ios-test"]) { + let appModel = NodeAppModel() + let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) + + let resolved = controller._test_resolvedDisplayName(defaults: defaults) + #expect(resolved == "My iOS Node") + #expect(defaults.string(forKey: displayKey) == "My iOS Node") + } + } + } + + @Test @MainActor func makeHelloBuildsCapsAndCommands() { + let defaults = UserDefaults.standard + let voiceWakeKey = VoiceWakePreferences.enabledKey + + withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { + withUserDefaults([ + "node.instanceId": "ios-test", + "node.displayName": "Test Node", + "camera.enabled": false, + voiceWakeKey: true, + ]) { + let appModel = NodeAppModel() + let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) + let hello = controller._test_makeHello(token: "token-123") + + #expect(hello.nodeId == "ios-test") + #expect(hello.displayName == "Test Node") + #expect(hello.token == "token-123") + + let caps = Set(hello.caps ?? []) + #expect(caps.contains(ClawdisCapability.canvas.rawValue)) + #expect(caps.contains(ClawdisCapability.screen.rawValue)) + #expect(caps.contains(ClawdisCapability.voiceWake.rawValue)) + #expect(!caps.contains(ClawdisCapability.camera.rawValue)) + + let commands = Set(hello.commands ?? []) + #expect(commands.contains(ClawdisCanvasCommand.present.rawValue)) + #expect(commands.contains(ClawdisScreenCommand.record.rawValue)) + #expect(!commands.contains(ClawdisCameraCommand.snap.rawValue)) + + #expect(!(hello.platform ?? "").isEmpty) + #expect(!(hello.deviceFamily ?? "").isEmpty) + #expect(!(hello.modelIdentifier ?? "").isEmpty) + #expect(!(hello.version ?? "").isEmpty) + } + } + } + + @Test @MainActor func makeHelloIncludesCameraCommandsWhenEnabled() { + withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { + withUserDefaults([ + "node.instanceId": "ios-test", + "node.displayName": "Test Node", + "camera.enabled": true, + VoiceWakePreferences.enabledKey: false, + ]) { + let appModel = NodeAppModel() + let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) + let hello = controller._test_makeHello(token: "token-456") + + let caps = Set(hello.caps ?? []) + #expect(caps.contains(ClawdisCapability.camera.rawValue)) + + let commands = Set(hello.commands ?? []) + #expect(commands.contains(ClawdisCameraCommand.snap.rawValue)) + #expect(commands.contains(ClawdisCameraCommand.clip.rawValue)) + } + } + } +}