import AppKit import ComposableArchitecture import Foundation import HexCore import XCTest @testable import Hex @MainActor final class RecordingRaceTests: XCTestCase { func testNewRecordingCancelsPendingDiscardCleanup() async throws { let now = Date(timeIntervalSince1970: 1_234) let activeApp = NSWorkspace.shared.frontmostApplication let stopURL = FileManager.default.temporaryDirectory .appendingPathComponent("discard-cleanup-\(UUID().uuidString).wav") let created = FileManager.default.createFile( atPath: stopURL.path, contents: Data("test".utf8) ) XCTAssertTrue(created) defer { try? FileManager.default.removeItem(at: stopURL) } let probe = RecordingProbe(stopURL: stopURL) let store = TestStore(initialState: Self.makeState()) { TranscriptionFeature() } withDependencies: { $0.date.now = now $0.recording.startRecording = { await probe.recordStart() } $0.recording.stopRecording = { await probe.beginStop() } $0.sleepManagement.preventSleep = { _ in } $0.sleepManagement.allowSleep = {} $0.soundEffects.play = { _ in } } await store.send(.startRecording) { $0.isRecording = true $0.recordingStartTime = now $0.sourceAppBundleID = activeApp?.bundleIdentifier $0.sourceAppName = activeApp?.localizedName } await store.send(.discard) { $0.isRecording = false $0.isPrewarming = false } await probe.waitForPendingStop() await store.send(.startRecording) { $0.isRecording = true $0.recordingStartTime = now $0.sourceAppBundleID = activeApp?.bundleIdentifier $0.sourceAppName = activeApp?.localizedName } await probe.resumePendingStop() await store.finish() let counts = await probe.counts() XCTAssertEqual(counts.startCalls, 2) XCTAssertEqual(counts.stopCalls, 1) XCTAssertFalse(FileManager.default.fileExists(atPath: stopURL.path)) } func testStopGuardIgnoresOnlyStaleSessions() { let currentSessionID = UUID() XCTAssertFalse( RecordingClientLive.shouldIgnoreStopRequest( snapshotSessionID: currentSessionID, currentSessionID: currentSessionID ) ) XCTAssertFalse( RecordingClientLive.shouldIgnoreStopRequest( snapshotSessionID: nil, currentSessionID: currentSessionID ) ) XCTAssertTrue( RecordingClientLive.shouldIgnoreStopRequest( snapshotSessionID: currentSessionID, currentSessionID: UUID() ) ) } func testCaptureControllerIgnoresCallbacksFromOlderGeneration() { XCTAssertTrue( SuperFastCaptureController.shouldProcessCallback( callbackGeneration: 2, currentGeneration: 2 ) ) XCTAssertFalse( SuperFastCaptureController.shouldProcessCallback( callbackGeneration: 1, currentGeneration: 2 ) ) } func testFailedRecordingStopEndsTranscription() async { let now = Date(timeIntervalSince1970: 1_234) var state = Self.makeState() state.isRecording = true state.recordingStartTime = now state.$hexSettings.withLock { settings in settings.hotkey = HotKey(key: .a, modifiers: [.command]) } let store = TestStore(initialState: state) { TranscriptionFeature() } withDependencies: { $0.date.now = now $0.recording.stopRecording = { .failed(.fallbackExportFailed("copy failed")) } $0.sleepManagement.allowSleep = {} } await store.send(.stopRecording) { $0.isRecording = false $0.isTranscribing = true $0.error = nil $0.isPrewarming = true } await store.receive(\.transcriptionError) { $0.isTranscribing = false $0.isPrewarming = false $0.error = "Failed to export recorded audio: copy failed" } } func testShortRecordingReleasesSleepAssertion() async throws { let now = Date(timeIntervalSince1970: 1_234) let stopURL = FileManager.default.temporaryDirectory .appendingPathComponent("short-recording-\(UUID().uuidString).wav") let created = FileManager.default.createFile( atPath: stopURL.path, contents: Data("test".utf8) ) XCTAssertTrue(created) defer { try? FileManager.default.removeItem(at: stopURL) } let probe = SleepProbe() let store = TestStore(initialState: Self.makeState()) { TranscriptionFeature() } withDependencies: { $0.date.now = now $0.recording.startRecording = {} $0.recording.stopRecording = { .captured(stopURL) } $0.sleepManagement.preventSleep = { _ in await probe.recordPreventSleep() } $0.sleepManagement.allowSleep = { await probe.recordAllowSleep() } $0.soundEffects.play = { _ in } } await store.send(.startRecording) { $0.isRecording = true $0.recordingStartTime = now $0.sourceAppBundleID = NSWorkspace.shared.frontmostApplication?.bundleIdentifier $0.sourceAppName = NSWorkspace.shared.frontmostApplication?.localizedName } await store.send(.stopRecording) { $0.isRecording = false } await store.finish() let counts = await probe.counts() XCTAssertEqual(counts.preventSleepCalls, 1) XCTAssertEqual(counts.allowSleepCalls, 1) XCTAssertFalse(FileManager.default.fileExists(atPath: stopURL.path)) } func testDiscardCancelsPendingRecordingStart() async { let now = Date(timeIntervalSince1970: 1_234) let stopURL = FileManager.default.temporaryDirectory .appendingPathComponent("pending-start-discard-\(UUID().uuidString).wav") let sleepProbe = PendingSleepProbe() let recordingProbe = RecordingProbe(stopURL: stopURL) let store = TestStore(initialState: Self.makeState()) { TranscriptionFeature() } withDependencies: { $0.date.now = now $0.recording.startRecording = { await recordingProbe.recordStart() } $0.recording.stopRecording = { await recordingProbe.beginImmediateStop() } $0.sleepManagement.preventSleep = { _ in await sleepProbe.preventSleep() } $0.sleepManagement.allowSleep = {} $0.soundEffects.play = { _ in } } await store.send(.startRecording) { $0.isRecording = true $0.recordingStartTime = now $0.sourceAppBundleID = NSWorkspace.shared.frontmostApplication?.bundleIdentifier $0.sourceAppName = NSWorkspace.shared.frontmostApplication?.localizedName } await sleepProbe.waitUntilPending() await store.send(.discard) { $0.isRecording = false $0.isPrewarming = false } await sleepProbe.resume() await store.finish() let counts = await recordingProbe.counts() XCTAssertEqual(counts.startCalls, 0) XCTAssertEqual(counts.stopCalls, 1) } func testEmptyTranscriptionDeletesCapturedAudio() async throws { let audioURL = FileManager.default.temporaryDirectory .appendingPathComponent("empty-transcription-\(UUID().uuidString).wav") let created = FileManager.default.createFile( atPath: audioURL.path, contents: Data("test".utf8) ) XCTAssertTrue(created) defer { try? FileManager.default.removeItem(at: audioURL) } let store = TestStore(initialState: Self.makeState()) { TranscriptionFeature() } await store.send(.transcriptionResult("", audioURL, 1.25)) await store.finish() XCTAssertFalse(FileManager.default.fileExists(atPath: audioURL.path)) } func testHistoryUsesRecordingDurationCapturedAtStop() async { let duration = 1.25 let audioURL = FileManager.default.temporaryDirectory .appendingPathComponent("history-duration-\(UUID().uuidString).wav") let transcript = Transcript( timestamp: Date(timeIntervalSince1970: 1_234), text: "hello", audioPath: audioURL, duration: duration, sourceAppBundleID: nil, sourceAppName: nil ) let probe = TranscriptPersistenceProbe() let store = TestStore(initialState: Self.makeState()) { TranscriptionFeature() } withDependencies: { $0.transcriptPersistence.save = { text, audioURL, duration, sourceAppBundleID, sourceAppName in await probe.record(duration: duration) return transcript } $0.pasteboard.paste = { _ in } $0.soundEffects.play = { _ in } } await store.send(.transcriptionResult("hello", audioURL, duration)) while await probe.duration == nil { await Task.yield() } store.assert { $0.$transcriptionHistory.withLock { $0.history = [transcript] } } await store.finish() let storedDuration = await probe.duration XCTAssertEqual(storedDuration, duration) } private static func makeState() -> TranscriptionFeature.State { TranscriptionFeature.State( hexSettings: Shared(value: .init()), isRemappingScratchpadFocused: false, modelBootstrapState: Shared(value: .init(isModelReady: true)), transcriptionHistory: Shared(value: .init()) ) } } private actor RecordingProbe { private let stopURL: URL private var startCalls = 0 private var stopCalls = 0 private var stopContinuation: CheckedContinuation? init(stopURL: URL) { self.stopURL = stopURL } func recordStart() { startCalls += 1 } func beginStop() async -> RecordingStopResult { stopCalls += 1 return await withCheckedContinuation { continuation in stopContinuation = continuation } } func beginImmediateStop() -> RecordingStopResult { stopCalls += 1 return .captured(stopURL) } func waitForPendingStop() async { while stopContinuation == nil { await Task.yield() } } func resumePendingStop() { stopContinuation?.resume(returning: .captured(stopURL)) stopContinuation = nil } func counts() -> (startCalls: Int, stopCalls: Int) { (startCalls, stopCalls) } } private actor SleepProbe { private var preventSleepCalls = 0 private var allowSleepCalls = 0 func recordPreventSleep() { preventSleepCalls += 1 } func recordAllowSleep() { allowSleepCalls += 1 } func counts() -> (preventSleepCalls: Int, allowSleepCalls: Int) { (preventSleepCalls, allowSleepCalls) } } private actor PendingSleepProbe { private var continuation: CheckedContinuation? func preventSleep() async { await withCheckedContinuation { continuation in self.continuation = continuation } } func waitUntilPending() async { while continuation == nil { await Task.yield() } } func resume() { continuation?.resume() continuation = nil } } private actor TranscriptPersistenceProbe { private(set) var duration: TimeInterval? func record(duration: TimeInterval) { self.duration = duration } }