checkpoint before checking out main

This commit is contained in:
Rocky
2026-08-25 13:42:00 +08:00
parent 57844ce615
commit dccafe6f9e
105 changed files with 4302 additions and 1323 deletions
+102 -7
View File
@@ -21,6 +21,37 @@ final class AIAgentSkillLayoutTests: XCTestCase {
XCTAssertTrue(layout.confirmedShortcutIDs.isEmpty)
}
func testSystemSemanticSkillsStayEnabledButHiddenFromSkillManagement() {
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
let hiddenIDs = AIClipboardSkillCatalog.hiddenFromSkillManagementIDs
XCTAssertTrue(hiddenIDs.isSubset(of: Set(store.enabledSkills.map(\.id))))
XCTAssertTrue(
hiddenIDs.isDisjoint(with: Set(store.skillManagementEnabledSkills.map(\.id)))
)
XCTAssertTrue(
hiddenIDs.isDisjoint(with: Set(store.skillManagementAvailableSkills.map(\.id)))
)
XCTAssertTrue(
store.skillManagementEnabledSkills.contains {
$0.id == AIClipboardSkillCatalog.translateID
}
)
store.disable(AIClipboardSkillCatalog.replyID)
XCTAssertFalse(
store.skillManagementAvailableSkills.contains {
$0.id == AIClipboardSkillCatalog.replyID
}
)
XCTAssertTrue(
store.mergedCatalog.contains {
$0.id == AIClipboardSkillCatalog.replyID
}
)
}
func testEmptyEnabledListIsPreserved() {
let defaults = makeDefaults()
let store = AppGroupStore(defaults: defaults)
@@ -30,7 +61,7 @@ final class AIAgentSkillLayoutTests: XCTestCase {
XCTAssertEqual(store.agentSkillLayout.enabledIDs, [])
}
func testLegacyLayoutAppendsNewDefaultSkillsWithoutRestoringDisabledLegacySkill() throws {
func testLegacyLayoutInstallsCurrentRequiredDefaultSkills() throws {
let defaults = makeDefaults()
let legacy = AIAgentSkillLayout(
enabledIDs: [
@@ -50,7 +81,7 @@ final class AIAgentSkillLayoutTests: XCTestCase {
Array(migrated.enabledIDs.prefix(2)),
[AIClipboardSkillCatalog.replyID, AIClipboardSkillCatalog.translateID]
)
XCTAssertFalse(migrated.enabledIDs.contains(AIClipboardSkillCatalog.summarizeID))
XCTAssertTrue(migrated.enabledIDs.contains(AIClipboardSkillCatalog.summarizeID))
XCTAssertTrue(migrated.enabledIDs.contains(AIClipboardSkillCatalog.acceptInvitationID))
XCTAssertTrue(migrated.enabledIDs.contains(AIClipboardSkillCatalog.extractEventsID))
}
@@ -77,10 +108,16 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.playfulReplyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.organizeListID
]
)
}
@@ -106,10 +143,16 @@ final class AIAgentSkillLayoutTests: XCTestCase {
migrated.enabledIDs,
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.organizeListID
]
)
}
@@ -135,8 +178,14 @@ final class AIAgentSkillLayoutTests: XCTestCase {
migrated.enabledIDs,
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.organizeListID
]
)
}
@@ -167,17 +216,63 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.clarifyRequestID
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.organizeListID
]
)
XCTAssertEqual(
defaults.integer(
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
),
6
7
)
}
func testVersionSixLayoutInstallsRequiredDefaultsOnlyOnce() throws {
let defaults = makeDefaults()
let initial = AIAgentSkillLayout(
enabledIDs: [AIClipboardSkillCatalog.playfulReplyID],
confirmedShortcutIDs: []
)
defaults.set(
try JSONEncoder().encode(initial),
forKey: AppGroupConfiguration.Keys.agentSkillLayout
)
defaults.set(
6,
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
)
let store = AppGroupStore(defaults: defaults)
XCTAssertEqual(
store.agentSkillLayout.enabledIDs,
[
AIClipboardSkillCatalog.playfulReplyID,
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.clarifyRequestID,
AIClipboardSkillCatalog.empathyReplyID,
AIClipboardSkillCatalog.organizeListID
]
)
store.setAgentSkillLayout(
AIAgentSkillLayout(
enabledIDs: store.agentSkillLayout.enabledIDs.filter {
$0 != AIClipboardSkillCatalog.replyID
},
confirmedShortcutIDs: []
)
)
XCTAssertFalse(store.agentSkillLayout.isEnabled(AIClipboardSkillCatalog.replyID))
}
func testCannotEnableExportSkillBeforeShortcutConfirmation() {
let store = AIAgentSkillLayoutStore(defaults: makeDefaults())
store.disable(AIClipboardSkillCatalog.extractTodosID)
@@ -34,6 +34,33 @@ final class AIHistoryAndUsageTests: XCTestCase {
XCTAssertEqual(store.totalInputCharacterCount, 4)
}
@MainActor
func testCurrentMonthBuildsCompleteLeapMonthAndZeroFillsMissingDays() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try XCTUnwrap(TimeZone(secondsFromGMT: 0))
let now = try XCTUnwrap(
calendar.date(from: DateComponents(year: 2024, month: 2, day: 14))
)
let points = UsageStatisticsStore.currentMonth(
from: [
"2024-02-01": 120,
"2024-02-29": 480
],
now: now,
calendar: calendar
)
XCTAssertEqual(points.count, 29)
XCTAssertEqual(points.first?.value, 120)
XCTAssertEqual(points[13].value, 0)
XCTAssertEqual(points.last?.value, 480)
XCTAssertEqual(
points.map { calendar.component(.day, from: $0.date) },
Array(1...29)
)
}
func testLegacyHistoryEntryDefaultsToDictationSource() throws {
let payload: [String: Any] = [
"id": UUID().uuidString,
@@ -194,6 +194,51 @@ final class AccountAPIClientTests: XCTestCase {
XCTAssertEqual(requests.single?.url?.path, "/v1/auth/refresh")
}
func testRecreatedClientRestoresRetainedSessionAndRefreshesIt() async throws {
let retained = makeAccountSession(accessExpiry: 1_020)
let replacement = makeAccountSession(
accessToken: "access-after-reinstall",
refreshToken: "refresh-after-reinstall"
)
let transport = QueueAccountTransport([
.init(statusCode: 200, body: try sessionEnvelopeData(retained)),
.init(statusCode: 200, body: try sessionEnvelopeData(replacement))
])
let store = InMemoryAccountSecurityStore()
let originalClient = AccountAPIClient(
baseURL: URL(string: "https://account.test")!,
transport: transport,
sessionVault: store,
now: { Date(timeIntervalSince1970: 1_000) }
)
_ = try await originalClient.signInWithApple(
AppleSignInRequest(
identityToken: "identity",
authorizationCode: "authorization",
nonce: "raw-nonce",
deviceCheckToken: "device-token",
appAttest: nil
)
)
let recreatedClient = AccountAPIClient(
baseURL: URL(string: "https://account.test")!,
transport: transport,
sessionVault: store,
now: { Date(timeIntervalSince1970: 1_000) }
)
let accessToken = try await recreatedClient.accessTokenForAuthorizedRequest()
XCTAssertEqual(accessToken, replacement.accessToken)
let stored = await store.session
XCTAssertEqual(stored, replacement)
let requests = await transport.requests
XCTAssertEqual(requests.map(\.url?.path), [
"/v1/auth/apple",
"/v1/auth/refresh"
])
}
func testConcurrentUnauthorizedRequestsMergeRefreshRotation() async throws {
let old = makeAccountSession()
let replacement = makeAccountSession(
@@ -60,6 +60,7 @@ final class AccountCenterViewModelTests: XCTestCase {
)
await coordinator.restoreIfNeeded()
XCTAssertEqual(coordinator.sessionPhase, .signedOut)
let handled = coordinator.handleIncomingURL(
URL(string: "https://osglab.com/i/\(validCode)")!
)
@@ -123,6 +124,83 @@ final class AccountCenterViewModelTests: XCTestCase {
XCTAssertEqual(restoreCount, 1)
}
@MainActor
func testTransientRestoreFailureCanRetryWithoutSigningOut() async {
let account = AccountSession(
accountID: UUID(),
createdAtEpochSeconds: 1_700_000_000
)
let service = AccountServiceSpy(
restoredSession: account,
snapshot: makeSnapshot(account: account),
restoreFailureCount: 1
)
let coordinator = AccountSessionCoordinator(
dependencies: AccountDependencies(
sessionService: service,
centerService: service
),
pendingReferralStore: InMemoryPendingReferralStore()
)
await coordinator.restoreIfNeeded()
XCTAssertEqual(coordinator.sessionPhase, .restoring)
XCTAssertEqual(coordinator.operationErrorKey, "account.error.restore")
let firstClearCount = await service.managedGatewayClearCount()
XCTAssertEqual(firstClearCount, 0)
await coordinator.restoreIfNeeded()
XCTAssertEqual(coordinator.sessionPhase, .signedIn(account))
XCTAssertNil(coordinator.operationErrorKey)
let restoreCount = await service.restoreCount()
XCTAssertEqual(restoreCount, 2)
}
@MainActor
func testSignInOperationRemainsVisibleUntilAuthenticationCompletes() async {
let account = AccountSession(
accountID: UUID(),
createdAtEpochSeconds: 1_700_000_000
)
let service = AccountServiceSpy(
restoredSession: nil,
signInSession: account,
snapshot: makeSnapshot(account: account),
signInDelayNanoseconds: 20_000_000
)
let coordinator = AccountSessionCoordinator(
dependencies: AccountDependencies(
sessionService: service,
centerService: service
),
pendingReferralStore: InMemoryPendingReferralStore()
)
await coordinator.restoreIfNeeded()
let signIn = Task {
await coordinator.signIn(
with: AppleAuthorizationPayload(
identityToken: "identity",
authorizationCode: "authorization",
nonce: "nonce"
)
)
}
while coordinator.operation != .signingIn {
await Task.yield()
}
XCTAssertEqual(coordinator.operation, .signingIn)
XCTAssertEqual(coordinator.sessionPhase, .signedOut)
await signIn.value
XCTAssertNil(coordinator.operation)
XCTAssertEqual(coordinator.sessionPhase, .signedIn(account))
}
@MainActor
func testFreshAccountSnapshotDoesNotReloadWhenEnteringAccountPages() async {
let account = AccountSession(
@@ -529,12 +607,15 @@ private final class AccountSessionEventSourceStub: AccountSessionEventSourcing {
private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing {
private let restored: AccountSession?
private let signedInAccount: AccountSession?
private let centerSnapshot: AccountCenterSnapshot?
private let refreshedSnapshot: AccountCenterSnapshot?
private let shouldFailRedemption: Bool
private let shouldFailAccountRefresh: Bool
private let signOutDelayNanoseconds: UInt64
private let signInDelayNanoseconds: UInt64
private let accountLoadDelayNanoseconds: UInt64
private var remainingRestoreFailures: Int
private var redeemed: [String] = []
private var centerLoadCount = 0
private var logoutCount = 0
@@ -545,30 +626,43 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
init(
restoredSession: AccountSession?,
signInSession: AccountSession? = nil,
snapshot: AccountCenterSnapshot? = nil,
refreshedSnapshot: AccountCenterSnapshot? = nil,
shouldFailRedemption: Bool = false,
shouldFailAccountRefresh: Bool = false,
signOutDelayNanoseconds: UInt64 = 0,
signInDelayNanoseconds: UInt64 = 0,
restoreFailureCount: Int = 0,
accountLoadDelayNanoseconds: UInt64 = 0
) {
restored = restoredSession
signedInAccount = signInSession ?? restoredSession
centerSnapshot = snapshot
self.refreshedSnapshot = refreshedSnapshot
self.shouldFailRedemption = shouldFailRedemption
self.shouldFailAccountRefresh = shouldFailAccountRefresh
self.signOutDelayNanoseconds = signOutDelayNanoseconds
self.signInDelayNanoseconds = signInDelayNanoseconds
remainingRestoreFailures = restoreFailureCount
self.accountLoadDelayNanoseconds = accountLoadDelayNanoseconds
}
func restoreSession() async throws -> AccountSession? {
sessionRestoreCount += 1
if remainingRestoreFailures > 0 {
remainingRestoreFailures -= 1
throw AccountServiceSpyError.failed
}
return restored
}
func signIn(with payload: AppleAuthorizationPayload) async throws -> AccountSession {
guard let restored else { throw AccountIntegrationError.unavailable }
return restored
if signInDelayNanoseconds > 0 {
try await Task.sleep(nanoseconds: signInDelayNanoseconds)
}
guard let signedInAccount else { throw AccountIntegrationError.unavailable }
return signedInAccount
}
func signOut() async throws {
@@ -30,7 +30,8 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertEqual(config.translationTargetLocaleId, TranslationLanguageCatalog.offLocaleId)
XCTAssertFalse(config.translationEnabled)
XCTAssertEqual(config.handednessPreference, .left)
XCTAssertTrue(config.cursorDragNavigationEnabled)
// Legacy field remains decode-compatible after the UI feature was removed.
XCTAssertFalse(config.cursorDragNavigationEnabled)
XCTAssertEqual(config.keyboardHapticIntensity, .light)
XCTAssertEqual(config.polishIntensity, .light)
XCTAssertEqual(config.aiResponseLength, .medium)
@@ -93,6 +94,24 @@ final class AppGroupConfigurationTests: XCTestCase {
XCTAssertEqual(loaded.flowInactivityDuration, .threeHours)
}
func testAppGroupStorePersistsLocaleChanges() {
let defaults = makeDefaults()
let store = AppGroupStore(defaults: defaults)
store.setLocaleId("en-US")
XCTAssertEqual(store.localeId, "en-US")
}
func testRemovedCursorDragSettingStillDecodesLegacyValue() {
let defaults = makeDefaults()
defaults.set(true, forKey: AppGroupConfiguration.Keys.cursorDragNavigationEnabled)
let loaded = AppGroupConfiguration.load(fromAvailable: defaults)
XCTAssertTrue(loaded.cursorDragNavigationEnabled)
}
func testFieldLevelSavePreservesNewerUnrelatedProcessChange() {
let defaults = makeDefaults()
let baseline = AppGroupConfiguration.load(fromAvailable: defaults)
@@ -45,7 +45,13 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
preferredLanguages: ["zh-Hans"]
).map(\.id)
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.translateID])
XCTAssertEqual(
recommendations,
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.replyID
]
)
}
func testInvitationWithDatePromotesCalendarAndBothReplyChoices() {
@@ -91,7 +97,8 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations,
[
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.replyID
]
)
}
@@ -111,7 +118,8 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations,
[
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.replyID
]
)
}
@@ -130,6 +138,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.openLinkID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.summarizeWebPageID))
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.replyID])
}
func testSinglePhoneNumberOffersCallAndCreateContact() {
@@ -149,7 +158,8 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations,
[
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.replyID
]
)
}
@@ -169,7 +179,8 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations,
[
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.replyID
]
)
}
@@ -190,6 +201,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.callPhoneID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.createContactID))
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.replyID])
}
func testTaskListPromotesTodoAndOrganizationSkills() {
@@ -259,7 +271,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertEqual(ranked, baseline)
}
func testRecommendationsSelectOnlySemanticallyRelevantSkills() {
func testRecommendationsAddReplyToSemanticallyRelevantSkills() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: AIClipboardSkillCatalog.catalog,
sourceText: "北京市朝阳区望京街 10 号,到了给我电话。",
@@ -268,12 +280,18 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
limit: 5
).map(\.id)
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.navigateID])
XCTAssertEqual(
recommendations,
[
AIClipboardSkillCatalog.navigateID,
AIClipboardSkillCatalog.replyID
]
)
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.summarizeID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateID))
}
func testRecommendationsStayEmptyWhenNoSemanticLabelMatches() {
func testRecommendationsFallBackToReplyWhenNoSemanticLabelMatches() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: AIClipboardSkillCatalog.catalog,
sourceText: "知道了",
@@ -282,7 +300,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
limit: 5
).map(\.id)
XCTAssertTrue(recommendations.isEmpty)
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.replyID])
}
func testNegativeReplyableMessageDoesNotOfferPlayfulReply() {
@@ -300,7 +318,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertEqual(recommendations, [AIClipboardSkillCatalog.replyID])
}
func testInvitationKeepsTwoSpecificRepliesWithoutGenericReplies() {
func testInvitationKeepsSpecificRepliesAndGenericReply() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: AIClipboardSkillCatalog.catalog,
sourceText: "今晚七点老地方吃饭,你能来吗?",
@@ -319,12 +337,13 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
[
AIClipboardSkillCatalog.extractEventsID,
AIClipboardSkillCatalog.acceptInvitationID,
AIClipboardSkillCatalog.declineInvitationID
AIClipboardSkillCatalog.declineInvitationID,
AIClipboardSkillCatalog.replyID
]
)
}
func testRecommendationsNeverContainMoreThanTwoReplySkills() {
func testForeignQuestionKeepsReplyAndOneSpecializedFollowUp() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: AIClipboardSkillCatalog.catalog,
sourceText: "Could you send the final proposal by Friday?",
@@ -116,6 +116,47 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase {
)
}
func testLocalAndPolishOnlySetupLinksToTextPolish() {
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyDeepLink(
isLocalEngine: true,
isASRConfigured: true
),
.textPolish
)
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyDeepLink(
isLocalEngine: false,
isASRConfigured: true
),
.textPolish
)
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyMessageKey(
isLocalEngine: false,
isASRConfigured: true
),
"home.setup.polishKeyMissing"
)
}
func testCloudSetupLinksToSpeechRecognitionWhenASRIsMissing() {
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyDeepLink(
isLocalEngine: false,
isASRConfigured: false
),
.speechRecognition
)
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyMessageKey(
isLocalEngine: false,
isASRConfigured: false
),
"home.setup.cloudIncomplete"
)
}
private func descriptor(
for lifecycle: FlowPiPLifecycleState
) -> FlowHomePiPStatusDescriptor {
@@ -1,15 +1,15 @@
// RimeFrequentTermStoreTests.swift
// FrequentTermStoreTests.swift
// OSGKeyboardTests
@testable import OSGKeyboardShared
import XCTest
final class RimeFrequentTermStoreTests: XCTestCase {
final class FrequentTermStoreTests: XCTestCase {
private var defaults: UserDefaults!
private var suiteName: String!
override func setUpWithError() throws {
suiteName = "RimeFrequentTermStoreTests.\(UUID().uuidString)"
suiteName = "FrequentTermStoreTests.\(UUID().uuidString)"
defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
}
@@ -19,8 +19,8 @@ final class RimeFrequentTermStoreTests: XCTestCase {
suiteName = nil
}
func testRepeatedRimeCommitBecomesSuggestion() {
let store = RimeFrequentTermStore(defaults: defaults)
func testRepeatedCommitBecomesSuggestion() {
let store = FrequentTermStore(defaults: defaults)
store.recordCommittedText("少数派")
XCTAssertTrue(store.suggestions(excludingPersonalTerms: []).isEmpty)
@@ -32,7 +32,7 @@ final class RimeFrequentTermStoreTests: XCTestCase {
}
func testSuggestionsExcludeExistingDictionaryTermsAndCommonWords() {
let store = RimeFrequentTermStore(defaults: defaults)
let store = FrequentTermStore(defaults: defaults)
for _ in 0..<4 {
store.recordCommittedText("我们")
store.recordCommittedText("飞书文档")
@@ -47,7 +47,7 @@ final class RimeFrequentTermStoreTests: XCTestCase {
}
func testSuggestionsRankFrequencyBeforeRecency() {
let store = RimeFrequentTermStore(defaults: defaults)
let store = FrequentTermStore(defaults: defaults)
let earlier = Date(timeIntervalSince1970: 100)
let later = Date(timeIntervalSince1970: 200)
for _ in 0..<3 {
@@ -64,7 +64,7 @@ final class RimeFrequentTermStoreTests: XCTestCase {
}
func testPunctuationAndSingleCharactersAreIgnored() {
let store = RimeFrequentTermStore(defaults: defaults)
let store = FrequentTermStore(defaults: defaults)
for _ in 0..<3 {
store.recordCommittedText("")
store.recordCommittedText("你好!")
@@ -75,7 +75,7 @@ final class RimeFrequentTermStoreTests: XCTestCase {
}
func testClearRemovesLearnedSuggestions() {
let store = RimeFrequentTermStore(defaults: defaults)
let store = FrequentTermStore(defaults: defaults)
store.recordCommittedText("少数派")
store.recordCommittedText("少数派")
XCTAssertFalse(store.suggestions(excludingPersonalTerms: []).isEmpty)
@@ -84,4 +84,48 @@ final class RimeFrequentTermStoreTests: XCTestCase {
XCTAssertTrue(store.suggestions(excludingPersonalTerms: []).isEmpty)
}
func testEnglishTermsAreCaseInsensitiveAndCommonWordsAreIgnored() {
let store = FrequentTermStore(defaults: defaults)
for term in ["OpenAI", "openAI", "the", "the"] {
store.recordCommittedText(term)
}
let suggestion = store.suggestions(excludingPersonalTerms: []).first
XCTAssertEqual(suggestion?.term, "openAI")
XCTAssertEqual(suggestion?.commitCount, 2)
}
func testEnglishNamesAndProductSeparatorsAreAccepted() {
let store = FrequentTermStore(defaults: defaults)
for term in ["O'Connor", "O'Connor", "GPT-5", "GPT-5"] {
store.recordCommittedText(term)
}
XCTAssertEqual(
Set(store.suggestions(excludingPersonalTerms: []).map(\.term)),
["O'Connor", "GPT-5"]
)
}
func testLegacyRimeTermsMigrateWithoutLosingHistory() throws {
let legacyTerm = FrequentTerm(
term: "少数派",
commitCount: 3,
firstSeenAt: Date(timeIntervalSince1970: 100),
lastSeenAt: Date(timeIntervalSince1970: 200)
)
defaults.set(
try JSONEncoder().encode([legacyTerm]),
forKey: FrequentTermStore.legacyRimeDefaultsKey
)
let store = FrequentTermStore(defaults: defaults)
let suggestions = store.suggestions(excludingPersonalTerms: [])
XCTAssertEqual(suggestions.first, legacyTerm)
XCTAssertNotNil(defaults.data(forKey: FrequentTermStore.defaultsKey))
XCTAssertNil(defaults.data(forKey: FrequentTermStore.legacyRimeDefaultsKey))
}
}
@@ -147,15 +147,15 @@ final class IntelligentPolishTests: XCTestCase {
XCTAssertEqual(PersonalDictionary.Entry.inferCategory(for: "LLM"), .acronym)
}
func testPersonalDictionaryMigratesLegacyHistorySource() {
let legacy = PersonalDictionary(entries: [
func testPersonalDictionaryPreservesRecommendedHistorySource() {
let dictionary = PersonalDictionary(entries: [
PersonalDictionary.Entry(term: "Kubernetes", category: .productName, source: .history)
])
let data = try! JSONEncoder().encode(legacy)
let data = try! JSONEncoder().encode(dictionary)
defaults.set(data, forKey: "config.personalDictionary.v1")
let loaded = store.personalDictionary
XCTAssertEqual(loaded.entries.first?.source, .manual)
XCTAssertEqual(loaded.entries.first?.source, .history)
XCTAssertEqual(loaded.entries.first?.term, "Kubernetes")
}
@@ -0,0 +1,128 @@
// PersonalDictionaryEntryServiceTests.swift
// OSGKeyboardTests
//
// Verifies that every manual-entry UI gets the same alias-generation behavior
// without allowing a late asynchronous response to overwrite newer edits.
@testable import OSGKeyboardShared
import XCTest
@MainActor
final class PersonalDictionaryEntryServiceTests: XCTestCase {
private var suiteName: String!
private var defaults: UserDefaults!
private var store: AppGroupStore!
override func setUp() {
super.setUp()
suiteName = "group.com.osgkeyboard.shared.tests.entry-service.\(UUID().uuidString)"
defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
store = AppGroupStore(defaults: defaults)
}
override func tearDown() {
defaults.removePersistentDomain(forName: suiteName)
super.tearDown()
}
func testSuggestedTermGeneratesAliasesAndPreservesUsageCount() async throws {
var pushedDictionaries: [PersonalDictionary] = []
let service = PersonalDictionaryEntryService(
store: store,
aliasGeneration: { _ in ["洛基", "肉鸡"] },
cloudPush: { pushedDictionaries.append($0) }
)
let saved = try XCTUnwrap(
service.saveEntry(term: "Rocky", source: .history, minimumUsageCount: 3)
)
XCTAssertTrue(saved.shouldGenerateAliases)
XCTAssertEqual(saved.dictionary.entries.first?.source, .history)
XCTAssertEqual(saved.dictionary.entries.first?.usageCount, 3)
XCTAssertTrue(saved.dictionary.entries.first?.aliases.isEmpty == true)
let finished = await service.finishSaving(saved)
XCTAssertEqual(Set(finished.entries.first?.aliases ?? []), Set(["洛基", "肉鸡"]))
XCTAssertEqual(finished.entries.first?.usageCount, 3)
XCTAssertEqual(pushedDictionaries.count, 2)
}
func testAliasCompletionPreservesChangesMadeWhileGenerating() async throws {
let service = PersonalDictionaryEntryService(
store: store,
aliasGeneration: { _ in ["swift u i"] },
cloudPush: { _ in }
)
let saved = try XCTUnwrap(service.saveEntry(term: "SwiftUI", source: .manual))
store.updatePersonalDictionary { dictionary -> UUID? in
guard let concurrent = dictionary.upsertManual(term: "Cursor") else { return nil }
dictionary.version += 1
return concurrent.id
}
let finished = await service.finishSaving(saved)
XCTAssertEqual(Set(finished.entries.map(\.term)), Set(["SwiftUI", "Cursor"]))
XCTAssertEqual(
finished.entry(matchingTerm: "SwiftUI")?.aliases,
["swift u i"]
)
}
func testAliasCompletionDoesNotRestoreDeletedEntry() async throws {
let service = PersonalDictionaryEntryService(
store: store,
aliasGeneration: { _ in ["late alias"] },
cloudPush: { _ in }
)
let saved = try XCTUnwrap(service.saveEntry(term: "Deleted", source: .manual))
store.deletePersonalDictionaryEntry(id: saved.entryID)
let finished = await service.finishSaving(saved)
XCTAssertNil(finished.entries.first(where: { $0.id == saved.entryID }))
XCTAssertNotNil(finished.deletedEntryIDs[saved.entryID])
}
func testAliasCompletionIgnoresEntryRenamedAfterSave() async throws {
let service = PersonalDictionaryEntryService(
store: store,
aliasGeneration: { _ in ["stale alias"] },
cloudPush: { _ in }
)
let saved = try XCTUnwrap(service.saveEntry(term: "Before", source: .manual))
store.updatePersonalDictionary { dictionary -> UUID? in
guard let renamed = dictionary.upsertManual(
term: "After",
existingID: saved.entryID
) else {
return nil
}
dictionary.version += 1
return renamed.id
}
let finished = await service.finishSaving(saved)
XCTAssertEqual(finished.entries.first?.term, "After")
XCTAssertTrue(finished.entries.first?.aliases.isEmpty == true)
}
func testAliasGenerationFailureKeepsSavedEntry() async throws {
var pushCount = 0
let service = PersonalDictionaryEntryService(
store: store,
aliasGeneration: { _ in [] },
cloudPush: { _ in pushCount += 1 }
)
let saved = try XCTUnwrap(service.saveEntry(term: "Durable", source: .manual))
let finished = await service.finishSaving(saved)
XCTAssertEqual(finished.entries.first?.term, "Durable")
XCTAssertTrue(finished.entries.first?.aliases.isEmpty == true)
XCTAssertEqual(pushCount, 1)
}
}
@@ -1,7 +1,7 @@
// PolishStyleLearningServiceTests.swift
// OSGKeyboard · Tests
//
// Verifies corpus eligibility, the 5,000-character gate, and that style
// Verifies corpus eligibility, the 2,500-character gate, and that style
// generation receives both paired examples and the prompts that produced them.
@testable import OSGKeyboardShared
@@ -54,7 +54,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
XCTAssertEqual(corpus.examples.count, 1)
XCTAssertEqual(corpus.examples.first?.polishStyleID, "builtin.light")
XCTAssertEqual(corpus.effectiveCharacterCount, 7)
XCTAssertEqual(corpus.remainingCharacterCount, 4_993)
XCTAssertEqual(corpus.remainingCharacterCount, 2_493)
XCTAssertFalse(corpus.isReady)
}
@@ -74,8 +74,8 @@ final class PolishStyleLearningServiceTests: XCTestCase {
XCTAssertEqual(corpus.effectiveCharacterCount, 7)
}
func testCorpusUnlocksAtFiveThousandEffectiveCharacters() {
let text = String(repeating: "", count: 5_000)
func testCorpusUnlocksAtTwoThousandFiveHundredEffectiveCharacters() {
let text = String(repeating: "", count: 2_500)
let corpus = PolishStyleLearningCorpusBuilder.build(
from: [
SpeechHistoryEntry(
@@ -86,7 +86,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
]
)
XCTAssertEqual(corpus.effectiveCharacterCount, 5_000)
XCTAssertEqual(corpus.effectiveCharacterCount, 2_500)
XCTAssertEqual(corpus.remainingCharacterCount, 0)
XCTAssertTrue(corpus.isReady)
}
@@ -108,7 +108,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
store.setPolishStyleCatalog(catalog)
store.setActivePolishStyleId(activeStyle.id)
let source = String(repeating: "测试语料", count: 1_250)
let source = String(repeating: "测试语料", count: 625)
let corpus = PolishStyleLearningCorpus(
examples: [
PolishStyleLearningExample(
@@ -120,7 +120,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
createdAt: Date()
)
],
effectiveCharacterCount: 5_000
effectiveCharacterCount: 2_500
)
let client = StyleLearningCapturingClient(
response: ##"{"name":"","prompt":"# \n自然直接\n# \n不改变原意\n# \n输入 ","allowsAddedEmoji":false}"##
@@ -156,7 +156,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
createdAt: Date()
)
],
effectiveCharacterCount: 5_000
effectiveCharacterCount: 2_500
)
let service = PolishStyleLearningService(
store: store,
@@ -169,7 +169,7 @@ final class PolishStyleLearningServiceTests: XCTestCase {
} catch let error as PolishStyleLearningError {
XCTAssertEqual(
error,
.insufficientCorpus(required: 5_000, actual: 5)
.insufficientCorpus(required: 2_500, actual: 5)
)
} catch {
XCTFail("Unexpected error: \(error)")