// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import CryptoKit import Foundation import TermindApplication import TermindDomain import TermindInfrastructure import TermindPersistence import TermindPresentation import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host @testable import Termind @Suite @MainActor struct SSHImportTests { private func root() throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent("SSHImport-\(UUID())") try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) return url } private func hash(_ address: String) -> String { let salt = Data(repeating: 7, count: 20) let digest = HMAC.authenticationCode(for: Data(address.utf8), using: SymmetricKey(data: salt)) return "|1|\(salt.base64EncodedString())|\(Data(digest).base64EncodedString())" } @Test func overQuotaImportRetainsSelectionAndWritesNothingUntilEntitlementResolves() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try material.privateKey.write(to: url.appendingPathComponent("id_ed25519"), atomically: true, encoding: .utf8) try """ Host prod stage HostName %h.example User deploy IdentityFile ~/.ssh/id_ed25519 """.write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) try "prod.example \(material.publicKey)".write( to: url.appendingPathComponent("known_hosts"), atomically: true, encoding: .utf8) let hosts = (0..<3).map { Host(name: "Existing \($0)", host: "existing\($0).invalid") } let library = HostLibraryModel(hosts: hosts) library.hostAccess.entitlement = .loading var upgrades = 0 library.requestHostUpgrade = { upgrades += 1 } library.resolveInitialHostEntitlement = { library.hostAccess.entitlement = .free } defer { library.resolveInitialHostEntitlement = nil } let model = SSHImportModel(library: library, reader: SSHImportReader()) await model.scan(url, knownHostsOnly: false) let selected = model.selectedItems().map(\.id) #expect(model.canImport) await model.commit() #expect(!model.completed) #expect(model.error == nil) #expect(upgrades == 1) #expect(model.selectedItems().map(\.id) == selected) #expect(library.hosts == hosts) #expect(library.identities.isEmpty && library.keys.isEmpty && library.knownHosts.isEmpty) library.hostAccess.entitlement = .purchased(.purchased) await model.commit() #expect(model.completed) #expect(library.hosts.count == 5) #expect(library.keys.count == 1 && library.knownHosts.count == 1) } @Test func appDefaultsNewCredentialsAndSSHImportToThisMacWhenICloudSyncIsOff() throws { let domain = "SSHImportDefaults.\(UUID())" let defaults = try #require(UserDefaults(suiteName: domain)) defer { defaults.removePersistentDomain(forName: domain) } defaults.set(false, forKey: SettingsKey.iCloudSyncEnabled) let hosts = HostLibraryModel() let app = AppModel( hosts: hosts, dependencies: AppDependencies( hostRepository: InMemoryHostRepository(), outboxRunner: nil, settingsDefaults: defaults)) defer { app.shutdownForTermination() } #expect(app.newVaultKeyStorage == .thisDevice) #expect(app.newVaultPasswordStorage == .thisDevice) #expect(app.makeSSHImportModel().storage == .thisDevice) } @Test func hashedKeysMatchPortsAndSurvivePersistenceWithoutHosts() throws { let entry = KnownHostEntry(hostname: hash("[2001:db8::1]:2222"), fingerprint: "SHA256:key") let decoded = try JSONDecoder().decode(KnownHostEntry.self, from: JSONEncoder().encode(entry)) #expect(decoded.matches(hostname: "2001:db8::1", port: 2222)) #expect(!decoded.matches(hostname: "2001:db8::1", port: 22)) let store = HostLibraryModel(knownHosts: [decoded]) let host = Host(name: "IPv6", host: "2001:db8::1", port: 2222) #expect(try store.expectedHostKeys(for: host) == [entry.identity]) store.forgetKnownHosts(for: host) #expect(store.knownHosts.isEmpty) } @Test func configHonorsFirstValuesHostPatternsAndOrderedIncludes() { let root = URL(fileURLWithPath: "/fixture/.ssh") var parser = SSHConfigParser( root: root, home: URL(fileURLWithPath: "/fixture"), username: "local", files: [ "/fixture/.ssh/config": """ Host prod alias HostName prod.example User deploy IdentityFile "~/.ssh/deploy key" Include conf/*.conf Host * !alias Port 2222 Host * User fallback """, "/fixture/.ssh/conf/01.conf": "Port=2200\nIdentityFile ~/.ssh/second", "/fixture/.ssh/conf/02.conf": "Port 2300", ]) let entries = parser.parse().compactMap(\.host) #expect(entries.count == 2) #expect(entries.allSatisfy { $0.port == 2200 && $0.username == "deploy" }) #expect(entries[0].identityPaths == ["/fixture/.ssh/deploy key", "/fixture/.ssh/second"]) } @Test func configReportsCyclesAndDoesNotExecuteMatchOrProxyCommands() { let root = URL(fileURLWithPath: "/fixture") var parser = SSHConfigParser( root: root, home: root, username: "local", files: [ "/fixture/config": "Host app\nHostName app.example\nProxyCommand touch /tmp/never\nInclude loop\nMatch exec \"exit 0\"\nUser wrong", "/fixture/loop": "Include config", ]) let entries = parser.parse() let host = entries.first { $0.host != nil } #expect(host?.host?.username == "local") #expect(host?.warnings.contains(where: { $0.contains("proxycommand") }) == true) #expect(host?.warnings.contains(where: { $0.contains("Match") }) == true) #expect(entries.contains { $0.unavailable }) } @Test func knownHostsValidatesWireKeysAndPreservesMultipleEndpoints() throws { let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) let content = """ # comment example,[2001:db8::1]:2222 \(material.publicKey) \(hash("other.example")) \(material.publicKey) @revoked revoked.example \(material.publicKey) *.wild.example \(material.publicKey) broken ssh-ed25519 AAAA """ let rows = SSHImportParser.knownHosts(content, source: "known_hosts") #expect(rows.filter { !$0.unavailable }.count == 3) #expect(rows.compactMap(\.knownHost).allSatisfy { $0.fingerprint == material.fingerprint }) #expect(rows.filter(\.unavailable).count == 3) #expect(rows.compactMap(\.knownHost).contains { $0.matches(hostname: "other.example", port: 22) }) } @Test func contentClassifierDoesNotUseFilenames() throws { let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) #expect(SSHImportParser.contentKind(material.privateKey) == .privateKey) #expect(SSHImportParser.contentKind("example.invalid \(material.publicKey)") == .knownHosts) #expect(SSHImportParser.contentKind("Host app\n HostName app.invalid") == .configuration) #expect(SSHImportParser.contentKind("ordinary notes") == .unknown) } @Test func folderRecursivelyFindsRenamedContentAndDoesNotReparseIncludes() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let pieces = url.appendingPathComponent("pieces", isDirectory: true) let secrets = url.appendingPathComponent("secrets", isDirectory: true) try FileManager.default.createDirectory(at: pieces, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: secrets, withIntermediateDirectories: true) let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try "Include pieces/*".write( to: url.appendingPathComponent("entry.anything"), atomically: true, encoding: .utf8) try "Host app\n HostName app.invalid\n User deploy\n IdentityFile secrets/credential.data".write( to: pieces.appendingPathComponent("fragment.notes"), atomically: true, encoding: .utf8) try material.privateKey.write( to: secrets.appendingPathComponent("credential.data"), atomically: true, encoding: .utf8) try "app.invalid \(material.publicKey)".write( to: pieces.appendingPathComponent("trust.random"), atomically: true, encoding: .utf8) let rows = try await SSHImportReader().scan(root: url, knownHostsOnly: false) #expect(rows.compactMap(\.host).map(\.alias) == ["app"]) #expect(rows.compactMap(\.key).count == 1) #expect(rows.compactMap(\.knownHost).count == 1) } @Test func standaloneKnownHostsRejectsOtherSSHContentButAcceptsComments() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let file = url.appendingPathComponent("arbitrary-name") let reader = SSHImportReader() try "Host app\nHostName app.invalid".write(to: file, atomically: true, encoding: .utf8) await #expect(throws: SSHImportSourceError.unsupportedKnownHostsContent) { _ = try await reader.scan(root: file, knownHostsOnly: true) } try "# intentionally empty\n".write(to: file, atomically: true, encoding: .utf8) #expect(try await reader.scan(root: file, knownHostsOnly: true).isEmpty) } @Test func privateKeyIsRereadAndDigestCheckedAtCommitBoundary() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let first = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) let second = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) let file = url.appendingPathComponent("credential") try first.privateKey.write(to: file, atomically: true, encoding: .utf8) let reader = SSHImportReader() let rows = try await reader.scan(root: url, knownHostsOnly: false) let id = try #require(rows.compactMap(\.key).first?.id) try second.privateKey.write(to: file, atomically: true, encoding: .utf8) await #expect(throws: SSHImportSourceError.sourceChanged) { _ = try await reader.credentials(for: [id]) } } @Test func droppedSourcesUseTheSameTypeValidationAsTheFilePicker() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let file = url.appendingPathComponent("known_hosts") try "# empty".write(to: file, atomically: true, encoding: .utf8) let reader = SSHImportReader() await #expect(throws: SSHImportSourceError.expectedFolder) { _ = try await reader.scan(root: file, knownHostsOnly: false) } await #expect(throws: SSHImportSourceError.expectedFile) { _ = try await reader.scan(root: url, knownHostsOnly: true) } #expect(try await reader.scan(root: file, knownHostsOnly: true).isEmpty) } @Test func malformedPrivateKeysAreUnavailableRatherThanAskingForAPassphrase() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } try "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAA\n-----END OPENSSH PRIVATE KEY-----".write( to: url.appendingPathComponent("broken"), atomically: true, encoding: .utf8) let reader = SSHImportReader() let rows = try await reader.scan(root: url, knownHostsOnly: false) #expect(rows.count == 1) #expect(rows[0].unavailable && !rows[0].selected && !rows[0].requiresPassphrase) } @Test func selectionStartsCheckedAndCollapseDoesNotChangeIt() { let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader()) model.items = [ SSHImportItem(category: .hosts, name: "a", source: "config"), SSHImportItem(category: .keys, name: "b", source: "key"), SSHImportItem(category: .knownHosts, name: "c", source: "known_hosts"), SSHImportItem(category: .keys, name: "bad", source: "bad", selected: false, unavailable: true), ] #expect(model.selectedItems().count == 3) model.expanded.remove(.keys) #expect(model.selectedItems().count == 3) model.setSelected(false, category: .keys) #expect(model.selectedItems().count == 2) model.setSelected(false) #expect(!model.canImport) model.setSelected(true) #expect(model.selectedItems().count == 3) #expect(!model.items[3].selected) } @Test func folderImportMigratesSecretsAndIsIdempotent() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try material.privateKey.write(to: url.appendingPathComponent("id_ed25519"), atomically: true, encoding: .utf8) try "Host prod\n HostName prod.example\n User deploy\n IdentityFile ~/.ssh/id_ed25519\n".write( to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) try "prod.example \(material.publicKey)".write( to: url.appendingPathComponent("known_hosts"), atomically: true, encoding: .utf8) let group = HostGroup(name: "Imported") let store = HostLibraryModel(groups: [group]) let model = SSHImportModel(library: store, reader: SSHImportReader()) model.hostGroupID = group.id model.hostTags = ["production", "ssh"] model.storage = .thisDevice model.importColor = .purple await model.scan(url, knownHostsOnly: false) #expect(model.selectedItems().count == 4) #expect(store.hosts.isEmpty && store.keys.isEmpty) #expect(model.canImport) await model.commit() #expect(model.completed) #expect(store.hosts.count == 1 && store.keys.count == 1 && store.knownHosts.count == 1) let identity = try #require(store.identities.first) #expect(identity.username == "deploy" && identity.keyID == store.keys.first?.id) #expect(store.hosts.first?.groupID == group.id) #expect(store.hosts.first?.colorTag == .purple) #expect(store.hosts.first?.tags == ["production", "ssh"]) #expect(store.keys.first?.colorTag == .purple) #expect(store.keys.first?.storage == .thisDevice) #expect(identity.colorTag == .purple) #expect(identity.passwordStorage == .thisDevice) #expect(store.keyAvailability(for: try #require(store.keys.first)) == .available) let again = SSHImportModel(library: store, reader: SSHImportReader()) await again.scan(url, knownHostsOnly: false) #expect(again.selectedItems().isEmpty) #expect(!again.canImport) } @Test func encryptedKeyCanBeUnlockedAndDeselectionRefreshesHostAssociation() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: "test-password") try material.privateKey.write(to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8) try "Host prod\nIdentityFile ~/.ssh/deploy".write( to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader()) await model.scan(url, knownHostsOnly: false) let id = try #require(model.items.first { $0.category == .keys }?.id) #expect(!model.canImport) await model.unlock(id, passphrase: "wrong") #expect(!model.canImport) await model.unlock(id, passphrase: "test-password") #expect(model.canImport) let host = try #require(model.items.first { $0.host != nil }) #expect(model.chosenKey(for: host)?.id == id) model.setSelected(false, category: .keys) #expect(model.items.first(where: { $0.id == host.id })?.host?.identityID == nil) #expect(model.canImport) } @Test func identitySelectionControlsKeysAndHostBindingWithoutChangingHostSelection() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try material.privateKey.write( to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8) try "Host app\n User deploy\n IdentityFile ~/.ssh/deploy".write( to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader()) await model.scan(url, knownHostsOnly: false) let hostID = try #require(model.items.first(where: { $0.host != nil })?.id) let identityID = try #require(model.items.first(where: { $0.identity != nil })?.id) let keyID = try #require(model.items.first(where: { $0.key != nil })?.id) #expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == identityID) model.setSelected(false, itemID: identityID) #expect(model.items.first(where: { $0.id == hostID })?.selected == true) #expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil) #expect(model.items.first(where: { $0.id == keyID })?.selected == true) model.setSelected(false, itemID: hostID) model.setSelected(true, itemID: hostID) #expect(model.items.first(where: { $0.id == identityID })?.selected == false) #expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil) model.setSelected(true, itemID: identityID) #expect(model.items.first(where: { $0.id == keyID })?.selected == true) #expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == identityID) model.setSelected(false, itemID: keyID) #expect(model.items.first(where: { $0.id == identityID })?.selected == false) #expect(model.items.first(where: { $0.id == hostID })?.selected == true) #expect(model.items.first(where: { $0.id == hostID })?.host?.identityID == nil) } @Test func sharedIdentityIsImportedOnceAndReceivesImportDefaults() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try material.privateKey.write( to: url.appendingPathComponent("deploy"), atomically: true, encoding: .utf8) try """ Host app-one app-two User deploy IdentityFile ~/.ssh/deploy """.write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let store = HostLibraryModel() let model = SSHImportModel(library: store, reader: SSHImportReader()) model.storage = .thisDevice model.importColor = .orange await model.scan(url, knownHostsOnly: false) #expect(model.items.filter { $0.category == .identities }.count == 1) await model.commit() let identity = try #require(store.identities.first) #expect(store.identities.count == 1) #expect(store.hosts.count == 2) #expect(store.hosts.allSatisfy { $0.identityID == identity.id }) #expect(identity.passwordStorage == .thisDevice) #expect(identity.colorTag == .orange) #expect(!identity.usesPassword) } @Test func exactExistingIdentityIsReusedWhileDifferentKeysGetDistinctNames() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } let first = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) let second = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) try first.privateKey.write(to: url.appendingPathComponent("first"), atomically: true, encoding: .utf8) try second.privateKey.write(to: url.appendingPathComponent("second"), atomically: true, encoding: .utf8) try """ Host first User deploy IdentityFile ~/.ssh/first Host second User deploy IdentityFile ~/.ssh/second """.write(to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let existingKey = SSHKeyRef( label: "Saved", publicKey: first.publicKey, fingerprint: first.fingerprint, credentialRef: "saved") let existingIdentity = Identity(name: "Existing Deploy", username: "deploy", keyID: existingKey.id) let secrets = InMemorySecretStore() try secrets.setKeyCredential( .software(SSHPrivateKeySecret(privateKey: first.privateKey)), for: existingKey.credentialRef, storage: existingKey.storage) let store = HostLibraryModel(keys: [existingKey], identities: [existingIdentity], secrets: secrets) let model = SSHImportModel(library: store, reader: SSHImportReader()) await model.scan(url, knownHostsOnly: false) let identities = model.items.filter { $0.category == .identities } #expect(identities.count == 2) #expect(identities.contains { $0.existingID == existingIdentity.id && $0.name == "Existing Deploy" }) #expect(identities.contains { $0.existingID == nil && $0.name.contains("second") }) } @Test func jumpDependenciesAndCyclesBlockCommit() { let model = SSHImportModel(library: HostLibraryModel(), reader: SSHImportReader()) model.items = [ SSHImportItem( category: .hosts, name: "app", source: "config", host: SSHImportedHost(alias: "app", hostname: "app", username: "u", jumps: ["jump"])), SSHImportItem( category: .hosts, name: "jump", source: "config", host: SSHImportedHost(alias: "jump", hostname: "jump", username: "u")), ] #expect(model.canImport) model.items[1].selected = false #expect(!model.canImport) model.items[1].selected = true model.items[1].host?.jumps = ["app"] #expect(!model.canImport) #expect(model.routeProblem != nil) } @Test func importRequiresKnownHostConflictReview() async { let original = KnownHostEntry(hostname: "host", fingerprint: "SHA256:old") let store = HostLibraryModel(knownHosts: [original]) let model = SSHImportModel(library: store, reader: SSHImportReader()) model.items = [ SSHImportItem( category: .knownHosts, name: "host", source: "known_hosts", knownHost: KnownHostEntry(hostname: "host", fingerprint: "SHA256:new")) ] #expect(!model.canImport) model.items[0].acceptedWarnings = true #expect(model.canImport) await model.commit() #expect(model.completed) #expect(store.knownHosts.first?.fingerprint == original.fingerprint) } @Test func multiHopImportsPreserveStandaloneJumpConfigurations() async throws { let url = try root() defer { try? FileManager.default.removeItem(at: url) } try "Host app\nProxyJump first,second\nHost first\nHost second".write( to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let store = HostLibraryModel() let model = SSHImportModel(library: store, reader: SSHImportReader()) await model.scan(url, knownHostsOnly: false) #expect(model.canImport) await model.commit() #expect(model.completed) let second = try #require(store.hosts.first { $0.name == "second" }) #expect(second.jumpHostID == nil) let app = try #require(store.hosts.first { $0.name == "app" }) let intermediate = try #require(app.jumpHostID.flatMap(store.host)) #expect(intermediate.host == "second") #expect(intermediate.jumpHostID.flatMap(store.host)?.name == "first") let again = SSHImportModel(library: store, reader: SSHImportReader()) await again.scan(url, knownHostsOnly: false) #expect(again.selectedItems().isEmpty) } @Test func externalKeyIsOnlyReadAfterExplicitAuthorization() async throws { let url = try root() let external = try root() defer { try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: external) } let material = try SSHKeyMaterial.generate(.ed25519, comment: "", passphrase: nil) let keyURL = external.appendingPathComponent("deploy") try material.privateKey.write(to: keyURL, atomically: true, encoding: .utf8) try "Host app\nIdentityFile \(keyURL.path)".write( to: url.appendingPathComponent("config"), atomically: true, encoding: .utf8) let reader = SSHImportReader() let before = try await reader.scan(root: url, knownHostsOnly: false) #expect(before.compactMap(\.key).isEmpty) #expect(before.contains { $0.needsAuthorization == keyURL.path }) let after = try await reader.scan(root: url, knownHostsOnly: false, grants: [keyURL]) #expect(after.compactMap(\.key).count == 1) #expect(!after.contains { $0.needsAuthorization != nil }) } @Test func keychainPagePruningDoesNotEraseKnownHostSelection() { let entry = KnownHostEntry(hostname: "host", fingerprint: "SHA256:key") let store = HostLibraryModel(knownHosts: [entry]) let window = WindowModel() window.selectVaultItem(.knownHost(entry.id), on: .knownHosts) window.pruneVaultSelections(in: CredentialLibraryModel(library: store)) #expect(window.vaultSelection(for: .knownHosts) == [.knownHost(entry.id)]) } @Test func failedSnapshotRollsBackMetadataAndNewCredentials() throws { let persistence = FailingImportPersistence() let secrets = InMemorySecretStore() let store = HostLibraryModel(persistence: persistence, secrets: secrets) let key = SSHKeyRef(label: "Imported", credentialRef: "import-test") let batch = SSHImportBatch( hosts: [Host(name: "new", host: "new")], keys: [key], knownHosts: [KnownHostEntry(hostname: "new", fingerprint: "SHA256:key")], credentials: [key.id: .software(SSHPrivateKeySecret(privateKey: "test-private-material"))]) #expect(throws: (any Error).self) { try store.commitSSHImport(batch) } #expect(store.hosts.isEmpty && store.keys.isEmpty && store.knownHosts.isEmpty) #expect(try secrets.keyCredential(for: key.credentialRef, storage: key.storage) == nil) #expect(!store.saveKnownHost(KnownHostEntry(hostname: "new", fingerprint: "SHA256:key"))) #expect(store.knownHosts.isEmpty) } @Test func vaultKnownHostNavigationUsesFingerprintAndRetainsSelection() { let entry = KnownHostEntry(hostname: "host", fingerprint: "SHA256:key") let store = HostLibraryModel(knownHosts: [entry]) let window = WindowModel() window.selectVaultItem(.knownHost(entry.id), on: .knownHosts) window.vaultPages.knownHosts.query = "key" window.openPage(.hosts) window.openPage(.knownHosts) window.pruneVaultSelections(in: store) #expect(window.vaultSelection(for: .knownHosts) == [.knownHost(entry.id)]) #expect(window.vaultPages.knownHosts.query == "key") #expect(window.findKind == .knownHosts) #expect(VaultPage.allCases.last == .knownHosts) } } @MainActor private final class FailingImportPersistence: VaultPersisting { func load() throws -> VaultSnapshot { VaultSnapshot() } func save( _ snapshot: VaultSnapshot, touching keys: Set ) throws -> VaultMutationSet { throw CocoaError(.fileWriteUnknown) } }