fix(macos): preserve audio when transcription fails
PR and Push Build/Test / build-and-test (pull_request) Failing after 11m18s

This commit is contained in:
dh
2026-09-01 11:55:15 +02:00
parent 68db6955a2
commit 84dc696ce9
6 changed files with 132 additions and 5 deletions
@@ -15,6 +15,34 @@ namespace MeetingAssistant.Tests;
public sealed class MacOsMeetingAudioSourceTests
{
[Fact]
public void NativeMicrophoneHelperDeclaresMacOsPrivacyMetadata()
{
if (!OperatingSystem.IsMacOS())
{
return;
}
var configuration = new DirectoryInfo(AppContext.BaseDirectory)
.Parent?.Name ?? "Debug";
var helperPath = Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory,
"..",
"..",
"..",
"..",
"MeetingAssistant",
"bin",
configuration,
"net10.0",
"Native",
"macos-meeting-audio-capture"));
var helperImage = System.Text.Encoding.UTF8.GetString(File.ReadAllBytes(helperPath));
Assert.Contains("cloud.schweigert.meeting-assistant.audio-capture", helperImage, StringComparison.Ordinal);
Assert.Contains("NSMicrophoneUsageDescription", helperImage, StringComparison.Ordinal);
}
[Fact]
public async Task MacOsMicrophoneStreamsNativePcmUsingRunFormat()
{
@@ -1378,6 +1378,45 @@ public sealed class RecordingCoordinatorTests
Assert.False(audioArchive.Deleted);
}
[Fact]
public async Task StopQueuesAzureBacklogAndRetainsAudioWhenTranscriptionFails()
{
var audioSource = new ControlledAudioSource();
var provider = new FailingAfterFirstAudioProvider();
var audioArchive = new InMemoryRecordedAudioStore();
var backlog = new InMemoryOfflineTranscriptionBacklog();
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(provider),
new InMemoryTranscriptStore(),
new InMemoryMeetingNoteStore(),
new CapturingMeetingNoteOpener(),
new InMemoryMeetingArtifactStore(),
audioArchive,
new CapturingMeetingSummaryPipeline(),
Options.Create(new MeetingAssistantOptions
{
Recording = new RecordingOptions
{
TranscriptionProvider = "azure-speech"
}
}),
NullLogger<MeetingRecordingCoordinator>.Instance,
offlineTranscriptionBacklog: backlog);
await coordinator.StartAsync(CancellationToken.None);
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
await provider.WaitUntilFailureObservedAsync();
var stopped = await coordinator.StopAsync(CancellationToken.None);
Assert.False(stopped.IsRecording);
var item = Assert.Single(backlog.Items);
Assert.Equal("memory-recording.wav", item.AudioPath);
Assert.True(audioArchive.Completed);
Assert.False(audioArchive.Deleted);
}
[Fact]
public async Task OfflineBacklogReplaysQueuedRecordingAndCompletesMeetingArtifacts()
{
@@ -4988,6 +5027,31 @@ public sealed class RecordingCoordinatorTests
}
}
private sealed class FailingAfterFirstAudioProvider : IStreamingTranscriptionProvider
{
private readonly TaskCompletionSource failureObserved =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public Task WaitUntilFailureObservedAsync()
{
return failureObserved.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
IAsyncEnumerable<AudioChunk> audio,
SpeechRecognitionPipelineOptions options,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var _ in audio.WithCancellation(cancellationToken))
{
failureObserved.TrySetResult();
throw new InvalidOperationException("Configured transcription backend is unavailable.");
}
yield break;
}
}
private static byte[] Samples(params short[] samples)
{
var bytes = new byte[samples.Length * sizeof(short)];
@@ -4995,6 +5059,3 @@ public sealed class RecordingCoordinatorTests
return bytes;
}
}