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:
@@ -0,0 +1,252 @@
|
||||
// FlowSessionManagerAnalyticsTests.swift
|
||||
// OSGKeyboardTests
|
||||
//
|
||||
// Pure coverage for the feature mapping and utterance-bound operation registry
|
||||
// used by FlowSessionManager's asynchronous ASR/assistant pipeline.
|
||||
|
||||
import Foundation
|
||||
@testable import OSGKeyboard
|
||||
import OSGKeyboardShared
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class FlowSessionManagerAnalyticsTests: XCTestCase {
|
||||
func testEveryFlowEntryMapsToOneStableFeature() {
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .aiQuestion),
|
||||
.aiAssistant
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .clipboardTransform),
|
||||
.agent
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .customSkill),
|
||||
.agent
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .agentPlanning),
|
||||
.agent
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .dictationPolish),
|
||||
.polish
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .translation),
|
||||
.polish
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.feature(for: .editLastInput),
|
||||
.polish
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.recordingFeature(for: .dictation),
|
||||
.transcription
|
||||
)
|
||||
XCTAssertEqual(
|
||||
FlowAnalyticsFeatureMapping.recordingFeature(for: .aiQuestion),
|
||||
.aiAssistant
|
||||
)
|
||||
}
|
||||
|
||||
func testStreamingCancelledProducesOneCancelledTerminal() {
|
||||
let client = RecordingFlowAnalyticsClient()
|
||||
let registry = FlowAnalyticsOperationRegistry()
|
||||
let utteranceID = UUID()
|
||||
registry.start(
|
||||
utteranceID: utteranceID,
|
||||
feature: .transcription,
|
||||
executionMode: .managed,
|
||||
client: client
|
||||
)
|
||||
|
||||
registry.cancel(utteranceID: utteranceID)
|
||||
|
||||
XCTAssertEqual(
|
||||
client.events(),
|
||||
[
|
||||
.started(.transcription),
|
||||
.failed(.transcription, .cancelled)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testASRSucceedsButAssistantFailureDoesNotRecordTranscriptionSuccess() {
|
||||
let client = RecordingFlowAnalyticsClient()
|
||||
let registry = FlowAnalyticsOperationRegistry()
|
||||
let utteranceID = UUID()
|
||||
registry.start(
|
||||
utteranceID: utteranceID,
|
||||
feature: .aiAssistant,
|
||||
executionMode: .managed,
|
||||
client: client
|
||||
)
|
||||
|
||||
// ASR completion is intentionally not a terminal value event.
|
||||
registry.fail(utteranceID: utteranceID, category: .network)
|
||||
|
||||
XCTAssertEqual(
|
||||
client.events(),
|
||||
[
|
||||
.started(.aiAssistant),
|
||||
.failed(.aiAssistant, .network)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testOrdinaryDictationSuccessRecordsTranscription() {
|
||||
let client = RecordingFlowAnalyticsClient()
|
||||
let registry = FlowAnalyticsOperationRegistry()
|
||||
let utteranceID = UUID()
|
||||
registry.start(
|
||||
utteranceID: utteranceID,
|
||||
feature: FlowAnalyticsFeatureMapping.recordingFeature(for: .dictation),
|
||||
executionMode: .local,
|
||||
client: client
|
||||
)
|
||||
|
||||
registry.succeed(utteranceID: utteranceID)
|
||||
|
||||
XCTAssertEqual(
|
||||
client.events(),
|
||||
[
|
||||
.started(.transcription),
|
||||
.succeeded(.transcription)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testVoiceAssistantSuccessRecordsOneValueTask() {
|
||||
let client = RecordingFlowAnalyticsClient()
|
||||
let registry = FlowAnalyticsOperationRegistry()
|
||||
let utteranceID = UUID()
|
||||
registry.start(
|
||||
utteranceID: utteranceID,
|
||||
feature: FlowAnalyticsFeatureMapping.recordingFeature(for: .aiQuestion),
|
||||
executionMode: .byok,
|
||||
client: client
|
||||
)
|
||||
|
||||
registry.operation(for: utteranceID)?.succeed()
|
||||
registry.discard(utteranceID: utteranceID)
|
||||
|
||||
XCTAssertEqual(
|
||||
client.events(),
|
||||
[
|
||||
.started(.aiAssistant),
|
||||
.succeeded(.aiAssistant)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testRepeatedTerminalCallsStillRecordOnlyOnce() {
|
||||
let client = RecordingFlowAnalyticsClient()
|
||||
let registry = FlowAnalyticsOperationRegistry()
|
||||
let utteranceID = UUID()
|
||||
registry.start(
|
||||
utteranceID: utteranceID,
|
||||
feature: .aiAssistant,
|
||||
executionMode: .managed,
|
||||
client: client
|
||||
)
|
||||
|
||||
registry.succeed(utteranceID: utteranceID)
|
||||
registry.fail(utteranceID: utteranceID, category: .provider)
|
||||
registry.cancel(utteranceID: utteranceID)
|
||||
|
||||
XCTAssertEqual(
|
||||
client.events(),
|
||||
[
|
||||
.started(.aiAssistant),
|
||||
.succeeded(.aiAssistant)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class RecordingFlowAnalyticsClient:
|
||||
AnalyticsClient,
|
||||
@unchecked Sendable {
|
||||
enum Event: Equatable {
|
||||
case started(AnalyticsFeature)
|
||||
case succeeded(AnalyticsFeature)
|
||||
case failed(AnalyticsFeature, AnalyticsFailureCategory)
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var recordedEvents: [Event] = []
|
||||
|
||||
func events() -> [Event] {
|
||||
lock.withLock { recordedEvents }
|
||||
}
|
||||
|
||||
func startAIFeature(
|
||||
_ feature: AnalyticsFeature,
|
||||
executionMode: AnalyticsExecutionMode
|
||||
) -> any AnalyticsAIOperation {
|
||||
append(.started(feature))
|
||||
return RecordingFlowAnalyticsOperation(feature: feature) { [weak self] event in
|
||||
self?.append(event)
|
||||
}
|
||||
}
|
||||
|
||||
func recordSessionActivity() {}
|
||||
func recordKeyboardActivated() {}
|
||||
func recordPurchaseViewed() {}
|
||||
func recordPurchaseStarted() {}
|
||||
func recordPurchaseCancelled() {}
|
||||
func recordReferralShared() {}
|
||||
|
||||
func recordInviteOpened(
|
||||
acquisitionChannel: AnalyticsAcquisitionChannel,
|
||||
surface: AnalyticsSurface
|
||||
) {}
|
||||
|
||||
private func append(_ event: Event) {
|
||||
lock.withLock {
|
||||
recordedEvents.append(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class RecordingFlowAnalyticsOperation:
|
||||
AnalyticsAIOperation,
|
||||
@unchecked Sendable {
|
||||
private let feature: AnalyticsFeature
|
||||
private let onTerminal: @Sendable (RecordingFlowAnalyticsClient.Event) -> Void
|
||||
private let lock = NSLock()
|
||||
private var isFinished = false
|
||||
|
||||
init(
|
||||
feature: AnalyticsFeature,
|
||||
onTerminal: @escaping @Sendable (
|
||||
RecordingFlowAnalyticsClient.Event
|
||||
) -> Void
|
||||
) {
|
||||
self.feature = feature
|
||||
self.onTerminal = onTerminal
|
||||
}
|
||||
|
||||
func succeed() {
|
||||
finish(.succeeded(feature))
|
||||
}
|
||||
|
||||
func fail(category: AnalyticsFailureCategory) {
|
||||
finish(.failed(feature, category))
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
fail(category: .cancelled)
|
||||
}
|
||||
|
||||
private func finish(_ event: RecordingFlowAnalyticsClient.Event) {
|
||||
let shouldRecord = lock.withLock {
|
||||
guard !isFinished else { return false }
|
||||
isFinished = true
|
||||
return true
|
||||
}
|
||||
guard shouldRecord else { return }
|
||||
onTerminal(event)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user