b275b6b0d9
- Cover reply-variant decision paths, scene-detector abstention, and the merged reply center stance catalog. - Lock in clipboard semantic analyzer thresholds, complaint-only suppression, and existing-skill routing. - Add durable session, refresh, and managed-gateway reliability tests for the Apple account path. - Verify personal-style prompt derivation, low-confidence ASR tendencies, and Polish style learning against real corpus evidence. - Exercise the assistant keyboard and Polish styles UI flows in end-to-end UI tests.
343 lines
12 KiB
Swift
343 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.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")
|
|
}
|
|
}
|