227 lines
11 KiB
Swift
227 lines
11 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AgentKit
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@Suite @MainActor
|
|
struct SavedNameTests {
|
|
@Test func missingInputsDoNotInventNameHints() {
|
|
#expect(Host(name: "", host: " \n").defaultName == "")
|
|
#expect(Identity(name: "", username: " \n").defaultName == "")
|
|
#expect(AIModel(id: "").savingName.displayName == "")
|
|
let forwarding = PortForwardingRule(hostID: UUID())
|
|
#expect(forwarding.defaultName(hostName: "") == "")
|
|
#expect(forwarding.defaultName(hostName: " \n") == "")
|
|
}
|
|
|
|
@Test(arguments: ["", " \t\n", "Production"])
|
|
func vaultNamesMaterializeOnlyOnSaveAndSurviveReload(_ name: String) throws {
|
|
let host = Host(name: name, host: "example.invalid")
|
|
let identity = Identity(name: name, username: "deploy")
|
|
let key = SSHKeyRef(label: name, algorithm: .rsa)
|
|
let original = VaultSnapshot(hosts: [host], identities: [identity], keys: [key])
|
|
let persistence = NameVaultPersistence(original)
|
|
let library = HostLibraryModel(persistence: persistence)
|
|
#expect(library.host(host.id)?.name == name)
|
|
#expect(library.identity(identity.id)?.name == name)
|
|
#expect(library.key(key.id)?.label == name)
|
|
#expect(persistence.writes == 0)
|
|
library.replacePersistedSnapshot(original)
|
|
#expect(persistence.writes == 0)
|
|
|
|
#expect(library.saveHost(host))
|
|
try library.upsert(identity, passwordUpdate: .keep)
|
|
try library.upsert(key)
|
|
let reopened = HostLibraryModel(persistence: persistence)
|
|
let isBlank = name != "Production"
|
|
#expect(reopened.host(host.id)?.name == (isBlank ? "example.invalid" : name))
|
|
#expect(reopened.identity(identity.id)?.name == (isBlank ? "deploy" : name))
|
|
#expect(reopened.key(key.id)?.label == (isBlank ? "RSA" : name))
|
|
#expect(persistence.writes == 3)
|
|
}
|
|
|
|
@Test func storedDefaultsAreOrdinaryNamesUntilExplicitlyCleared() throws {
|
|
let library = HostLibraryModel()
|
|
let host = Host(name: "", host: "first.invalid")
|
|
#expect(library.saveHost(host))
|
|
var savedHost = try #require(library.host(host.id))
|
|
savedHost.host = "second.invalid"
|
|
#expect(library.saveHost(savedHost))
|
|
#expect(library.host(host.id)?.name == "first.invalid")
|
|
savedHost.name = " \n"
|
|
#expect(library.saveHost(savedHost))
|
|
#expect(library.host(host.id)?.name == "second.invalid")
|
|
|
|
let identity = Identity(name: "", username: "deploy")
|
|
#expect(library.upsert(identity))
|
|
var savedIdentity = try #require(library.identity(identity.id))
|
|
savedIdentity.username = "root"
|
|
#expect(library.upsert(savedIdentity))
|
|
#expect(library.identity(identity.id)?.name == "deploy")
|
|
savedIdentity.name = ""
|
|
#expect(library.upsert(savedIdentity))
|
|
#expect(library.identity(identity.id)?.name == "root")
|
|
|
|
let key = SSHKeyRef(label: "", algorithm: .ed25519)
|
|
try library.upsert(key)
|
|
var savedKey = try #require(library.key(key.id))
|
|
savedKey.algorithm = .rsa
|
|
try library.upsert(savedKey)
|
|
#expect(library.key(key.id)?.label == "Ed25519")
|
|
savedKey.label = ""
|
|
try library.upsert(savedKey)
|
|
#expect(library.key(key.id)?.label == "RSA")
|
|
}
|
|
|
|
@Test(arguments: SSHKeyAlgorithm.allCases, KeyStorage.allCases)
|
|
func keyHintsMatchPersistedAlgorithmAndStorageDefaults(_ algorithm: SSHKeyAlgorithm, _ storage: KeyStorage) throws {
|
|
let library = HostLibraryModel()
|
|
if algorithm == .securityKey, storage == .secureEnclave { return }
|
|
if algorithm != .securityKey, storage == .external { return }
|
|
var key = SSHKeyRef(label: " \n", algorithm: algorithm, storage: storage)
|
|
if algorithm == .securityKey {
|
|
key.securityKey = SecurityKeyCredential(
|
|
credentialID: Data([1]), publicKeyX963: Data([4]) + Data(repeating: 1, count: 64))
|
|
}
|
|
try library.upsert(key)
|
|
#expect(library.key(key.id)?.label == key.defaultName)
|
|
if algorithm == .securityKey, storage != .secureEnclave {
|
|
#expect(library.key(key.id)?.label == "Security Key")
|
|
}
|
|
#expect(key.defaultName == (storage == .secureEnclave ? "Secure Enclave" : algorithm.typeName))
|
|
}
|
|
|
|
@Test(arguments: PortForwardingRule.Kind.allCases)
|
|
func forwardingSaveReturnsTheConcreteDraftAndKeepsItsNameWhenTheHostChanges(_ kind: PortForwardingRule.Kind) throws
|
|
{
|
|
var host = Host(name: "AWS", host: "example.invalid")
|
|
let persistence = NameVaultPersistence(.init(hosts: [host]))
|
|
let library = HostLibraryModel(persistence: persistence)
|
|
let forwardings = ForwardingLibraryModel(library: library)
|
|
let draft = PortForwardingRule(hostID: host.id, kind: kind, label: " \n")
|
|
let saved = try forwardings.save(draft)
|
|
#expect(saved.label == draft.defaultName(hostName: "AWS"))
|
|
#expect(PortForwardingEditorPolicy.normalized(saved) == library.portForwarding(saved.id))
|
|
host.name = "Renamed"
|
|
#expect(library.saveHost(host))
|
|
var changed = saved
|
|
changed.kind = .dynamic
|
|
let resaved = try forwardings.save(changed)
|
|
#expect(resaved.label == saved.label)
|
|
#expect(HostLibraryModel(persistence: persistence).portForwarding(saved.id)?.label == saved.label)
|
|
changed.label = ""
|
|
#expect(try forwardings.save(changed).label == "Renamed (-D)")
|
|
}
|
|
|
|
@Test func failedVaultSavesKeepLegacyNamesAndTheStoredSnapshot() throws {
|
|
let host = Host(name: "", host: "example.invalid")
|
|
let identity = Identity(name: "", username: "deploy")
|
|
let key = SSHKeyRef(label: "")
|
|
let rule = PortForwardingRule(hostID: host.id)
|
|
let original = VaultSnapshot(hosts: [host], identities: [identity], keys: [key], portForwardings: [rule])
|
|
let persistence = NameVaultPersistence(original)
|
|
let library = HostLibraryModel(persistence: persistence)
|
|
persistence.fails = true
|
|
#expect(!library.saveHost(host))
|
|
#expect(!library.upsert(identity))
|
|
#expect(throws: (any Error).self) { try library.upsert(identity, passwordUpdate: .keep) }
|
|
#expect(throws: (any Error).self) { try library.upsert(key) }
|
|
#expect(throws: (any Error).self) { try ForwardingLibraryModel(library: library).save(rule) }
|
|
#expect(library.host(host.id)?.name == "")
|
|
#expect(library.identity(identity.id)?.name == "")
|
|
#expect(library.key(key.id)?.label == "")
|
|
#expect(library.portForwarding(rule.id)?.label == "")
|
|
#expect(persistence.writes == 0)
|
|
#expect(persistence.snapshot.hosts == original.hosts)
|
|
#expect(persistence.snapshot.identities == original.identities)
|
|
#expect(persistence.snapshot.keys == original.keys)
|
|
#expect(persistence.snapshot.portForwardings == original.portForwardings)
|
|
persistence.fails = false
|
|
#expect(try ForwardingLibraryModel(library: library).save(rule).label == "example.invalid (-L)")
|
|
}
|
|
|
|
@Test func providerAndModelNamesRoundTripWithoutBackfillingReadsOrFailedWrites() throws {
|
|
let provider = ModelProvider(name: " \n", models: [AIModel(id: "private-model", displayName: " \n")])
|
|
let snapshot = AIConfigurationSnapshot(providers: [provider], roles: [:])
|
|
let persistence = NameAIPersistence(snapshot)
|
|
let ai = AIConfigurationModel(persistence: persistence, persistsSecrets: false)
|
|
ai.applyRemoteSnapshot(snapshot, webSearch: .init())
|
|
#expect(ai.modelProviders == [provider])
|
|
#expect(persistence.writes == 0)
|
|
persistence.fails = true
|
|
#expect(!ai.saveProvider(provider, endpoint: .init(apiType: .messages)))
|
|
#expect(ai.modelProviders == [provider])
|
|
#expect(persistence.writes == 0)
|
|
persistence.fails = false
|
|
#expect(ai.saveProvider(provider, endpoint: .init(apiType: .messages)))
|
|
let reloaded = AIConfigurationModel(persistence: persistence, persistsSecrets: false)
|
|
let saved = try #require(reloaded.modelProvider(provider.id))
|
|
#expect(saved.name == "Anthropic")
|
|
#expect(saved.models.first?.displayName == "private-model")
|
|
reloaded.setEndpoint(.init(apiType: .generateContent), for: saved.id)
|
|
#expect(reloaded.modelProvider(saved.id)?.name == "Anthropic")
|
|
}
|
|
|
|
@Test func modelWritesFreezeDefaultsAndCatalogRefreshPreservesNames() throws {
|
|
let provider = ModelProvider(name: "Gateway")
|
|
let ai = AIConfigurationModel(modelProviders: [provider], persistsSecrets: false)
|
|
ai.addModel(AIModel(id: "manual", displayName: " \n"), to: provider.id)
|
|
ai.addModel(AIModel(id: "fetched", displayName: "Catalog Name", origin: .fetched), to: provider.id)
|
|
#expect(ai.modelProvider(provider.id)?.model("manual")?.displayName == "manual")
|
|
ai.replaceModels(
|
|
[
|
|
AIModel(id: "manual", displayName: "Future Catalog Name", contextLength: 42),
|
|
AIModel(id: "fetched", displayName: "Changed Catalog Name"),
|
|
AIModel(id: "new", displayName: ""),
|
|
], for: provider.id)
|
|
#expect(ai.modelProvider(provider.id)?.model("manual")?.displayName == "manual")
|
|
#expect(ai.modelProvider(provider.id)?.model("manual")?.contextLength == 42)
|
|
#expect(ai.modelProvider(provider.id)?.model("fetched")?.displayName == "Catalog Name")
|
|
#expect(ai.modelProvider(provider.id)?.model("new")?.displayName == "new")
|
|
var edited = try #require(ai.modelProvider(provider.id)?.model("fetched"))
|
|
edited.displayName = "Custom"
|
|
ai.updateModel(edited, in: provider.id)
|
|
#expect(ai.modelProvider(provider.id)?.model("fetched")?.displayName == "Custom")
|
|
edited.displayName = " \t"
|
|
ai.updateModel(edited, in: provider.id)
|
|
#expect(ai.modelProvider(provider.id)?.model("fetched")?.displayName == "fetched")
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class NameVaultPersistence: VaultPersisting {
|
|
var snapshot: VaultSnapshot
|
|
var writes = 0
|
|
var fails = false
|
|
init(_ snapshot: VaultSnapshot) { self.snapshot = snapshot }
|
|
func load() throws -> VaultSnapshot { snapshot }
|
|
func save(
|
|
_ snapshot: VaultSnapshot, touching keys: Set<VaultRecordKey>
|
|
) throws -> VaultMutationSet {
|
|
if fails { throw CocoaError(.fileWriteUnknown) }
|
|
self.snapshot = snapshot
|
|
writes += 1
|
|
return .empty
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class NameAIPersistence: AIConfigurationPersisting {
|
|
var snapshot: AIConfigurationSnapshot
|
|
var writes = 0
|
|
var fails = false
|
|
init(_ snapshot: AIConfigurationSnapshot) { self.snapshot = snapshot }
|
|
func load() throws -> AIConfigurationSnapshot { snapshot }
|
|
func save(_ snapshot: AIConfigurationSnapshot) throws {
|
|
if fails { throw CocoaError(.fileWriteUnknown) }
|
|
self.snapshot = snapshot
|
|
writes += 1
|
|
}
|
|
}
|