Merge remote-tracking branch 'refs/remotes/fork/codex/macos-support' into codex/macos-support
PR and Push Build/Test / macos-build-and-test (pull_request) Canceled after 0s
PR and Push Build/Test / build-and-test (pull_request) Canceled after 0s

# Conflicts:
#	MeetingAssistant/Program.cs
#	README.md
This commit is contained in:
dh
2026-09-20 12:44:03 +02:00
15 changed files with 127 additions and 30 deletions
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using MeetingAssistant.Recording;
using MeetingAssistant.Workflow;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
@@ -54,6 +55,17 @@ public sealed class HealthEndpointTests : IClassFixture<WebApplicationFactory<Pr
Assert.Equal(HostOperatingSystem.DetectCurrent(), first);
}
[Fact]
public async Task NeutralBuildReportsSpeakerPlaybackUnavailable()
{
var playback = factory.Services.GetRequiredService<IWorkflowRulesEditorSamplePlaybackQueue>();
var result = await playback.QueueAsync(1, [1, 2]);
Assert.Contains("Windows build", result);
Assert.DoesNotContain("Queued", result);
}
[Fact]
public async Task ApplicationStartupDeletesStaleTemporaryRecordings()
{
@@ -5,6 +5,7 @@ using MeetingAssistant.Transcription;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -170,7 +171,7 @@ public sealed class MacOsMeetingAudioSourceTests
microphonePcm: Pcm16(2_000),
systemPcm: Pcm16(10_000));
var speechPipelines = new CapturingSpeechRecognitionPipelineFactory();
await using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configuration) =>
{
@@ -225,6 +226,9 @@ public sealed class MacOsMeetingAudioSourceTests
}
finally
{
await factory.DisposeAsync();
using var connection = new SqliteConnection($"Data Source={Path.Combine(testRoot, "speakers.db")}");
SqliteConnection.ClearPool(connection);
if (Directory.Exists(testRoot))
{
Directory.Delete(testRoot, recursive: true);
@@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>
@@ -2432,12 +2432,17 @@ public sealed class RecordingCoordinatorTests
await coordinator.StopAsync(CancellationToken.None);
}
[Fact]
public async Task ToggleToDifferentLaunchProfileBuffersAudioCapturedWhilePreviousPipelineDrains()
[Theory]
[InlineData("default", "english")]
[InlineData("english", "default")]
public async Task ProfileSwitchDrainsFinalTranscriptAndResumesBufferedAudioWithoutBlockingNotifications(
string initialProfile,
string targetProfile)
{
var audioSource = new ControlledAudioSource();
var pipelineFactory = new BlockingProfileSwitchSpeechRecognitionPipelineFactory();
var transcriptStore = new InMemoryTranscriptStore();
var promptService = new IgnoringMeetingInactivityPromptService();
var launchProfiles = CreateLaunchProfiles(
Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "default"),
Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "english"));
@@ -2452,13 +2457,16 @@ public sealed class RecordingCoordinatorTests
new CapturingMeetingSummaryPipeline(),
Options.Create(launchProfiles.GetRequiredProfile(null).Options),
NullLogger<MeetingRecordingCoordinator>.Instance,
inactivityPromptService: promptService,
launchProfiles: launchProfiles);
await coordinator.StartAsync(CancellationToken.None);
await coordinator.StartAsync(initialProfile, CancellationToken.None);
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
await transcriptStore.WaitForTextAsync("default chunk:2");
await transcriptStore.WaitForTextAsync($"{initialProfile} chunk:2");
await WaitUntilAsync(() => promptService.DismissAllCount == 1);
var switchTask = coordinator.ToggleAsync("english", CancellationToken.None);
using var switchCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var switchTask = coordinator.ToggleAsync(targetProfile, switchCancellation.Token);
await pipelineFactory.WaitUntilFirstPipelineDrainIsBlockedAsync();
await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0, 3, 0], 16000, 1), CancellationToken.None);
@@ -2466,10 +2474,21 @@ public sealed class RecordingCoordinatorTests
pipelineFactory.ReleaseFirstPipelineDrain();
await switchTask.WaitAsync(TimeSpan.FromSeconds(5));
await transcriptStore.WaitForTextAsync("english chunk:6");
await transcriptStore.WaitForTextAsync($"{targetProfile} chunk:6");
await WaitUntilAsync(() => promptService.DismissAllCount == 3);
Assert.Equal([null, "english"], pipelineFactory.ProfileNames);
await coordinator.StopAsync(CancellationToken.None);
Assert.Equal(targetProfile, coordinator.CurrentStatus.LaunchProfile);
Assert.Equal([initialProfile, targetProfile], pipelineFactory.ProfileNames);
var segments = transcriptStore.Segments.ToList();
var finalResultIndex = segments.FindIndex(segment => segment.Text == "Final result emitted while switching profiles.");
var markerIndex = segments.FindIndex(segment => segment.Text.Contains($"Transcription profile changed to {targetProfile}", StringComparison.Ordinal));
Assert.True(finalResultIndex >= 0 && finalResultIndex < markerIndex);
await coordinator.ToggleAsync(initialProfile, CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0], 16000, 1), CancellationToken.None);
await transcriptStore.WaitForTextAsync($"{initialProfile} chunk:4");
await coordinator.StopAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(coordinator.CurrentStatus.IsRecording);
}
[Fact]
@@ -5186,6 +5205,11 @@ public sealed class RecordingCoordinatorTests
drainBlocked.TrySetResult();
await releaseDrain.Task.WaitAsync(cancellationToken);
yield return new TranscriptionSegment(
TimeSpan.Zero,
TimeSpan.FromSeconds(3),
"Unknown",
"Final result emitted while switching profiles.");
}
}