Running the SpikerSoft iOS unit-test scheme in the simulator intermittently segfaults ~1.5 s after launch:
`EXC_BAD_ACCESS (SIGSEGV)`, `KERN_INVALID_ADDRESS` in `objc_retain`, main thread
All crashing frames are inside Apple's TextToSpeech.framework (block dispatched to the main queue); the XCTWaiter frames below are just XCTest pumping the run loop
No app/test frames in the stack; the current unit tests are pure logic tests that never speak
Cause
The unit tests run inside the SpikerSoft host app, so `AppDependencies.shared` is built at launch and `AudioCueService.init` eagerly creates its `AVSpeechSynthesizer` + sets the delegate. Merely instantiating the synthesizer spins up the simulator's TTS stack (MacinTalk / SpeechDictionary / MauiTTSSupport, five AXSpeech threads), whose async voice-asset setup intermittently retains a dead pointer — a known-flaky area of the simulator runtime. Our ownership is correct (single long-lived synthesizer, delegate alive); the crash is in framework init, not our code.
Impact
Flaky local test runs, and an intermittent CI flake for the iOS coverage lane being added on `feat/test-coverage-sonar` (#1000).
Fix
Create the synthesizer lazily on first `play(_:)` in `AudioCueService` so unit-test runs (which never announce) never touch TextToSpeech. App behavior unchanged — first real cue constructs it on the main actor.
## Symptom
Running the SpikerSoft iOS unit-test scheme in the simulator intermittently segfaults ~1.5 s after launch:
- \`EXC_BAD_ACCESS (SIGSEGV)\`, \`KERN_INVALID_ADDRESS\` in \`objc_retain\`, main thread
- All crashing frames are inside Apple's **TextToSpeech.framework** (block dispatched to the main queue); the XCTWaiter frames below are just XCTest pumping the run loop
- No app/test frames in the stack; the current unit tests are pure logic tests that never speak
## Cause
The unit tests run inside the SpikerSoft host app, so \`AppDependencies.shared\` is built at launch and \`AudioCueService.init\` eagerly creates its \`AVSpeechSynthesizer\` + sets the delegate. Merely instantiating the synthesizer spins up the simulator's TTS stack (MacinTalk / SpeechDictionary / MauiTTSSupport, five AXSpeech threads), whose async voice-asset setup intermittently retains a dead pointer — a known-flaky area of the simulator runtime. Our ownership is correct (single long-lived synthesizer, delegate alive); the crash is in framework init, not our code.
## Impact
Flaky local test runs, and an intermittent CI flake for the iOS coverage lane being added on \`feat/test-coverage-sonar\` (#1000).
## Fix
Create the synthesizer lazily on first \`play(_:)\` in \`AudioCueService\` so unit-test runs (which never announce) never touch TextToSpeech. App behavior unchanged — first real cue constructs it on the main actor.
I checked every remaining reference so the laziness actually holds on a test-host launch: the only touches are inside play(_:) (:52stopSpeaking, :56speak) and deactivateSession() (:94), and the latter short-circuits on guard sessionActive, … before evaluating synthesizer.isSpeaking — sessionActive is only set true by activateSession() from within play(_:). So a unit-test run that never announces never constructs the synthesizer and never spins up the simulator TTS stack. App behavior is unchanged: the first real cue builds it on the main actor.
Live: nothing runtime-observable — this is test-host/CI hygiene, so on-main code evidence is the whole bar. Corroborating: the iOS coverage lane it was blocking now runs clean — SonarQube shows project spikersoft-ios analyzed 2026-08-07T13:36:09+0000.
Verified complete 2026-08-07 — closing.
- **Code:** `spikersoft-ios@6926525` (PR #15, `fix/audiocue-lazy-synthesizer-1001`, commit `80ab510`) — `SpikerSoft/Core/Audio/AudioCueService.swift:27` is now
```swift
private lazy var synthesizer: AVSpeechSynthesizer = {
let synthesizer = AVSpeechSynthesizer()
synthesizer.delegate = self
return synthesizer
}()
```
I checked every remaining reference so the laziness actually holds on a test-host launch: the only touches are inside `play(_:)` (`:52` `stopSpeaking`, `:56` `speak`) and `deactivateSession()` (`:94`), and the latter short-circuits on `guard sessionActive, …` before evaluating `synthesizer.isSpeaking` — `sessionActive` is only set true by `activateSession()` from within `play(_:)`. So a unit-test run that never announces never constructs the synthesizer and never spins up the simulator TTS stack. App behavior is unchanged: the first real cue builds it on the main actor.
- **Live:** nothing runtime-observable — this is test-host/CI hygiene, so on-main code evidence is the whole bar. Corroborating: the iOS coverage lane it was blocking now runs clean — SonarQube shows project `spikersoft-ios` analyzed `2026-08-07T13:36:09+0000`.
- **Shipped by:** ios PR #15.
Not migrated: nothing left to do.
— Opus 5 Agent
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Symptom
Running the SpikerSoft iOS unit-test scheme in the simulator intermittently segfaults ~1.5 s after launch:
Cause
The unit tests run inside the SpikerSoft host app, so `AppDependencies.shared` is built at launch and `AudioCueService.init` eagerly creates its `AVSpeechSynthesizer` + sets the delegate. Merely instantiating the synthesizer spins up the simulator's TTS stack (MacinTalk / SpeechDictionary / MauiTTSSupport, five AXSpeech threads), whose async voice-asset setup intermittently retains a dead pointer — a known-flaky area of the simulator runtime. Our ownership is correct (single long-lived synthesizer, delegate alive); the crash is in framework init, not our code.
Impact
Flaky local test runs, and an intermittent CI flake for the iOS coverage lane being added on `feat/test-coverage-sonar` (#1000).
Fix
Create the synthesizer lazily on first `play(_:)` in `AudioCueService` so unit-test runs (which never announce) never touch TextToSpeech. App behavior unchanged — first real cue constructs it on the main actor.
Verified complete 2026-08-07 — closing.
spikersoft-ios@6926525(PR #15,fix/audiocue-lazy-synthesizer-1001, commit80ab510) —SpikerSoft/Core/Audio/AudioCueService.swift:27is nowplay(_:)(:52stopSpeaking,:56speak) anddeactivateSession()(:94), and the latter short-circuits onguard sessionActive, …before evaluatingsynthesizer.isSpeaking—sessionActiveis only set true byactivateSession()from withinplay(_:). So a unit-test run that never announces never constructs the synthesizer and never spins up the simulator TTS stack. App behavior is unchanged: the first real cue builds it on the main actor.spikersoft-iosanalyzed2026-08-07T13:36:09+0000.Not migrated: nothing left to do.
— Opus 5 Agent