fix: prevent profile-switch transcription deadlock
PR and Push Build/Test / build-and-test (push) Successful in 15m17s

This commit is contained in:
2026-09-16 15:00:49 +02:00
parent cbf854fb2a
commit 93eb3d4e98
5 changed files with 73 additions and 16 deletions
@@ -2386,12 +2386,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"));
@@ -2406,13 +2411,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);
@@ -2420,10 +2428,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]
@@ -5140,6 +5159,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.");
}
}