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
@@ -422,6 +422,46 @@ final class AccountCenterViewModelTests: XCTestCase {
XCTAssertEqual(deleteCount, 1)
}
@MainActor
func testExpiredSessionEventClearsSignedInAccountState() async {
let account = AccountSession(
accountID: UUID(),
createdAtEpochSeconds: 1_700_000_000
)
let service = AccountServiceSpy(
restoredSession: account,
snapshot: makeSnapshot(account: account)
)
let eventSource = AccountSessionEventSourceStub()
var signedOutCallbackCount = 0
let coordinator = AccountSessionCoordinator(
dependencies: AccountDependencies(
sessionService: service,
sessionEventSource: eventSource,
centerService: service
),
pendingReferralStore: InMemoryPendingReferralStore(),
onAccountSignedOut: {
signedOutCallbackCount += 1
}
)
await coordinator.restoreIfNeeded()
XCTAssertTrue(coordinator.isSignedIn)
eventSource.expire()
for _ in 0..<100 where coordinator.isSignedIn {
await Task.yield()
}
XCTAssertEqual(coordinator.sessionPhase, .signedOut)
XCTAssertEqual(coordinator.snapshotPhase, .idle)
XCTAssertEqual(coordinator.operationErrorKey, "account.error.sessionExpired")
XCTAssertEqual(coordinator.creditPurchases.catalogPhase, .idle)
XCTAssertEqual(signedOutCallbackCount, 1)
let managedGatewayClearCount = await service.managedGatewayClearCount()
XCTAssertEqual(managedGatewayClearCount, 1)
}
private func makeReferral(status: AccountReferralStatus) -> AccountReferral {
AccountReferral(
id: UUID(),
@@ -469,6 +509,24 @@ private final class MutableAccountClock {
}
}
@MainActor
private final class AccountSessionEventSourceStub: AccountSessionEventSourcing {
private let stream: AsyncStream<AccountSessionEvent>
private let continuation: AsyncStream<AccountSessionEvent>.Continuation
init() {
(stream, continuation) = AsyncStream.makeStream()
}
func events() async -> AsyncStream<AccountSessionEvent> {
stream
}
func expire() {
continuation.yield(.expired)
}
}
private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing {
private let restored: AccountSession?
private let centerSnapshot: AccountCenterSnapshot?
@@ -482,6 +540,7 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
private var logoutCount = 0
private var accountDeleteCount = 0
private var sessionRestoreCount = 0
private var gatewayClearCount = 0
private var receivedCachedSnapshot: AccountCenterSnapshot?
init(
@@ -523,6 +582,10 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
accountDeleteCount += 1
}
func clearManagedGateway() async {
gatewayClearCount += 1
}
func loadAccountCenter() async throws -> AccountCenterSnapshot {
try await loadAccountCenter(cachedSnapshot: nil)
}
@@ -575,6 +638,10 @@ private actor AccountServiceSpy: AccountSessionServicing, AccountCenterServicing
func deleteCount() -> Int {
accountDeleteCount
}
func managedGatewayClearCount() -> Int {
gatewayClearCount
}
}
private enum AccountServiceSpyError: Error, Sendable {