chore: adiciona .gitignore e commit.command
This commit is contained in:
+374
@@ -0,0 +1,374 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user