// ClipboardCommandResumeTests.swift // OSGKeyboardTests import XCTest @testable import OSGKeyboardShared final class ClipboardCommandResumeTests: XCTestCase { private var defaults: UserDefaults! private var suiteName: String! override func setUp() { super.setUp() suiteName = "ClipboardCommandResumeTests.\(UUID().uuidString)" defaults = UserDefaults(suiteName: suiteName) } override func tearDown() { if let suiteName { defaults?.removePersistentDomain(forName: suiteName) } defaults = nil suiteName = nil super.tearDown() } func testMarkPreferVoiceSurvivesUntilCleared() { XCTAssertFalse(ClipboardCommandResume.shouldPreferVoice(defaults: defaults)) ClipboardCommandResume.markPreferVoice(defaults: defaults) XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults)) ClipboardCommandResume.clear(defaults: defaults) XCTAssertFalse(ClipboardCommandResume.shouldPreferVoice(defaults: defaults)) } func testStoreSnapshotRoundTrip() { ClipboardCommandResume.storeSnapshot("周末有空一起吃饭吗?", defaults: defaults) XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults)) XCTAssertEqual( ClipboardCommandResume.pendingSnapshot(defaults: defaults), "周末有空一起吃饭吗?" ) } /// Simulates extension jetsam: writer process flushes, reader process is new. func testStickySurvivesNewUserDefaultsInstanceAfterSynchronize() { let text = "是AI语音输入法,更是好输入法。It is an AI voice input method." ClipboardCommandResume.markPreferVoice(defaults: defaults) ClipboardCommandResume.storeSnapshot(text, defaults: defaults) // New UserDefaults handle on the same suite ≈ new extension process. let reopened = UserDefaults(suiteName: suiteName!) XCTAssertNotNil(reopened) guard let reopened else { return } XCTAssertTrue( ClipboardCommandResume.shouldPreferVoice(defaults: reopened), "Recreated process must still prefer voice after paste-alert jetsam" ) XCTAssertEqual( ClipboardCommandResume.pendingSnapshot(defaults: reopened), text ) // And the open-surface policy must then force voice over default typing. XCTAssertEqual( KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: reopened), preferred: .typing ), .voice ) } func testOpenSurfacePolicyForcesVoiceWhenStickyEvenIfDefaultTyping() { let resolved = KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: true, preferred: .typing ) XCTAssertEqual(resolved, .voice) } func testOpenSurfacePolicyHonorsDefaultTypingWhenNoSticky() { let resolved = KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: false, preferred: .typing ) XCTAssertEqual(resolved, .typing) } func testOpenSurfacePolicyClipboardActiveBeatsTypingDefault() { let resolved = KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: true, stickyPreferVoice: false, preferred: .typing ) XCTAssertEqual(resolved, .voice) } func testPasteAlertReopenDecisionMatrix() { // Exact bug from device: default typing + sticky after Allow Paste. XCTAssertEqual( KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: true, preferred: .typing ), .voice, "Allow Paste reopen must not land on default Chinese typing grid" ) // Recording lock also forces voice. XCTAssertEqual( KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: true, clipboardCommandActive: false, stickyPreferVoice: false, preferred: .typing ), .voice ) } func testMarkStartIssuedBlocksDuplicateRoundClaim() { let id = UUID() XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults)) ClipboardCommandResume.markStartIssued(id, defaults: defaults) XCTAssertTrue(ClipboardCommandResume.hasStartIssued(defaults: defaults)) XCTAssertEqual(ClipboardCommandResume.startIssuedUtteranceId(defaults: defaults), id) XCTAssertTrue(ClipboardCommandResume.shouldPreferVoice(defaults: defaults)) let reopened = UserDefaults(suiteName: suiteName!) XCTAssertEqual( ClipboardCommandResume.startIssuedUtteranceId(defaults: reopened), id, "Recreated extension must see the already-issued start and not send another" ) ClipboardCommandResume.clear(defaults: defaults) XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults)) XCTAssertNil(ClipboardCommandResume.startIssuedUtteranceId(defaults: reopened)) } func testClaimThenClearSurvivesTwentyAlternatingRounds() { for round in 1...20 { let id = UUID() ClipboardCommandResume.storeSnapshot("材料-\(round)", defaults: defaults) ClipboardCommandResume.markStartIssued(id, defaults: defaults) let reader = UserDefaults(suiteName: suiteName!) XCTAssertEqual( ClipboardCommandResume.startIssuedUtteranceId(defaults: reader), id, "round \(round) claim must survive new defaults handle" ) XCTAssertEqual( KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: reader), preferred: .typing ), .voice, "round \(round) must stay on voice after paste-alert recreate" ) XCTAssertEqual( ClipboardPreparingPolicy.restoreAction( hasStartIssued: ClipboardCommandResume.hasStartIssued(defaults: reader), phase: .idle ), .awaitExistingStart, "round \(round) must not pressBegan again" ) ClipboardCommandResume.clear(defaults: defaults) XCTAssertFalse( ClipboardCommandResume.hasStartIssued(defaults: reader), "round \(round) clear must drop claim for next independent long-press" ) } } func testPreparingTimeoutConstantIsPositive() { XCTAssertGreaterThan(ClipboardCommandResume.preparingTimeout, 1) XCTAssertLessThanOrEqual(ClipboardCommandResume.preparingTimeout, 15) } func testColdStartStickyRestoreIsPreferVoiceOnlyWithoutClaim() { ClipboardCommandResume.storeSnapshot("冻结材料", defaults: defaults) XCTAssertFalse(ClipboardCommandResume.hasStartIssued(defaults: defaults)) XCTAssertEqual( ClipboardPreparingPolicy.restoreAction( hasStartIssued: false, phase: .idle ), .preferVoiceOnly ) XCTAssertEqual( KeyboardOpenSurfacePolicy.resolve( locksTypingSurface: false, clipboardCommandActive: false, stickyPreferVoice: ClipboardCommandResume.shouldPreferVoice(defaults: defaults), preferred: .typing ), .voice, "Default typing keyboard must still open voice after cold-start sticky" ) } }