498f407585
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
58 lines
2.3 KiB
Swift
58 lines
2.3 KiB
Swift
// MacAudioRecorderSnapshotStreamTests.swift
|
|
// OSGKeyboard · Mac tests
|
|
//
|
|
// Regression guard for the freeze that hit when the hold-to-talk key was
|
|
// released. `MacAudioRecorder` finished its snapshot continuation while holding
|
|
// a non-reentrant `NSLock`; `AsyncStream.Continuation.finish()` invokes
|
|
// `onTermination` synchronously on the calling thread, that handler re-took the
|
|
// same lock, and because the release path runs `stop()` on the main actor the
|
|
// whole app wedged.
|
|
|
|
@testable import OSGKeyboard
|
|
import XCTest
|
|
|
|
final class MacAudioRecorderSnapshotStreamTests: XCTestCase {
|
|
|
|
/// Installing a second stream finishes the first one. Run off-main and
|
|
/// bounded by a semaphore timeout so a reintroduced lock re-entry fails the
|
|
/// test instead of hanging the whole suite.
|
|
func testReplacingSnapshotStreamDoesNotDeadlock() {
|
|
let recorder = MacAudioRecorder()
|
|
let firstStream = recorder.makeSnapshotStream()
|
|
let drain = Task { for await _ in firstStream {} }
|
|
|
|
let installed = DispatchSemaphore(value: 0)
|
|
DispatchQueue.global().async {
|
|
_ = recorder.makeSnapshotStream()
|
|
installed.signal()
|
|
}
|
|
|
|
XCTAssertEqual(
|
|
installed.wait(timeout: .now() + 2),
|
|
.success,
|
|
"Replacing the snapshot stream deadlocked: finish() ran while holding the recorder lock."
|
|
)
|
|
drain.cancel()
|
|
}
|
|
|
|
/// The outgoing stream's termination handler fires *during* the install of
|
|
/// its replacement, so it must recognise itself as stale and leave the new
|
|
/// sink attached — otherwise live ASR silently receives no audio.
|
|
func testReplacingSnapshotStreamKeepsTheNewSinkAttached() {
|
|
let recorder = MacAudioRecorder()
|
|
let firstStream = recorder.makeSnapshotStream()
|
|
let drain = Task { for await _ in firstStream {} }
|
|
|
|
let secondStream = recorder.makeSnapshotStream()
|
|
|
|
XCTAssertTrue(
|
|
recorder.hasLiveSnapshotSink,
|
|
"The replaced stream's termination detached the sink that had just replaced it."
|
|
)
|
|
// The sink lives only as long as the stream: releasing `secondStream`
|
|
// early would terminate it and invalidate the assertion above.
|
|
withExtendedLifetime(secondStream) {}
|
|
drain.cancel()
|
|
}
|
|
}
|