Files
Termind/FrameworkTests/AISettingsIntegrationTests.swift
T
Kaede Akino e9c269ab5e Replace starter skills with built-in skill helpers
Keep previously installed starter skills as ordinary user-owned records. Provide hidden read-only Skill Creator and Skill Installer instructions, use AgentKit 0.13.0 for reviewed complete GitHub package installation, and remove skill seeding and Reset.

Move Hermes Memory below MCP, use Enabled for its switch, and localize its Chinese title as persistent memory. Preserve existing user edits and synchronize user skill packages through the existing atomic store.

Validation: AgentKit 213 tests and iOS 452 tests passed. macOS app 1183 tests passed; shared frameworks passed 231/232 with the existing SpeechWaveformViewTests PNG byte-equality failure. Related skill, sync, localization and UI checks passed. Strict Swift formatting, Chinese localization checks, and git diff --check passed.
2026-09-17 08:34:10 +08:00

157 lines
7.8 KiB
Swift

import AgentKit
import Foundation
import SwiftData
import TermindApplication
import TermindDomain
import TermindInfrastructure
import TermindPersistence
import Testing
@Suite @MainActor
struct AISettingsIntegrationTests {
@Test func rolesResolveOnlyEnabledChatModelsAndFallBackToChat() {
var provider = ModelProvider(name: "Enabled")
provider.models = [AIModel(id: "chat"), AIModel(id: "embedding", kind: .embedding)]
let ai = AIConfigurationModel(modelProviders: [provider])
#expect(ai.resolve(.chat)?.model.id == "chat")
#expect(ai.resolve(.commandGenerator)?.model.id == "chat")
#expect(ai.resolve(.guardian)?.model.id == "chat")
ai.setModelProviderEnabled(false, for: provider.id)
#expect(ai.resolve(.chat) == nil)
}
@Test func guardianAutomaticPrioritySelectsTheHighestModelInEachFamily() {
let provider = ModelProvider(
name: "Models",
models: [
AIModel(id: "OPENAI-CODEX/CODEX-AUTO-REVIEW"),
AIModel(id: "gpt-5.6-luna"), AIModel(id: "GPT-6.1-LUNA"),
AIModel(id: "claude-sonnet-5"), AIModel(id: "claude-sonnet-4-6"),
AIModel(id: "gemini-3.8-flash"), AIModel(id: "other"),
]
)
let codex = AIModelRef(modelProviderID: provider.id, modelID: "OPENAI-CODEX/CODEX-AUTO-REVIEW")
let newestLuna = AIModelRef(modelProviderID: provider.id, modelID: "GPT-6.1-LUNA")
let codexChat = AIConfigurationModel(modelProviders: [provider], roles: [.chat: codex])
#expect(codexChat.resolve(.guardian)?.model.id == "OPENAI-CODEX/CODEX-AUTO-REVIEW")
let lunaChat = AIConfigurationModel(modelProviders: [provider], roles: [.chat: newestLuna])
#expect(lunaChat.resolve(.guardian)?.model.id == "OPENAI-CODEX/CODEX-AUTO-REVIEW")
let explicit = AIConfigurationModel(
modelProviders: [provider], roles: [.chat: newestLuna, .guardian: newestLuna]
)
#expect(explicit.resolve(.guardian)?.model.id == "GPT-6.1-LUNA")
let fallbackProvider = ModelProvider(name: "Fallback", models: [AIModel(id: "other")])
let fallback = AIConfigurationModel(modelProviders: [fallbackProvider])
#expect(fallback.resolve(.guardian)?.model.id == "other")
let additionalFamilies = ModelProvider(
name: "Additional",
models: [
AIModel(id: "deepseek-v4-flash"), AIModel(id: "DEEPSEEK-V5.2-FLASH"),
AIModel(id: "glm-6-flash"),
]
)
let deepSeekChat = AIModelRef(
modelProviderID: additionalFamilies.id, modelID: "DEEPSEEK-V5.2-FLASH"
)
let additional = AIConfigurationModel(
modelProviders: [additionalFamilies], roles: [.chat: deepSeekChat]
)
#expect(additional.resolve(.guardian)?.model.id == "DEEPSEEK-V5.2-FLASH")
let semanticVersions = ModelProvider(
name: "Semantic",
models: [AIModel(id: "chat"), AIModel(id: "gpt-0.8-luna"), AIModel(id: "gpt-0.11-luna")]
)
let semantic = AIConfigurationModel(modelProviders: [semanticVersions])
#expect(semantic.resolve(.guardian)?.model.id == "gpt-0.11-luna")
let glmVersions = ModelProvider(
name: "GLM",
models: [AIModel(id: "chat"), AIModel(id: "glm-4-flash"), AIModel(id: "GLM-4.7-FLASH")]
)
let glm = AIConfigurationModel(modelProviders: [glmVersions])
#expect(glm.resolve(.guardian)?.model.id == "GLM-4.7-FLASH")
}
@Test func skillImportExportPreservesContentAndSupportingFiles() throws {
let directory = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }
let skill = AgentSkill(
name: "disk-check", title: "Disk", summary: "Inspect disks.", body: "# Disk\nRead usage.")
try Data(AgentSkillDocument.render(skill).utf8).write(to: directory.appending(path: "SKILL.md"))
try Data("unused".utf8).write(to: directory.appending(path: "reference.txt"))
let outcome = try SkillImporter.read(directory)
#expect(outcome.document.name == skill.name)
#expect(outcome.document.description == skill.summary)
#expect(outcome.document.body.contains("Read usage."))
#expect(outcome.warnings.isEmpty)
#expect(outcome.document.package.files["reference.txt"] == Data("unused".utf8))
let library = SkillLibraryModel()
let first = library.install(outcome.document, importedFrom: outcome.origin)
let second = library.install(outcome.document, importedFrom: outcome.origin)
#expect(first.name == "disk-check")
#expect(second.name == "disk-check-2")
}
@Test func importerRejectsOversizedAndNonUTF8Files() throws {
let file = FileManager.default.temporaryDirectory.appending(path: "\(UUID()).md")
defer { try? FileManager.default.removeItem(at: file) }
try Data(repeating: 65, count: AgentSkill.maximumBodyBytes + 1).write(to: file)
#expect(throws: (any Error).self) { try SkillImporter.read(file) }
try Data([0xff, 0xfe, 0xff]).write(to: file)
#expect(throws: (any Error).self) { try SkillImporter.read(file) }
}
@Test func syncedLibraryKeepsBuiltInsOutOfPersistenceAndPreservesOtherSettings() throws {
let container = try TermindPersistence.makeContainer(inMemory: true)
let domain = "AISettingsIntegrationTests.\(UUID())"
let defaults = UserDefaults(suiteName: domain)!
defer { defaults.removePersistentDomain(forName: domain) }
let persistence = SettingsSyncPersistence(context: container.mainContext, defaults: defaults)
try persistence.prepare()
let library = SkillLibraryModel(persistence: SyncedSkillLibraryPersistence(persistence))
#expect(library.skills.isEmpty)
#expect(library.catalog.names == ["skill-creator", "skill-installer"])
try persistence.prepare()
#expect(try persistence.load().skills.isEmpty)
let ai = AIConfigurationModel(persistence: SyncedAIConfigurationPersistence(persistence))
let server = MCPServer(name: "Preserved")
ai.upsert(server)
let custom = AgentSkill(name: "custom", summary: "s", body: "b")
library.upsert(custom)
#expect(try persistence.load().configuration.mcpServers == [server])
#expect(try persistence.load().skills.contains { $0.id == custom.id })
#expect(library.skills.map(\.id) == [custom.id])
#expect(
try persistence.journal.pendingEnvelopes().allSatisfy {
guard case .skill(let skill, _) = $0.payload else { return true }
return !AgentSkillLibrary.reservedNames.contains(skill.name)
})
}
@Test func failedProviderTransactionKeepsModelsAndAssignmentsTogether() {
let persistence = FailingAISettings()
var provider = ModelProvider(name: "Test")
provider.models = [AIModel(id: "one")]
let ref = AIModelRef(modelProviderID: provider.id, modelID: "one")
persistence.snapshot = .init(providers: [provider], roles: [.chat: ref])
let ai = AIConfigurationModel(persistence: persistence)
provider.models = []
#expect(!ai.saveProvider(provider, endpoint: ProviderEndpoint(url: "https://example.invalid/v1")))
#expect(ai.modelProvider(provider.id)?.models.map(\.id) == ["one"])
#expect(ai.roles[.chat] == ref)
}
}
@MainActor private final class FailingAISettings: AIConfigurationPersisting {
var snapshot = AIConfigurationSnapshot(providers: [], roles: [:])
func load() throws -> AIConfigurationSnapshot { snapshot }
func save(_ value: AIConfigurationSnapshot) throws { throw CocoaError(.fileWriteNoPermission) }
}