import XCTest import AVFoundation import Speech @testable import MealMood /// Smoke cover for the dictation entry point. /// /// **This does not reproduce the crash it was written for.** The production /// crash (`dispatch_assert_queue` inside the `requestAuthorization` handler) /// needs TCC to deliver the callback asynchronously on a background queue, which /// is what happens on device when the permission prompt is answered. The /// simulator delivers it synchronously on the calling thread instead, so the /// isolation check passes and the pre-fix code runs clean here — verified by /// reverting the fix and watching these tests still pass. /// /// What it does cover: `start` settles instead of hanging, and does not report /// recording when permission is unavailable. Verifying the crash itself needs a /// real device with the speech and microphone permissions reset. @MainActor final class SpeechDictationServiceTests: XCTestCase { func testStartSettlesWithoutRecordingWhenPermissionIsUnavailable() async throws { let service = SpeechDictationService() XCTAssertEqual(service.state, .idle) service.start(localeIdentifier: "en-US") let deadline = Date().addingTimeInterval(10) while service.state == .idle && Date() < deadline { try await Task.sleep(nanoseconds: 100_000_000) } XCTAssertNotEqual(service.state, .recording, "No permission is granted in this environment, so it must not record") } /// This one *does* reproduce the crash class, without needing a microphone. /// /// In 2.0.3 build 68 the app still died at the tap: `AVAudioNodeTapBlock` is /// pre-concurrency, so a closure literal written inside a `@MainActor` method /// was inferred main-actor isolated, and the tap fires from the realtime /// audio thread — Swift 6's isolation check tripped `dispatch_assert_queue`. /// Dropping the `self` capture had not been enough; isolation is inferred /// from where the closure is written. /// /// Calling the handler off the main thread is the exact condition that /// trapped, so this fails loudly if the isolation ever creeps back. func testTapHandlerRunsOffTheMainThread() async throws { let request = SFSpeechAudioBufferRecognitionRequest() let handler = SpeechDictationService.makeTapHandler(feeding: request) let format = try XCTUnwrap(AVAudioFormat(standardFormatWithSampleRate: 44_100, channels: 1)) let buffer = try XCTUnwrap(AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)) buffer.frameLength = 1024 let ran = expectation(description: "tap handler ran on a background queue") DispatchQueue.global(qos: .userInitiated).async { XCTAssertFalse(Thread.isMainThread) handler(buffer, AVAudioTime(sampleTime: 0, atRate: 44_100)) ran.fulfill() } await fulfillment(of: [ran], timeout: 5) request.endAudio() } func testStartTwiceDoesNotBlowUp() { let service = SpeechDictationService() service.start(localeIdentifier: "en-US") service.start(localeIdentifier: "en-US") XCTAssertNotEqual(service.state, .recording) } }