Files
OSGKeyboard/OSGKeyboardTests/OfficialSkillCatalogTests.swift
T
2026-08-27 18:01:46 +08:00

347 lines
12 KiB
Swift

// OfficialSkillCatalogTests.swift
// OSGKeyboardTests
@testable import OSGKeyboardShared
import XCTest
@MainActor
final class OfficialSkillCatalogTests: XCTestCase {
private func makeDefaults() -> UserDefaults {
let suite = "group.com.osgkeyboard.shared.tests.officialSkills.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
defaults.removePersistentDomain(forName: suite)
return defaults
}
private func definition(
id: String = "official.rewrite",
systemImage: String = "wand.and.stars",
sortOrder: Int = 10,
kind: AIClipboardSkillKind = .transform,
englishName: String = "Rewrite",
englishSummary: String = "Rewrite clipboard text clearly",
englishPrompt: String = "Rewrite the clipboard clearly."
) -> OfficialSkillDefinition {
OfficialSkillDefinition(
id: id,
systemImage: systemImage,
sortOrder: sortOrder,
kind: kind,
thinkingEnabled: true,
localizations: [
"zh-Hans": OfficialSkillLocalization(
name: "改写",
summary: "清晰改写剪贴板内容",
prompt: "请清晰改写剪贴板内容。"
),
"en": OfficialSkillLocalization(
name: englishName,
summary: englishSummary,
prompt: englishPrompt
)
]
)
}
func testValidatedCatalogSortsAndMapsBothLanguages() throws {
let catalog = try OfficialSkillCatalog(
revision: 7,
generatedAt: "2026-08-21T03:00:00Z",
skills: [
definition(id: "official.second", sortOrder: 20),
definition(id: "official.first", sortOrder: 10)
]
).validated()
XCTAssertEqual(catalog.skills.map(\.id), ["official.first", "official.second"])
XCTAssertEqual(
catalog.resolvedSkills(language: .chinese).first?.customName,
"改写"
)
XCTAssertEqual(
catalog.resolvedSkills(language: .english).first?.customPrompt,
"Rewrite the clipboard clearly."
)
XCTAssertTrue(catalog.resolvedSkills(language: .english).first?.thinkingEnabled ?? false)
XCTAssertTrue(catalog.resolvedSkills(language: .english).first?.isDefault ?? false)
}
func testNewOfficialTextSkillsAreInstalledWithoutRestoringDisabledSkills() throws {
let defaults = makeDefaults()
let store = AppGroupStore(defaults: defaults)
let first = OfficialSkillCatalog(
revision: 1,
skills: [definition(id: "official.rewrite", sortOrder: 10)]
)
try store.setOfficialSkillCatalog(
first,
installingNewDefaultSkills: true
)
XCTAssertTrue(store.agentSkillLayout.isEnabled("official.rewrite"))
store.setAgentSkillLayout(
AIAgentSkillLayout(
enabledIDs: AIAgentSkillLayout.defaultEnabledIDs,
confirmedShortcutIDs: []
)
)
let second = OfficialSkillCatalog(
revision: 2,
skills: [
definition(id: "official.rewrite", sortOrder: 10),
definition(id: "official.second", sortOrder: 20)
]
)
try store.setOfficialSkillCatalog(
second,
installingNewDefaultSkills: true
)
XCTAssertFalse(store.agentSkillLayout.isEnabled("official.rewrite"))
XCTAssertTrue(store.agentSkillLayout.isEnabled("official.second"))
XCTAssertEqual(store.agentSkillLayout.enabledIDs.last, "official.second")
}
func testVersionTwoLayoutInstallsCachedOfficialTextSkillsOnce() throws {
let defaults = makeDefaults()
let store = AppGroupStore(defaults: defaults)
try store.setOfficialSkillCatalog(
OfficialSkillCatalog(
revision: 1,
skills: [definition(id: "official.rewrite")]
)
)
let layout = AIAgentSkillLayout(
enabledIDs: [AIClipboardSkillCatalog.replyID],
confirmedShortcutIDs: []
)
defaults.set(
try JSONEncoder().encode(layout),
forKey: AppGroupConfiguration.Keys.agentSkillLayout
)
defaults.set(
2,
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
)
let migrated = store.agentSkillLayout
XCTAssertEqual(
migrated.enabledIDs,
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.blessingReplyID,
AIClipboardSkillCatalog.organizeListID,
"official.rewrite"
]
)
}
func testValidationRejectsWholeInvalidSnapshot() {
let invalidCases = [
OfficialSkillCatalog(
schemaVersion: 2,
revision: 1,
skills: [definition()]
),
OfficialSkillCatalog(
revision: 1,
skills: [definition(), definition()]
),
OfficialSkillCatalog(
revision: 1,
skills: [definition(id: "remote.rewrite")]
),
OfficialSkillCatalog(
revision: 1,
skills: [definition(kind: .export)]
),
OfficialSkillCatalog(
revision: 1,
skills: [definition(englishPrompt: " ")]
),
OfficialSkillCatalog(
revision: 1,
skills: [
definition(
englishPrompt: String(
repeating: "x",
count: OfficialSkillCatalog.maximumPromptCharacters + 1
)
)
]
)
]
for catalog in invalidCases {
XCTAssertThrowsError(try catalog.validated())
}
}
func testContractBoundariesAcceptExactMaximums() {
let prefix = "official."
let maximumID = prefix + String(
repeating: "a",
count: OfficialSkillCatalog.maximumIDCharacters - prefix.count
)
let maximumDefinition = definition(
id: maximumID,
systemImage: String(
repeating: "s",
count: OfficialSkillCatalog.maximumSystemImageCharacters
),
sortOrder: OfficialSkillCatalog.maximumSortOrder,
englishName: String(
repeating: "n",
count: OfficialSkillCatalog.maximumNameCharacters
),
englishSummary: String(
repeating: "s",
count: OfficialSkillCatalog.maximumSummaryCharacters
),
englishPrompt: String(
repeating: "p",
count: OfficialSkillCatalog.maximumPromptCharacters
)
)
XCTAssertNoThrow(
try OfficialSkillCatalog(revision: 1, skills: [maximumDefinition]).validated()
)
XCTAssertNoThrow(
try OfficialSkillCatalog(
revision: 1,
skills: [definition(sortOrder: 0)]
).validated()
)
}
func testContractBoundariesRejectValuesOverLimits() {
let invalidDefinitions = [
definition(
id: "official." + String(
repeating: "a",
count: OfficialSkillCatalog.maximumIDCharacters + 1
- "official.".count
)
),
definition(
systemImage: String(
repeating: "s",
count: OfficialSkillCatalog.maximumSystemImageCharacters + 1
)
),
definition(sortOrder: -1),
definition(sortOrder: OfficialSkillCatalog.maximumSortOrder + 1),
definition(
englishName: String(
repeating: "n",
count: OfficialSkillCatalog.maximumNameCharacters + 1
)
),
definition(
englishSummary: String(
repeating: "s",
count: OfficialSkillCatalog.maximumSummaryCharacters + 1
)
),
definition(
englishPrompt: String(
repeating: "p",
count: OfficialSkillCatalog.maximumPromptCharacters + 1
)
)
]
for definition in invalidDefinitions {
XCTAssertThrowsError(
try OfficialSkillCatalog(revision: 1, skills: [definition]).validated()
)
}
}
func testInvalidWritePreservesLastKnownGoodSnapshot() throws {
let defaults = makeDefaults()
let store = AppGroupStore(defaults: defaults)
var good = OfficialSkillCatalog(revision: 4, skills: [definition()])
good.refreshedAt = Date(timeIntervalSince1970: 100)
good.etag = "\"rev-4\""
try store.setOfficialSkillCatalog(good)
let invalid = OfficialSkillCatalog(
revision: 5,
skills: [definition(englishPrompt: "")]
)
XCTAssertThrowsError(try store.setOfficialSkillCatalog(invalid))
XCTAssertEqual(store.officialSkillCatalog.revision, 4)
XCTAssertEqual(store.officialSkillCatalog.etag, "\"rev-4\"")
}
func testMergeOrderAndPrecedenceAreBuiltInOfficialUser() throws {
let official = OfficialSkillCatalog(
revision: 1,
skills: [
definition(id: AIClipboardSkillCatalog.replyID, sortOrder: 0),
definition(id: "official.rewrite", sortOrder: 1)
]
)
var users = AIUserSkillCatalog()
try users.upsert(AIUserSkill(id: "user.local", name: "Local", prompt: "Local prompt"))
let merged = AIClipboardSkillCatalog.all(
officialCatalog: official,
userCatalog: users,
uiLanguage: .english
)
XCTAssertEqual(merged.filter { $0.id == AIClipboardSkillCatalog.replyID }.count, 1)
XCTAssertNil(merged.first { $0.id == AIClipboardSkillCatalog.replyID }?.customPrompt)
XCTAssertEqual(
Array(merged.suffix(2).map(\.id)),
["official.rewrite", "user.local"]
)
}
func testReloadPublishesChangedOfficialCopyWithStableEnabledIDs() throws {
let defaults = makeDefaults()
let appGroup = AppGroupStore(defaults: defaults)
appGroup.setUILanguage(.english)
var first = OfficialSkillCatalog(
revision: 1,
skills: [definition(englishPrompt: "First prompt")]
)
first.refreshedAt = Date()
try appGroup.setOfficialSkillCatalog(first)
appGroup.setAgentSkillLayout(
AIAgentSkillLayout(
enabledIDs: ["official.rewrite"],
confirmedShortcutIDs: []
)
)
let store = AIAgentSkillLayoutStore(defaults: defaults)
XCTAssertEqual(store.enabledSkills.first?.customPrompt, "First prompt")
var second = OfficialSkillCatalog(
revision: 2,
skills: [definition(englishPrompt: "Second prompt")]
)
second.refreshedAt = Date()
try appGroup.setOfficialSkillCatalog(second)
store.reload()
XCTAssertEqual(store.layout.enabledIDs, ["official.rewrite"])
XCTAssertEqual(store.enabledSkills.first?.customPrompt, "Second prompt")
}
}