chore: adiciona .gitignore e commit.command
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
import ComposableArchitecture
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Hex
|
||||
|
||||
@MainActor
|
||||
final class HistoryPlaybackTests: XCTestCase {
|
||||
func testStoppingPlaybackCompletesWaitersExactlyOnce() async {
|
||||
let controller = AudioPlayerController()
|
||||
let waiter = Task {
|
||||
await controller.waitForPlaybackToFinish()
|
||||
}
|
||||
|
||||
await Task.yield()
|
||||
controller.stop()
|
||||
controller.stop()
|
||||
|
||||
await waiter.value
|
||||
await controller.waitForPlaybackToFinish()
|
||||
}
|
||||
|
||||
func testStalePlaybackFinishedDoesNotStopCurrentPlayback() async {
|
||||
let transcriptID = UUID()
|
||||
let playbackID = UUID()
|
||||
let store = TestStore(
|
||||
initialState: HistoryFeature.State(
|
||||
transcriptionHistory: Shared(value: .init()),
|
||||
playingTranscriptID: transcriptID,
|
||||
playbackID: playbackID
|
||||
)
|
||||
) {
|
||||
HistoryFeature()
|
||||
}
|
||||
|
||||
await store.send(.playbackFinished(UUID()))
|
||||
|
||||
XCTAssertEqual(store.state.playingTranscriptID, transcriptID)
|
||||
XCTAssertEqual(store.state.playbackID, playbackID)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import XCTest
|
||||
|
||||
@testable import Hex
|
||||
|
||||
final class KeyEventMonitorClientTests: XCTestCase {
|
||||
func testRegisteringFirstHandlerWhenInputMonitoringIsDeniedDoesNotReenterQueue() {
|
||||
let monitor = KeyEventMonitorClientLive(
|
||||
accessibilityTrustProvider: { false },
|
||||
accessibilityTrustPrompt: { false },
|
||||
inputMonitoringTrustProvider: { false }
|
||||
)
|
||||
|
||||
let token = monitor.handleKeyEvent { _ in false }
|
||||
|
||||
XCTAssertTrue(monitor.readState { true })
|
||||
token.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import Hex
|
||||
|
||||
final class LegacyModelCacheMigratorTests: XCTestCase {
|
||||
func testReplacesIncompleteCurrentDirectoryWithValidLegacyDirectory() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
let legacy = root.appendingPathComponent("model-coreml")
|
||||
let current = root.appendingPathComponent("model")
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
|
||||
try FileManager.default.createDirectory(at: legacy, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: current, withIntermediateDirectories: true)
|
||||
try Data("complete".utf8).write(to: legacy.appendingPathComponent("model.bin"))
|
||||
try Data("partial".utf8).write(to: current.appendingPathComponent("model.bin.partial"))
|
||||
|
||||
let migrated = try LegacyModelCacheMigrator.migrate(from: legacy, to: current) {
|
||||
FileManager.default.fileExists(atPath: $0.appendingPathComponent("model.bin").path)
|
||||
}
|
||||
|
||||
XCTAssertTrue(migrated)
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: legacy.path))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: current.appendingPathComponent("model.bin").path))
|
||||
XCTAssertFalse(FileManager.default.fileExists(atPath: current.appendingPathComponent("model.bin.partial").path))
|
||||
}
|
||||
|
||||
func testRestoresBothDirectoriesWhenLegacyDirectoryIsInvalid() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
let legacy = root.appendingPathComponent("model-coreml")
|
||||
let current = root.appendingPathComponent("model")
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
|
||||
try FileManager.default.createDirectory(at: legacy, withIntermediateDirectories: true)
|
||||
try FileManager.default.createDirectory(at: current, withIntermediateDirectories: true)
|
||||
try Data("legacy".utf8).write(to: legacy.appendingPathComponent("legacy.bin"))
|
||||
try Data("partial".utf8).write(to: current.appendingPathComponent("partial.bin"))
|
||||
|
||||
XCTAssertThrowsError(
|
||||
try LegacyModelCacheMigrator.migrate(from: legacy, to: current) { _ in false }
|
||||
)
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: legacy.appendingPathComponent("legacy.bin").path))
|
||||
XCTAssertTrue(FileManager.default.fileExists(atPath: current.appendingPathComponent("partial.bin").path))
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import ComposableArchitecture
|
||||
import HexCore
|
||||
import XCTest
|
||||
|
||||
@testable import Hex
|
||||
|
||||
@MainActor
|
||||
final class ModelDownloadFeatureTests: XCTestCase {
|
||||
func testPersistedModelDisplaysWhileAvailabilityLoads() {
|
||||
let state = makeState(selectedModel: ParakeetModel.englishV2.identifier)
|
||||
|
||||
XCTAssertTrue(state.availableModels.isEmpty)
|
||||
XCTAssertEqual(state.selectedModelNameForDisplay, ParakeetModel.englishV2.identifier)
|
||||
}
|
||||
|
||||
func testSuggestedParakeetFollowsOutputLanguage() {
|
||||
var state = makeState(selectedModel: "")
|
||||
XCTAssertEqual(state.preferredParakeetIdentifier, ParakeetModel.englishV2.identifier)
|
||||
|
||||
state.$hexSettings.withLock { $0.outputLanguage = "en" }
|
||||
XCTAssertEqual(state.preferredParakeetIdentifier, ParakeetModel.englishV2.identifier)
|
||||
|
||||
state.$hexSettings.withLock { $0.outputLanguage = "fr" }
|
||||
XCTAssertEqual(state.preferredParakeetIdentifier, ParakeetModel.multilingualV3.identifier)
|
||||
}
|
||||
|
||||
func testFailedModelRefreshPreservesInstalledSelection() async {
|
||||
var state = makeState(selectedModel: "installed-model")
|
||||
state.availableModels = [ModelInfo(name: "installed-model", isDownloaded: true)]
|
||||
|
||||
let store = TestStore(initialState: state) {
|
||||
ModelDownloadFeature()
|
||||
} withDependencies: {
|
||||
$0.transcription.getRecommendedModels = { throw TestError() }
|
||||
$0.transcription.getAvailableModels = { [] }
|
||||
}
|
||||
|
||||
await store.send(.fetchModels) {
|
||||
$0.isLoadingModels = true
|
||||
}
|
||||
await store.receive(\.modelsLoadFailed) {
|
||||
$0.isLoadingModels = false
|
||||
}
|
||||
|
||||
XCTAssertEqual(store.state.hexSettings.selectedModel, "installed-model")
|
||||
XCTAssertTrue(store.state.availableModels[id: "installed-model"]?.isDownloaded == true)
|
||||
}
|
||||
|
||||
func testStaleDownloadUpdatesAreIgnored() async {
|
||||
let currentID = UUID()
|
||||
var state = makeState(selectedModel: "installed-model")
|
||||
state.isDownloading = true
|
||||
state.downloadingModelName = "new-model"
|
||||
state.activeDownloadID = currentID
|
||||
state.downloadProgress = 0.25
|
||||
|
||||
let store = TestStore(initialState: state) {
|
||||
ModelDownloadFeature()
|
||||
}
|
||||
|
||||
await store.send(.downloadProgress(id: UUID(), progress: 0.9))
|
||||
await store.send(.downloadCompleted(id: UUID(), result: .success("other-model")))
|
||||
|
||||
XCTAssertEqual(store.state.activeDownloadID, currentID)
|
||||
XCTAssertEqual(store.state.downloadProgress, 0.25)
|
||||
XCTAssertEqual(store.state.hexSettings.selectedModel, "installed-model")
|
||||
}
|
||||
|
||||
func testModelsLoadedNeverClearsSelectionWhenNothingDetected() async {
|
||||
// Regression: 0.8.0 cleared selectedModel to "" when an availability scan
|
||||
// came back empty (e.g. after FluidAudio moved its cache directory),
|
||||
// permanently breaking transcription with no visible error.
|
||||
var state = makeState(selectedModel: ParakeetModel.englishV2.identifier)
|
||||
state.availableModels = []
|
||||
|
||||
let store = TestStore(initialState: state) {
|
||||
ModelDownloadFeature()
|
||||
}
|
||||
store.exhaustivity = .off
|
||||
|
||||
await store.send(.modelsLoaded(recommended: "", available: []))
|
||||
|
||||
XCTAssertEqual(store.state.hexSettings.selectedModel, ParakeetModel.englishV2.identifier)
|
||||
}
|
||||
|
||||
func testSelectedModelIsDownloadedMatchesPatterns() {
|
||||
var state = makeState(selectedModel: "distil*large-v3")
|
||||
state.availableModels = [
|
||||
ModelInfo(name: "distil-whisper_distil-large-v3", isDownloaded: true)
|
||||
]
|
||||
XCTAssertTrue(state.selectedModelIsDownloaded)
|
||||
|
||||
state.availableModels = [
|
||||
ModelInfo(name: "distil-whisper_distil-large-v3", isDownloaded: false)
|
||||
]
|
||||
XCTAssertFalse(state.selectedModelIsDownloaded)
|
||||
|
||||
var emptyState = makeState(selectedModel: "")
|
||||
emptyState.availableModels = [ModelInfo(name: "some-model", isDownloaded: true)]
|
||||
XCTAssertFalse(emptyState.selectedModelIsDownloaded)
|
||||
}
|
||||
|
||||
func testDeletingAnotherModelDoesNotChangeSelection() async {
|
||||
var state = makeState(selectedModel: "selected-model")
|
||||
state.availableModels = [
|
||||
ModelInfo(name: "selected-model", isDownloaded: true),
|
||||
ModelInfo(name: "other-model", isDownloaded: true),
|
||||
]
|
||||
|
||||
let store = TestStore(initialState: state) {
|
||||
ModelDownloadFeature()
|
||||
} withDependencies: {
|
||||
$0.transcription.deleteModel = { _ in
|
||||
try await Task.sleep(for: .seconds(60))
|
||||
}
|
||||
}
|
||||
|
||||
await store.send(.deleteModel("other-model"))
|
||||
XCTAssertEqual(store.state.hexSettings.selectedModel, "selected-model")
|
||||
await store.skipInFlightEffects()
|
||||
}
|
||||
|
||||
private func makeState(selectedModel: String) -> ModelDownloadFeature.State {
|
||||
var settings = HexSettings()
|
||||
settings.selectedModel = selectedModel
|
||||
settings.hasCompletedModelBootstrap = true
|
||||
return ModelDownloadFeature.State(
|
||||
hexSettings: Shared(value: settings),
|
||||
modelBootstrapState: Shared(value: .init(isModelReady: true))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private struct TestError: Error {}
|
||||
+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