using MeetingAssistant; using MeetingAssistant.LaunchProfiles; using MeetingAssistant.Transcription; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace MeetingAssistant.Tests; public sealed class PyannoteDiarizationWarmupHostedServiceTests { [Fact] public async Task HostedServiceWarmsEnabledValidationRuntimeWithoutBlockingStartup() { var commandRunner = new BlockingCommandRunner(); var finalizer = new PyannoteTranscriptFinalizer( commandRunner, Options.Create(new MeetingAssistantOptions()), NullLogger.Instance); var service = new PyannoteDiarizationWarmupHostedService( finalizer, new FakeLaunchProfileOptionsProvider( CreateValidationOptions("meeting-assistant-pyannote-validation:local")), NullLogger.Instance); await service.StartAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(1)); await commandRunner.WaitForRunAsync(); await service.StopAsync(CancellationToken.None); Assert.Contains(commandRunner.Commands, command => command.Arguments.Contains("meeting-assistant-pyannote-validation:local")); Assert.True(commandRunner.RunCancellationWasObserved); } [Fact] public async Task HostedServiceUsesApplicationValidationRuntimeInsteadOfNamedProfileOverride() { var commandRunner = new BlockingCommandRunner(); var finalizer = new PyannoteTranscriptFinalizer( commandRunner, Options.Create(new MeetingAssistantOptions()), NullLogger.Instance); var defaultOptions = CreateValidationOptions("meeting-assistant-pyannote-default:local"); var namedOptions = CreateValidationOptions("meeting-assistant-pyannote-named:local"); var service = new PyannoteDiarizationWarmupHostedService( finalizer, new FakeLaunchProfileOptionsProvider( [ new LaunchProfile("english", namedOptions), new LaunchProfile(ConfigurationLaunchProfileOptionsProvider.DefaultProfileName, defaultOptions) ]), NullLogger.Instance); await service.StartAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(1)); await commandRunner.WaitForRunAsync(); await service.StopAsync(CancellationToken.None); Assert.Contains(commandRunner.Commands, command => command.Arguments.Contains("meeting-assistant-pyannote-default:local")); Assert.DoesNotContain(commandRunner.Commands, command => command.Arguments.Contains("meeting-assistant-pyannote-named:local")); } private static MeetingAssistantOptions CreateValidationOptions(string image) { return new MeetingAssistantOptions { Recording = { TranscriptionProvider = "azure-speech" }, SpeakerIdentification = { PyannoteValidation = { Enabled = true, Diarization = { DockerCommand = "docker", Image = image, ModelsFolder = Path.Combine( Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "models"), Token = "hf_test", TokenEnv = "", CommandTimeout = TimeSpan.FromMinutes(1) } } } }; } private sealed class FakeLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider { private readonly IReadOnlyList profiles; public FakeLaunchProfileOptionsProvider(MeetingAssistantOptions options) : this([new LaunchProfile(ConfigurationLaunchProfileOptionsProvider.DefaultProfileName, options)]) { } public FakeLaunchProfileOptionsProvider(IReadOnlyList profiles) { this.profiles = profiles; } public LaunchProfile GetRequiredProfile(string? name) { var profileName = string.IsNullOrWhiteSpace(name) ? ConfigurationLaunchProfileOptionsProvider.DefaultProfileName : name; return profiles.Single(profile => profile.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); } public IReadOnlyList GetProfiles() { return profiles; } public IReadOnlyList GetHotkeys() { return []; } } private sealed class BlockingCommandRunner : ICommandRunner { private readonly TaskCompletionSource runStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); public IReadOnlyList Commands { get; private set; } = []; public bool RunCancellationWasObserved { get; private set; } public Task WaitForRunAsync() { return runStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); } public async Task RunAsync( string fileName, IReadOnlyList arguments, CancellationToken cancellationToken, IReadOnlyDictionary? environment = null) { Commands = Commands.Append(new CapturedCommand(fileName, arguments)).ToList(); if (arguments.Contains("inspect")) { return new CommandResult(0, "image-id", ""); } runStarted.TrySetResult(); try { await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); } catch (OperationCanceledException) { RunCancellationWasObserved = true; throw; } return new CommandResult(0, "", ""); } } private sealed record CapturedCommand(string FileName, IReadOnlyList Arguments); }