Files
gart/code/Hex-main/HexTests/RecordingRaceTests.swift
T

375 lines
11 KiB
Swift
Executable File

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<RecordingStopResult, Never>?
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<Void, Never>?
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
}
}