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.
253 lines
8.0 KiB
Swift
253 lines
8.0 KiB
Swift
// FlowReliabilityTests.swift
|
|
// OSGKeyboardTests
|
|
//
|
|
// Regression coverage for durable cross-process delivery and bounded fallback.
|
|
|
|
import AVFoundation
|
|
import Foundation
|
|
@testable import OSGKeyboard
|
|
@testable import OSGKeyboardHostSupport
|
|
@testable import OSGKeyboardShared
|
|
import XCTest
|
|
|
|
final class FlowReliabilityTests: XCTestCase {
|
|
private var defaults: UserDefaults!
|
|
private var suiteName: String!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
suiteName = "group.com.osgkeyboard.tests.flow-reliability.\(UUID().uuidString)"
|
|
defaults = UserDefaults(suiteName: suiteName)!
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
}
|
|
|
|
override func tearDown() {
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
defaults = nil
|
|
suiteName = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
func testCommandJournalPreservesRapidStartAndStop() {
|
|
let sessionId = UUID()
|
|
let utteranceId = UUID()
|
|
let start = FlowCommand(
|
|
sessionId: sessionId,
|
|
utteranceId: utteranceId,
|
|
commandSeq: 100,
|
|
action: .startRecording,
|
|
localeId: "zh-Hans"
|
|
)
|
|
let stop = FlowCommand(
|
|
sessionId: sessionId,
|
|
utteranceId: utteranceId,
|
|
commandSeq: 101,
|
|
action: .stopRecording,
|
|
localeId: "zh-Hans"
|
|
)
|
|
|
|
FlowSessionBridge.writeCommand(start, defaults: defaults)
|
|
FlowSessionBridge.writeCommand(stop, defaults: defaults)
|
|
|
|
XCTAssertEqual(
|
|
FlowSessionBridge.commands(after: 0, defaults: defaults),
|
|
[start, stop]
|
|
)
|
|
}
|
|
|
|
func testAbortInvalidatesInFlightUtteranceStart() {
|
|
let utteranceId = UUID()
|
|
let token = FlowUtteranceStartToken(
|
|
generation: 10,
|
|
utteranceId: utteranceId
|
|
)
|
|
|
|
XCTAssertTrue(
|
|
FlowUtteranceLifecyclePolicy.canContinueStart(
|
|
token: token,
|
|
currentGeneration: 10,
|
|
currentUtteranceId: utteranceId,
|
|
terminalUtteranceIds: []
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
FlowUtteranceLifecyclePolicy.canContinueStart(
|
|
token: token,
|
|
currentGeneration: 11,
|
|
currentUtteranceId: nil,
|
|
terminalUtteranceIds: [utteranceId]
|
|
)
|
|
)
|
|
}
|
|
|
|
func testTerminalUtteranceCannotBeResurrectedByLateStart() {
|
|
let utteranceId = UUID()
|
|
let token = FlowUtteranceStartToken(
|
|
generation: 10,
|
|
utteranceId: utteranceId
|
|
)
|
|
|
|
XCTAssertFalse(
|
|
FlowUtteranceLifecyclePolicy.canContinueStart(
|
|
token: token,
|
|
currentGeneration: 10,
|
|
currentUtteranceId: utteranceId,
|
|
terminalUtteranceIds: [utteranceId]
|
|
)
|
|
)
|
|
}
|
|
|
|
func testTerminalResultCannotBeDowngradedByLatePartial() {
|
|
let sessionId = UUID()
|
|
let utteranceId = UUID()
|
|
let final = FlowResult(
|
|
sessionId: sessionId,
|
|
utteranceId: utteranceId,
|
|
commandSeq: 1,
|
|
status: .final,
|
|
text: "最终文本",
|
|
rawText: "原始文本",
|
|
revision: 10
|
|
)
|
|
let stalePartial = FlowResult(
|
|
sessionId: sessionId,
|
|
utteranceId: utteranceId,
|
|
commandSeq: 1,
|
|
status: .partial,
|
|
text: "迟到片段",
|
|
revision: 11
|
|
)
|
|
|
|
FlowSessionBridge.writeResult(final, defaults: defaults)
|
|
FlowSessionBridge.writeResult(stalePartial, defaults: defaults)
|
|
|
|
XCTAssertEqual(FlowSessionBridge.latestResult(defaults: defaults), final)
|
|
}
|
|
|
|
func testPendingUtteranceSurvivesCoordinatorRecreation() {
|
|
let utteranceId = UUID()
|
|
|
|
FlowSessionBridge.setPendingKeyboardUtteranceId(utteranceId, defaults: defaults)
|
|
|
|
XCTAssertEqual(
|
|
FlowSessionBridge.pendingKeyboardUtteranceId(defaults: defaults),
|
|
utteranceId
|
|
)
|
|
}
|
|
|
|
func testResultMatcherRejectsPreviousHostGeneration() {
|
|
let sessionId = UUID()
|
|
let utteranceId = UUID()
|
|
let result = FlowResult(
|
|
sessionId: sessionId,
|
|
utteranceId: utteranceId,
|
|
commandSeq: 1,
|
|
status: .final,
|
|
text: "旧结果",
|
|
hostGeneration: "old"
|
|
)
|
|
|
|
XCTAssertNil(
|
|
FlowKeyboardResultMatcher.matchingResult(
|
|
latest: result,
|
|
activeSessionId: sessionId,
|
|
currentUtteranceId: utteranceId,
|
|
currentHostGeneration: "new"
|
|
)
|
|
)
|
|
}
|
|
|
|
func testDynamicBudgetsBoundWorstCaseDelivery() {
|
|
XCTAssertEqual(FlowSessionKeys.polishTimeout(forCharacterCount: 80), 10)
|
|
XCTAssertEqual(FlowSessionKeys.polishTimeout(forCharacterCount: 300), 20)
|
|
XCTAssertEqual(FlowSessionKeys.polishTimeout(forCharacterCount: 900), 35)
|
|
XCTAssertLessThanOrEqual(FlowSessionKeys.keyboardResultTimeout(engineMode: "cloud"), 65)
|
|
}
|
|
|
|
func testHardTimeoutDoesNotWaitForNonCooperativeOperation() async {
|
|
let started = Date()
|
|
|
|
do {
|
|
_ = try await HardTimeout.run(seconds: 0.05) {
|
|
await withCheckedContinuation { continuation in
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 0.4) {
|
|
continuation.resume(returning: "late")
|
|
}
|
|
}
|
|
}
|
|
XCTFail("Expected timeout")
|
|
} catch {
|
|
XCTAssertEqual(error as? HardTimeoutError, .timedOut)
|
|
}
|
|
|
|
XCTAssertLessThan(Date().timeIntervalSince(started), 0.2)
|
|
}
|
|
|
|
func testHardTimeoutPreservesCallerCancellation() async {
|
|
let task = Task {
|
|
try await HardTimeout.run(seconds: 5) {
|
|
try await Task.sleep(nanoseconds: 5_000_000_000)
|
|
return "late"
|
|
}
|
|
}
|
|
await Task.yield()
|
|
task.cancel()
|
|
|
|
do {
|
|
_ = try await task.value
|
|
XCTFail("Expected caller cancellation")
|
|
} catch {
|
|
XCTAssertTrue(error is CancellationError, "Unexpected error: \(error)")
|
|
XCTAssertNil(error as? HardTimeoutError)
|
|
}
|
|
}
|
|
|
|
func testAudioRoutePolicyRebuildsHFPTransitions() {
|
|
XCTAssertTrue(
|
|
FlowAudioRouteRecoveryPolicy.shouldRebuild(
|
|
reasonRaw: AVAudioSession.RouteChangeReason.newDeviceAvailable.rawValue,
|
|
formatIsStable: false,
|
|
engineIsRunning: true
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
FlowAudioRouteRecoveryPolicy.shouldRebuild(
|
|
reasonRaw: AVAudioSession.RouteChangeReason.routeConfigurationChange.rawValue,
|
|
formatIsStable: true,
|
|
engineIsRunning: true
|
|
)
|
|
)
|
|
XCTAssertTrue(
|
|
FlowAudioRouteRecoveryPolicy.shouldRebuild(
|
|
reasonRaw: AVAudioSession.RouteChangeReason.categoryChange.rawValue,
|
|
formatIsStable: false,
|
|
engineIsRunning: true
|
|
)
|
|
)
|
|
XCTAssertFalse(
|
|
FlowAudioRouteRecoveryPolicy.shouldRebuild(
|
|
reasonRaw: AVAudioSession.RouteChangeReason.categoryChange.rawValue,
|
|
formatIsStable: true,
|
|
engineIsRunning: true
|
|
)
|
|
)
|
|
XCTAssertTrue(
|
|
FlowAudioRouteRecoveryPolicy.shouldRebuild(
|
|
reasonRaw: AVAudioSession.RouteChangeReason.routeConfigurationChange.rawValue,
|
|
formatIsStable: true,
|
|
engineIsRunning: false
|
|
)
|
|
)
|
|
}
|
|
|
|
func testCaptureVoiceProcessingUsesSpeechOrientedSessionMode() {
|
|
XCTAssertEqual(FlowCaptureVoiceProcessing.captureMode, .voiceChat)
|
|
XCTAssertTrue(
|
|
FlowCaptureVoiceProcessing.captureOptions.contains(.allowBluetoothHFP)
|
|
)
|
|
XCTAssertTrue(
|
|
FlowCaptureVoiceProcessing.captureOptions.contains(.defaultToSpeaker)
|
|
)
|
|
}
|
|
}
|