Files
OSGKeyboard/OSGKeyboardTests/ClipboardPreparingPolicyTests.swift
T
Rocky 5d7fcdf24e feat(keyboard): harden polish/clipboard guards and ship 1.6.6 (build 59)
Keep marketing version at 1.6.6 and bump build to 59. Strengthen never-answer
polish safeguards, clipboard reply-intent continuity, voice undo UX, device
UITests harness, eval fixtures, and What's New assets.
2026-08-09 12:45:33 +08:00

360 lines
12 KiB
Swift

// ClipboardPreparingPolicyTests.swift
// OSGKeyboardTests
//
// Decision-matrix coverage for clipboard prepare stuck / double-start bugs.
import XCTest
@testable import OSGKeyboardShared
final class ClipboardPreparingPolicyTests: XCTestCase {
// MARK: - Restore (paste-alert recreate)
func testRestoreAwaitExistingWhenStartAlreadyIssued() {
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: true,
phase: .idle
),
.awaitExistingStart
)
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: true,
phase: .error
),
.awaitExistingStart
)
}
func testRestoreResumesIntentWhenNoStartHasBeenIssued() {
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: false,
phase: .idle
),
.resumeIntent,
"Cold-start return must resume the same intent automatically"
)
}
func testRestoreRefreshOnlyWhenAlreadyLive() {
for phase: ClipboardPreparingPhase in [
.requestingPermissions, .recording, .processing
] {
XCTAssertEqual(
ClipboardPreparingPolicy.restoreAction(
hasStartIssued: true,
phase: phase
),
.refreshOnly
)
}
}
/// Exact device bug: Allow Paste recreates keyboard while claim exists.
func testPasteAlertReopenMustNotPreferVoiceOnlyWhenClaimed() {
let action = ClipboardPreparingPolicy.restoreAction(
hasStartIssued: true,
phase: .idle
)
XCTAssertNotEqual(action, .resumeIntent)
XCTAssertEqual(action, .awaitExistingStart)
}
func testHostGateAllowsAutoRecordAfterWarmup() {
XCTAssertEqual(
ClipboardPreparingPolicy.hostGateAction(micPressAction: .startRecording),
.startRecordingNow
)
XCTAssertEqual(
ClipboardPreparingPolicy.hostGateAction(micPressAction: .openHostColdStart),
.openHostColdStart
)
XCTAssertEqual(
ClipboardPreparingPolicy.hostGateAction(
micPressAction: .waitForHostReady(recordWhenReady: true)
),
.waitForHost,
"Clipboard intent must wait and auto-resume without a second long-press"
)
XCTAssertEqual(
ClipboardPreparingPolicy.hostGateAction(micPressAction: .ignore),
.ignore
)
}
func testMicChromeGreyWhilePreparingBlueOnlyAfterConfirm() {
XCTAssertEqual(
ClipboardPreparingPolicy.micChrome(
isClipboardUtterance: true,
phase: .requestingPermissions,
awaitingHostConfirm: true
),
.preparingCancelable
)
XCTAssertEqual(
ClipboardPreparingPolicy.micChrome(
isClipboardUtterance: true,
phase: .recording,
awaitingHostConfirm: false
),
.recordingBlue
)
XCTAssertEqual(
ClipboardPreparingPolicy.micChrome(
isClipboardUtterance: false,
phase: .recording,
awaitingHostConfirm: false
),
.none
)
XCTAssertEqual(
ClipboardPreparingPolicy.micChrome(
isClipboardUtterance: true,
phase: .processing,
awaitingHostConfirm: false
),
.preparingCancelable,
"Post-record processing must still offer a cancel exit"
)
// Paste-acquire (idle + active flag) must not go blue.
XCTAssertEqual(
ClipboardPreparingPolicy.micChrome(
isClipboardUtterance: true,
phase: .idle,
awaitingHostConfirm: false
),
.none
)
}
// MARK: - Stop while preparing
func testTapDuringPreparingAbortsInsteadOfWaitingForever() {
XCTAssertEqual(
ClipboardPreparingPolicy.stopWhilePreparing(awaitingHostConfirm: true),
.abortPreparing
)
XCTAssertEqual(
ClipboardPreparingPolicy.stopWhilePreparing(awaitingHostConfirm: false),
.requestStop
)
}
// MARK: - Recover while preparing
func testRecoverConfirmsMatchingRecordingUtterance() {
let id = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: true,
currentUtteranceId: id,
hostBusyUtteranceId: id,
hostReason: .recording,
hasTerminalFailureForCurrent: false
),
.confirmRecording
)
}
func testRecoverAdoptsSiblingOnDoubleStart() {
let ours = UUID()
let sibling = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: true,
currentUtteranceId: ours,
hostBusyUtteranceId: sibling,
hostReason: .recording,
hasTerminalFailureForCurrent: false
),
.adoptSibling(sibling)
)
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: true,
currentUtteranceId: ours,
hostBusyUtteranceId: sibling,
hostReason: .processing,
hasTerminalFailureForCurrent: false
),
.adoptSibling(sibling)
)
}
func testRecoverAbortsOnHostTerminalFailure() {
let id = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: true,
currentUtteranceId: id,
hostBusyUtteranceId: nil,
hostReason: nil,
hasTerminalFailureForCurrent: true
),
.abortForHostFailure
)
}
func testRecoverDoesNothingWhenNotAwaiting() {
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: false,
currentUtteranceId: UUID(),
hostBusyUtteranceId: UUID(),
hostReason: .recording,
hasTerminalFailureForCurrent: true
),
.none
)
}
// MARK: - Ensure single startRecording
func testEnsureStartSkipsWhenAlreadyInFlight() {
let id = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: id,
isFlowRecording: true,
currentUtteranceId: id,
hostBusyUtteranceId: nil,
hostReason: nil,
hostReadyWithSession: true
),
.alreadyInFlight
)
}
func testEnsureStartAdoptsHostBusyInsteadOfSecondWrite() {
let issued = UUID()
let busy = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: issued,
isFlowRecording: false,
currentUtteranceId: issued,
hostBusyUtteranceId: busy,
hostReason: .recording,
hostReadyWithSession: true
),
.adoptBusy(busy, .recording)
)
}
func testEnsureStartWritesOnceWhenHostReady() {
let id = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: id,
isFlowRecording: false,
currentUtteranceId: id,
hostBusyUtteranceId: nil,
hostReason: nil,
hostReadyWithSession: true
),
.writeStart(id)
)
}
func testEnsureStartWaitsWhenHostNotReady() {
let id = UUID()
XCTAssertEqual(
ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: id,
isFlowRecording: false,
currentUtteranceId: id,
hostBusyUtteranceId: nil,
hostReason: nil,
hostReadyWithSession: false
),
.waitForHost
)
}
/// Simulate paste-alert reopen loop: claim → restore → ensure must not
/// produce two writeStart for the same issued id across polls.
func testDoubleStartStressMatrixTwentyRounds() {
for round in 1...20 {
let claim = UUID()
// First long-press claims.
XCTAssertTrue(claim.uuidString.isEmpty == false)
// Restore after Allow Paste / cold-start:
// - with claim → awaitExistingStart
// - without claim → resume the same intent automatically
let restoreClaimed = ClipboardPreparingPolicy.restoreAction(
hasStartIssued: true,
phase: .idle
)
XCTAssertEqual(restoreClaimed, .awaitExistingStart, "round \(round)")
let restoreWarm = ClipboardPreparingPolicy.restoreAction(
hasStartIssued: false,
phase: .idle
)
XCTAssertEqual(restoreWarm, .resumeIntent, "round \(round)")
XCTAssertEqual(
ClipboardPreparingPolicy.hostGateAction(micPressAction: .openHostColdStart),
.openHostColdStart,
"round \(round)"
)
// First ensure writes once.
let first = ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: claim,
isFlowRecording: false,
currentUtteranceId: claim,
hostBusyUtteranceId: nil,
hostReason: nil,
hostReadyWithSession: true
)
XCTAssertEqual(first, .writeStart(claim), "round \(round)")
// Second ensure (same process or restore) must not write again.
let second = ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: claim,
isFlowRecording: true,
currentUtteranceId: claim,
hostBusyUtteranceId: nil,
hostReason: nil,
hostReadyWithSession: true
)
XCTAssertEqual(second, .alreadyInFlight, "round \(round)")
// Sibling double-start on host → adopt, never another write.
let sibling = UUID()
let adopt = ClipboardPreparingPolicy.ensureStartAction(
issuedUtteranceId: claim,
isFlowRecording: false,
currentUtteranceId: claim,
hostBusyUtteranceId: sibling,
hostReason: .recording,
hostReadyWithSession: true
)
XCTAssertEqual(adopt, .adoptBusy(sibling, .recording), "round \(round)")
// Mic-not-ready failure while preparing → abort.
XCTAssertEqual(
ClipboardPreparingPolicy.recoverWhilePreparing(
awaitingHostConfirm: true,
currentUtteranceId: claim,
hostBusyUtteranceId: nil,
hostReason: nil,
hasTerminalFailureForCurrent: true
),
.abortForHostFailure,
"round \(round)"
)
// User tap while preparing cancels.
XCTAssertEqual(
ClipboardPreparingPolicy.stopWhilePreparing(awaitingHostConfirm: true),
.abortPreparing,
"round \(round)"
)
}
}
}