Expand onboarding and adaptive keyboard intelligence

Add resilient usage analytics, OOBE gateway flows, clipboard semantic ranking, purchase recovery, style learning, and managed current-information search.
This commit is contained in:
Rocky
2026-08-22 16:33:18 +08:00
parent ac374631ae
commit e5a83843db
162 changed files with 23574 additions and 1013 deletions
@@ -0,0 +1,263 @@
// 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)
}
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")
}
}