Files
OSGKeyboard/OSGKeyboardTests/OfficialSkillCatalogTests.swift
Rocky 57844ce615
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled
Fix skill migration test regressions
Use an explicit remote clipboard card after removing local clipboard hints and assert the complete set of newly migrated default skills.
2026-08-23 20:51:29 +08:00

340 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.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID,
"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")
}
}