185 lines
9.1 KiB
Swift
185 lines
9.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AgentKit
|
|
import Foundation
|
|
import SwiftData
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import Testing
|
|
|
|
@Suite @MainActor
|
|
struct SettingsPortTests {
|
|
@Test func permissionResolutionUsesTheLatestDefaultUntilAConversationChooses() {
|
|
let ai = AIConfigurationModel(lastSelectedPermissionMode: .approveForMe)
|
|
let conversation = ConversationModel()
|
|
#expect(ai.resolve(conversation: conversation.permissionMode) == .approveForMe)
|
|
|
|
conversation.permissionMode = .askForApproval
|
|
ai.select(.fullAccess)
|
|
#expect(ai.resolve(conversation: conversation.permissionMode) == .askForApproval)
|
|
}
|
|
|
|
@Test func permissionDefaultPersistsAcrossConfigurationInstances() throws {
|
|
let domain = "SettingsPortPermission.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: domain))
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
|
|
let first = AIConfigurationModel(permissionDefaults: defaults)
|
|
first.select(.approveForMe)
|
|
#expect(AIConfigurationModel(permissionDefaults: defaults).lastSelectedPermissionMode == .approveForMe)
|
|
|
|
first.select(.fullAccess)
|
|
#expect(AIConfigurationModel(permissionDefaults: defaults).lastSelectedPermissionMode == .fullAccess)
|
|
}
|
|
|
|
@Test func missingAndInvalidPermissionDefaultsRequireApproval() throws {
|
|
let domain = "SettingsPortPermissionFallback.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: domain))
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
|
|
#expect(AIConfigurationModel(permissionDefaults: defaults).lastSelectedPermissionMode == .askForApproval)
|
|
defaults.set("unknown-mode", forKey: "assistant.permissionMode")
|
|
#expect(AIConfigurationModel(permissionDefaults: defaults).lastSelectedPermissionMode == .askForApproval)
|
|
}
|
|
|
|
@Test func failedTerminalSaveKeepsTheAppliedPreferences() {
|
|
let persistence = FailingTerminalPreferences()
|
|
let model = TerminalSettingsModel(persistence: persistence)
|
|
#expect(!model.set("GitHub Light", for: \.lightTheme))
|
|
#expect(model.preferences.lightTheme == "Termind Light")
|
|
#expect(model.storageError != nil)
|
|
model.applyRemoteSnapshot(TerminalPreferences(lightTheme: "GitHub Light"))
|
|
#expect(model.preferences.lightTheme == "GitHub Light")
|
|
#expect(persistence.saves == 1)
|
|
}
|
|
|
|
@Test func mobileSettingsEditsPreserveSkillsMCPAndUnresolvedRoles() throws {
|
|
let container = try TermindPersistence.makeContainer(inMemory: true)
|
|
let domain = "SettingsPortTests.\(UUID())"
|
|
let defaults = UserDefaults(suiteName: domain)!
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults)
|
|
try store.prepare()
|
|
let server = MCPServer(name: "Preserved")
|
|
let unresolved = AIModelRef(modelProviderID: UUID(), modelID: "not-arrived")
|
|
try store.saveConfiguration(.init(providers: [], roles: [.guardian: unresolved], mcpServers: [server]))
|
|
let skills = try store.load().skills
|
|
let ai = AIConfigurationModel(persistence: SyncedAIConfigurationPersistence(store))
|
|
#expect(
|
|
ai.saveProvider(
|
|
ModelProvider(name: "Mobile"), endpoint: ProviderEndpoint(url: "https://example.invalid/v1")))
|
|
let saved = try store.load()
|
|
#expect(saved.skills == skills)
|
|
#expect(saved.configuration.mcpServers == [server])
|
|
#expect(saved.configuration.roles[.guardian] == unresolved)
|
|
#expect(try !store.journal.pendingEnvelopes().isEmpty)
|
|
}
|
|
|
|
@Test func applyingRemoteTerminalDoesNotEnterLocalWriteCallback() throws {
|
|
let container = try TermindPersistence.makeContainer(inMemory: true)
|
|
let domain = "SettingsPortTests.\(UUID())"
|
|
let defaults = UserDefaults(suiteName: domain)!
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
let store = SettingsSyncPersistence(context: container.mainContext, defaults: defaults)
|
|
try store.prepare()
|
|
var callbacks = 0
|
|
store.didRecordChanges = { _ in callbacks += 1 }
|
|
let preferences = TerminalPreferences(
|
|
darkCustomEnabled: true, darkCustomConfiguration: "background = 123456")
|
|
let payload = SettingsRecordPayload.terminal(preferences)
|
|
let envelope = SettingsEnvelope(
|
|
key: payload.key, version: .next(after: nil, writerID: UUID(), now: .now), payload: payload)
|
|
let result = try store.applyRemote([envelope])
|
|
#expect(result.snapshot.terminal == preferences)
|
|
#expect(defaults.double(forKey: SettingsKey.terminalFontSize) == 12)
|
|
#expect(callbacks == 0)
|
|
}
|
|
|
|
@Test func syncDefaultsPreserveIndependentSelectionsWhenPaused() {
|
|
let domain = "SettingsPortTests.\(UUID())"
|
|
let defaults = UserDefaults(suiteName: domain)!
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
SettingsKey.migrateSyncPreferences(in: defaults)
|
|
#expect(defaults.bool(forKey: SettingsKey.iCloudSyncEnabled))
|
|
#expect(SettingsKey.defaultSettingsSyncEnabled)
|
|
#expect(defaults.object(forKey: SettingsKey.settingsSyncEnabled) == nil)
|
|
defaults.set(true, forKey: SettingsKey.settingsSyncEnabled)
|
|
defaults.set(false, forKey: SettingsKey.iCloudSyncEnabled)
|
|
SettingsKey.migrateSyncPreferences(in: defaults)
|
|
#expect(!defaults.bool(forKey: SettingsKey.iCloudSyncEnabled))
|
|
#expect(defaults.bool(forKey: SettingsKey.settingsSyncEnabled))
|
|
}
|
|
|
|
@Test func newVaultCredentialStorageRequiresMasterAndProRegardlessOfLegacyVaultChoice() throws {
|
|
let domain = "VaultCredentialStorage.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: domain))
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
for iCloudEnabled in [false, true] {
|
|
for vaultEnabled in [false, true] {
|
|
for canUseVaultSync in [false, true] {
|
|
defaults.set(iCloudEnabled, forKey: SettingsKey.iCloudSyncEnabled)
|
|
defaults.set(vaultEnabled, forKey: SettingsKey.vaultSyncEnabled)
|
|
let expectedKey: KeyStorage =
|
|
iCloudEnabled && canUseVaultSync ? .iCloud : .thisDevice
|
|
let expectedPassword: PasswordStorage =
|
|
iCloudEnabled && canUseVaultSync ? .iCloud : .thisDevice
|
|
|
|
#expect(
|
|
NewVaultCredentialStorage.keyStorage(
|
|
defaults: defaults,
|
|
canUseVaultSync: canUseVaultSync) == expectedKey)
|
|
#expect(
|
|
NewVaultCredentialStorage.passwordStorage(
|
|
defaults: defaults,
|
|
canUseVaultSync: canUseVaultSync) == expectedPassword)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func newVaultCredentialStorageUsesEnabledDefaultsWhenPreferencesAreMissing() throws {
|
|
let domain = "VaultCredentialStorageDefaults.\(UUID())"
|
|
let defaults = try #require(UserDefaults(suiteName: domain))
|
|
defer { defaults.removePersistentDomain(forName: domain) }
|
|
|
|
#expect(NewVaultCredentialStorage.keyStorage(defaults: defaults, canUseVaultSync: true) == .iCloud)
|
|
#expect(NewVaultCredentialStorage.passwordStorage(defaults: defaults, canUseVaultSync: true) == .iCloud)
|
|
}
|
|
|
|
@Test func rolesTakePrecedenceOverLastSelectedModelAndDeletedModelsFallBack() {
|
|
let provider = ModelProvider(name: "Test", models: [AIModel(id: "role"), AIModel(id: "last")])
|
|
let role = AIModelRef(modelProviderID: provider.id, modelID: "role")
|
|
let last = AIModelRef(modelProviderID: provider.id, modelID: "last")
|
|
let ai = AIConfigurationModel(modelProviders: [provider], roles: [.chat: role], lastSelectedModel: last)
|
|
#expect(ai.resolve(conversation: nil as AIModelRef?)?.model.id == "role")
|
|
ai.removeModels(["role"], from: provider.id)
|
|
#expect(ai.resolve(.chat)?.model.id == "last")
|
|
}
|
|
|
|
@Test func historyRetainsTheNewestSharedLimitAndUnknownCompletion() {
|
|
let model = CommandHistoryModel()
|
|
#expect(model.record(" \n\t ") == nil)
|
|
for index in 0...CommandHistoryModel.perSessionLimit {
|
|
model.record("command \(index)")
|
|
}
|
|
let values = model.entries()
|
|
#expect(values.count == CommandHistoryModel.perSessionLimit)
|
|
#expect(values.first?.command == "command \(HistoryLimits.retainedEntries)")
|
|
#expect(values.last?.command == "command 1")
|
|
#expect(values.allSatisfy { $0.exitStatus == nil && $0.duration == nil })
|
|
}
|
|
|
|
}
|
|
|
|
@MainActor private final class FailingTerminalPreferences: TerminalPreferencesPersisting {
|
|
var saves = 0
|
|
func loadTerminal() throws -> TerminalPreferences { .init() }
|
|
func saveTerminal(_ value: TerminalPreferences) throws {
|
|
saves += 1
|
|
throw CocoaError(.fileWriteNoPermission)
|
|
}
|
|
}
|