Files
meeting-assistant/MeetingAssistant.Tests/FunAsrBackendLifecycleTests.cs
T
2026-05-20 02:06:16 +02:00

204 lines
7.4 KiB
C#

using MeetingAssistant.Transcription;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.Tests;
public sealed class FunAsrBackendLifecycleTests
{
[Fact]
public async Task LifecycleInstallsStartsAndStopsManagedDockerContainer()
{
var runner = new CapturingCommandRunner();
var readinessProbe = new CapturingReadinessProbe();
var modelsFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var lifecycle = new FunAsrDockerBackendLifecycle(
runner,
readinessProbe,
Options.Create(new MeetingAssistantOptions
{
FunAsr = new FunAsrOptions
{
Endpoint = "ws://127.0.0.1:10095",
Backend = new FunAsrBackendOptions
{
Enabled = true,
Image = "funasr:test",
ContainerName = "meeting-assistant-funasr-test",
ModelsFolder = modelsFolder,
StartupTimeout = TimeSpan.Zero
}
}
}),
NullLogger<FunAsrDockerBackendLifecycle>.Instance);
await lifecycle.EnsureStartedAsync(CancellationToken.None);
await lifecycle.EnsureStartedAsync(CancellationToken.None);
await lifecycle.DisposeAsync();
Assert.Equal(new Uri("ws://127.0.0.1:10095"), readinessProbe.Endpoint);
Assert.Equal(TimeSpan.Zero, readinessProbe.Timeout);
Assert.Collection(
runner.Commands,
command =>
{
Assert.Equal("docker", command.FileName);
Assert.Equal(["pull", "funasr:test"], command.Arguments);
},
command =>
{
Assert.Equal(["rm", "-f", "meeting-assistant-funasr-test"], command.Arguments);
},
command =>
{
Assert.Equal("docker", command.FileName);
Assert.Contains("run", command.Arguments);
Assert.Contains("-d", command.Arguments);
Assert.Contains("--privileged=true", command.Arguments);
Assert.Contains("--name", command.Arguments);
Assert.Contains("meeting-assistant-funasr-test", command.Arguments);
Assert.Contains("-p", command.Arguments);
Assert.Contains("10095:10095", command.Arguments);
Assert.Contains($"{modelsFolder}:/workspace/models", command.Arguments);
Assert.Contains("funasr:test", command.Arguments);
},
command =>
{
Assert.Equal(["stop", "meeting-assistant-funasr-test"], command.Arguments);
});
Assert.True(File.Exists(Path.Combine(modelsFolder, "hotwords.txt")));
}
[Fact]
public async Task DisabledLifecycleDoesNotRunDockerCommands()
{
var runner = new CapturingCommandRunner();
var lifecycle = new FunAsrDockerBackendLifecycle(
runner,
new CapturingReadinessProbe(),
Options.Create(new MeetingAssistantOptions
{
FunAsr = new FunAsrOptions
{
Backend = new FunAsrBackendOptions { Enabled = false }
}
}),
NullLogger<FunAsrDockerBackendLifecycle>.Instance);
await lifecycle.EnsureStartedAsync(CancellationToken.None);
await lifecycle.DisposeAsync();
Assert.Empty(runner.Commands);
}
[Fact]
public async Task LifecycleFailsWhenDockerCommandTimesOut()
{
var runner = new HangingCommandRunner();
var lifecycle = new FunAsrDockerBackendLifecycle(
runner,
new CapturingReadinessProbe(),
Options.Create(new MeetingAssistantOptions
{
FunAsr = new FunAsrOptions
{
Backend = new FunAsrBackendOptions
{
Enabled = true,
CommandTimeout = TimeSpan.FromMilliseconds(1)
}
}
}),
NullLogger<FunAsrDockerBackendLifecycle>.Instance);
var exception = await Assert.ThrowsAsync<TimeoutException>(
() => lifecycle.EnsureStartedAsync(CancellationToken.None));
Assert.Contains("timed out", exception.Message);
}
[Fact]
public async Task LifecycleWaitsForWebSocketReadinessBeforeMarkingStarted()
{
var runner = new CapturingCommandRunner();
var readinessProbe = new CapturingReadinessProbe();
var lifecycle = new FunAsrDockerBackendLifecycle(
runner,
readinessProbe,
Options.Create(new MeetingAssistantOptions
{
FunAsr = new FunAsrOptions
{
Endpoint = "ws://127.0.0.1:10095",
Backend = new FunAsrBackendOptions
{
Enabled = true,
Image = "funasr:test",
ContainerName = "meeting-assistant-funasr-test",
ModelsFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")),
StartupTimeout = TimeSpan.FromMinutes(3)
}
}
}),
NullLogger<FunAsrDockerBackendLifecycle>.Instance);
await lifecycle.EnsureStartedAsync(CancellationToken.None);
Assert.Equal(1, readinessProbe.CallCount);
Assert.Equal(new Uri("ws://127.0.0.1:10095"), readinessProbe.Endpoint);
Assert.Equal(TimeSpan.FromMinutes(3), readinessProbe.Timeout);
Assert.Equal(3, runner.Commands.Count);
await lifecycle.DisposeAsync();
}
private sealed class CapturingCommandRunner : ICommandRunner
{
public List<CapturedCommand> Commands { get; } = [];
public Task<CommandResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{
Commands.Add(new CapturedCommand(fileName, arguments.ToArray()));
return Task.FromResult(new CommandResult(0, "", ""));
}
}
private sealed record CapturedCommand(string FileName, IReadOnlyList<string> Arguments);
private sealed class HangingCommandRunner : ICommandRunner
{
public async Task<CommandResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken,
IReadOnlyDictionary<string, string>? environment = null)
{
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
return new CommandResult(0, "", "");
}
}
private sealed class CapturingReadinessProbe : IFunAsrBackendReadinessProbe
{
public int CallCount { get; private set; }
public Uri? Endpoint { get; private set; }
public TimeSpan Timeout { get; private set; }
public Task WaitUntilReadyAsync(
Uri endpoint,
TimeSpan timeout,
CancellationToken cancellationToken)
{
CallCount++;
Endpoint = endpoint;
Timeout = timeout;
return Task.CompletedTask;
}
}
}