using MeetingAssistant.Transcription; using Microsoft.Extensions.Logging.Abstractions; namespace MeetingAssistant.Tests; public sealed class SpeechRecognitionPipelineHostedServiceTests { [Fact] public async Task HostedServiceInitializesAndWarmsConfiguredPipelineWithoutBlockingStartup() { var pipeline = new CapturingSpeechRecognitionPipeline { BlockReadinessUntilCancelled = true }; var service = new SpeechRecognitionPipelineHostedService( new CapturingSpeechRecognitionPipelineFactory(pipeline), NullLogger.Instance); var startTask = service.StartAsync(CancellationToken.None); await startTask.WaitAsync(TimeSpan.FromSeconds(1)); await pipeline.WaitForInitializeAsync(); await pipeline.WaitForReadinessAsync(); await service.StopAsync(CancellationToken.None); Assert.Equal(1, pipeline.InitializeCount); Assert.Equal(1, pipeline.ReadinessCount); Assert.True(pipeline.ReadinessCancellationWasObserved); Assert.True(pipeline.Disposed); } private sealed class CapturingSpeechRecognitionPipelineFactory : ISpeechRecognitionPipelineFactory { private readonly ISpeechRecognitionPipeline pipeline; public CapturingSpeechRecognitionPipelineFactory(ISpeechRecognitionPipeline pipeline) { this.pipeline = pipeline; } public ISpeechRecognitionPipeline Create() { return pipeline; } } private sealed class CapturingSpeechRecognitionPipeline : ISpeechRecognitionPipeline { private readonly TaskCompletionSource initialized = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource readinessChecked = new(TaskCreationOptions.RunContinuationsAsynchronously); public int InitializeCount { get; private set; } public int ReadinessCount { get; private set; } public bool BlockReadinessUntilCancelled { get; init; } public bool ReadinessCancellationWasObserved { get; private set; } public bool Disposed { get; private set; } public Task WaitForInitializeAsync() { return initialized.Task.WaitAsync(TimeSpan.FromSeconds(5)); } public Task WaitForReadinessAsync() { return readinessChecked.Task.WaitAsync(TimeSpan.FromSeconds(5)); } public Task InitializeAsync(CancellationToken cancellationToken) { return InitializeAsync(SpeechRecognitionPipelineOptions.Default, cancellationToken); } public Task InitializeAsync( SpeechRecognitionPipelineOptions options, CancellationToken cancellationToken) { InitializeCount++; initialized.TrySetResult(); return Task.CompletedTask; } public async Task WaitUntilReadyAsync(CancellationToken cancellationToken) { ReadinessCount++; readinessChecked.TrySetResult(); if (!BlockReadinessUntilCancelled) { return; } try { await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); } catch (OperationCanceledException) { ReadinessCancellationWasObserved = true; throw; } } public ValueTask WriteAsync(MeetingAssistant.Recording.AudioChunk chunk, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } public Task CompleteAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public async IAsyncEnumerable ReadLiveTranscriptAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { await Task.CompletedTask; yield break; } public Task> ReadFinishedTranscriptAsync( string audioPath, SpeechRecognitionPipelineOptions options, CancellationToken cancellationToken) { return Task.FromResult>([]); } public ValueTask DisposeAsync() { Disposed = true; return ValueTask.CompletedTask; } } }