fix(mac,asr): harden menu-bar delivery, chunk retry, and polish validator

Retain the external target app for menu-bar paste and polish context, retry
failed middle ASR chunks once, and stop false-positive path/number violations.
This commit is contained in:
Rocky
2026-07-29 18:30:36 +08:00
parent 34be2e8dd1
commit 4c929d8b8b
17 changed files with 646 additions and 39 deletions
@@ -0,0 +1,101 @@
// MacDictationViewModelTests.swift
// OSGKeyboard · Mac tests
//
// Regression coverage for cancelling an asynchronous recorder start.
import Foundation
import XCTest
@testable import OSGKeyboard
@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 []
}
}
@@ -0,0 +1,54 @@
// MacTextInsertionServiceTests.swift
// OSGKeyboard · Mac tests
//
// Regression coverage for clipboard preservation and captured-app context.
import AppKit
import XCTest
@testable import OSGKeyboard
final class MacTextInsertionServiceTests: XCTestCase {
func testRestoreRequiresTranscriptToStillOwnPasteboard() {
XCTAssertTrue(
MacTextInsertionService.shouldRestorePasteboard(
transcriptChangeCount: 12,
currentChangeCount: 12
)
)
XCTAssertFalse(
MacTextInsertionService.shouldRestorePasteboard(
transcriptChangeCount: 12,
currentChangeCount: 13
),
"A newer clipboard write must not be overwritten by restoration."
)
}
func testRestoringOriginallyEmptyPasteboardClearsTranscript() {
let pasteboard = NSPasteboard(
name: NSPasteboard.Name("MacTextInsertionServiceTests.\(UUID().uuidString)")
)
pasteboard.clearContents()
pasteboard.setString("transcript", forType: .string)
MacTextInsertionService.restoreItems([], to: pasteboard)
XCTAssertNil(pasteboard.string(forType: .string))
}
func testCapturedBundleIdentifierDrivesPolishContext() {
XCTAssertEqual(
MacAppContextService.detectContext(bundleIdentifier: "com.apple.dt.Xcode"),
.code
)
XCTAssertEqual(
MacAppContextService.detectContext(bundleIdentifier: "com.tencent.xinWeChat"),
.chat
)
XCTAssertEqual(
MacAppContextService.detectContext(bundleIdentifier: "com.osgkeyboard.mac"),
.unknown
)
}
}