2cc81c4628
Add local clipboard intent recommendations, webpage and phone actions, and safer host handoffs. Refine managed gateway, catalog refresh, onboarding, and adaptive polish behavior.
333 lines
11 KiB
Swift
333 lines
11 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, "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")
|
|
}
|
|
}
|