375 lines
11 KiB
Swift
Executable File
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
|
|
}
|
|
}
|