fix: el tap del microfono seguia aislado al main actor (2.0.3 build 68)
El fix anterior arreglo los permisos —el crash se movio de start a beginSession, y de TCC a AVAudioNodeTap::CheckEmitBuffer— pero el tap seguia petando. Mi error: quitar el acceso a self del closure no elimina la inferencia de aislamiento. La inferencia viene de DONDE se escribe el closure, no de lo que captura. Escrito dentro de un metodo @MainActor, seguia siendo main-actor isolated, y el tap se dispara desde el hilo de audio en tiempo real. Ahora el handler se construye en makeTapHandler, que es nonisolated, y se pasa a installTap. La estructura se auto-verifica: quitarle nonisolated a makeTapHandler no compila, porque installTap es nonisolated y no puede llamar a un metodo aislado. El bug pasa de crash en produccion a error de compilacion. Tests: testTapHandlerRunsOffTheMainThread invoca el handler desde una cola de fondo, que es exactamente la condicion que trapeaba. No hace falta microfono; el intento anterior con AVAudioEngine se saltaba siempre porque el simulador no tiene entrada de audio utilizable. Barrido del mismo patron en el resto de servicios @MainActor: CalendarService y NotificationService usan las variantes async/await, que estan anotadas y no tienen closures. SpeechDictationService era el unico sitio. Lanes feedback y crashlog para leer los reportes de TestFlight desde la API: spaceship apunta a v1/betaFeedbacks, que Apple ya retiro; el endpoint vivo es v1/apps/<id>/betaFeedbackCrashSubmissions y el log viene inline en logText. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import XCTest
|
||||
import AVFoundation
|
||||
import Speech
|
||||
@testable import MealMood
|
||||
|
||||
/// Smoke cover for the dictation entry point.
|
||||
@@ -32,6 +34,36 @@ final class SpeechDictationServiceTests: XCTestCase {
|
||||
"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")
|
||||
|
||||
Reference in New Issue
Block a user