Files
OSGKeyboard/OSGKeyboardMacTests/MacDictationViewModelTests.swift
T
Rocky 498f407585 feat(account): add managed credits and cloud gateway
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.
2026-08-20 11:43:21 +08:00

102 lines
2.9 KiB
Swift

// MacDictationViewModelTests.swift
// OSGKeyboard · Mac tests
//
// Regression coverage for cancelling an asynchronous recorder start.
import Foundation
@testable import OSGKeyboard
import XCTest
@MainActor
final class MacDictationViewModelTests: XCTestCase {
func testCancellingButtonPreparationKeepsGateClosedUntilStartUnwinds() async {
let suiteName = "com.osgkeyboard.mac.tests.prepare.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) }
let recorder = SuspendedMacAudioRecorder()
let viewModel = MacDictationViewModel(
defaults: defaults,
recorder: recorder,
startHotkeyService: false
)
viewModel.toggleRecording()
let didStartPreparing = await waitUntil { recorder.isStartPending }
XCTAssertTrue(didStartPreparing)
XCTAssertTrue(viewModel.isPreparingToRecord)
viewModel.toggleRecording()
XCTAssertTrue(
viewModel.isPreparingToRecord,
"Cancellation must not reopen the start gate while recorder.start() is still unwinding."
)
recorder.completeStart()
let didFinishCancelling = await waitUntil { !viewModel.isPreparingToRecord }
XCTAssertTrue(didFinishCancelling)
XCTAssertFalse(viewModel.isRecording)
XCTAssertFalse(viewModel.isProcessing)
XCTAssertGreaterThanOrEqual(recorder.stopCallCount, 1)
}
private func waitUntil(
_ predicate: @escaping @MainActor () -> Bool
) async -> Bool {
for _ in 0..<100 {
if predicate() { return true }
try? await Task.sleep(for: .milliseconds(5))
}
return false
}
}
private final class SuspendedMacAudioRecorder: MacAudioRecording, @unchecked Sendable {
private let lock = NSLock()
private var startContinuation: CheckedContinuation<Void, any Error>?
private var stops = 0
var isStartPending: Bool {
lock.lock()
defer { lock.unlock() }
return startContinuation != nil
}
var stopCallCount: Int {
lock.lock()
defer { lock.unlock() }
return stops
}
func level() -> Float { 0 }
func start() async throws {
try await withCheckedThrowingContinuation { continuation in
lock.lock()
startContinuation = continuation
lock.unlock()
}
}
func completeStart() {
lock.lock()
let continuation = startContinuation
startContinuation = nil
lock.unlock()
continuation?.resume()
}
func makeSnapshotStream() -> AsyncStream<AudioBufferSnapshot> {
AsyncStream { $0.finish() }
}
func stop() -> [Float] {
lock.lock()
stops += 1
lock.unlock()
return []
}
}