ci: observe transcript append failure and Windows reader sharing
PR and Push Build/Test / build-and-test (push) Successful in 18m50s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m15s

This commit is contained in:
dh
2026-10-03 22:44:31 +02:00
parent 94a70b2005
commit 147c544496
4 changed files with 261 additions and 12 deletions
@@ -191,18 +191,21 @@ public sealed class RecordingCoordinatorTests
NullLogger<MarkdownMeetingNoteStore>.Instance);
var artifactStore = new MarkdownMeetingArtifactStore(
NullLogger<MarkdownMeetingArtifactStore>.Instance);
var writeProbe = new TranscriptWriteDiagnostic();
var transcriptStore = new DiagnosticTranscriptStore(
new VaultTranscriptStore(Options.Create(options), NullLogger<VaultTranscriptStore>.Instance),
writeProbe);
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(
new FixedSegmentStreamingTranscriptionProvider(
new TranscriptionSegment(
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(5),
"Guest-1",
"Azure returned ***** here."))),
new VaultTranscriptStore(
Options.Create(options),
NullLogger<VaultTranscriptStore>.Instance),
new DiagnosticTranscriptionProvider(
new FixedSegmentStreamingTranscriptionProvider(
new TranscriptionSegment(
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(5),
"Guest-1",
"Azure returned ***** here.")), writeProbe)),
transcriptStore,
noteStore,
new CapturingMeetingNoteOpener(),
artifactStore,
@@ -218,9 +221,45 @@ public sealed class RecordingCoordinatorTests
NullLogger<MeetingWorkflowEngine>.Instance));
var started = await coordinator.StartAsync(CancellationToken.None);
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "Azure returned"));
await coordinator.StopAsync(CancellationToken.None);
Exception? waitFailure = null;
Exception? stopFailure = null;
try
{
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
writeProbe.Record("test-audio-enqueued");
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "Azure returned"));
}
catch (Exception exception)
{
waitFailure = exception;
writeProbe.Record("test-wait-failed", exception);
}
finally
{
try
{
await coordinator.StopAsync(CancellationToken.None);
writeProbe.Record("test-stop-completed");
}
catch (Exception exception)
{
stopFailure = exception;
writeProbe.Record("test-stop-failed", exception);
}
}
if (waitFailure is TimeoutException)
{
throw new TimeoutException($"{waitFailure.Message} {writeProbe.Describe()}", waitFailure);
}
if (waitFailure is not null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(waitFailure).Throw();
}
if (stopFailure is not null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(stopFailure).Throw();
}
var content = await File.ReadAllTextAsync(started.TranscriptPath!);
Assert.Contains("[00:00:04] Guest-1: Azure returned [redacted] here.", content);
@@ -5212,6 +5251,94 @@ public sealed class RecordingCoordinatorTests
}
}
// Temporary diagnosis of Run4174. Observes public provider/store boundaries only.
private sealed class TranscriptWriteDiagnostic
{
private readonly System.Diagnostics.Stopwatch elapsed = System.Diagnostics.Stopwatch.StartNew();
private readonly ConcurrentQueue<string> events = new();
public void Record(string name, Exception? error = null)
{
events.Enqueue($"{elapsed.ElapsedMilliseconds}ms:{name}" + (error is null ? "" :
$":{error.GetType().FullName}:HResult=0x{error.HResult:X8}:{error.Message}"));
}
public string Describe() => "[DEBUG-transcript-write-4174] " + string.Join(" | ", events);
}
private sealed class DiagnosticTranscriptionProvider(
IStreamingTranscriptionProvider inner,
TranscriptWriteDiagnostic probe) : IStreamingTranscriptionProvider
{
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
IAsyncEnumerable<AudioChunk> audio,
SpeechRecognitionPipelineOptions options,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var segment in inner.TranscribeAsync(ObserveAudioAsync(audio, cancellationToken), options, cancellationToken))
{
probe.Record("fake-segment-yielded");
yield return segment;
}
}
private async IAsyncEnumerable<AudioChunk> ObserveAudioAsync(
IAsyncEnumerable<AudioChunk> audio,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var chunk in audio.WithCancellation(cancellationToken))
{
probe.Record("fake-audio-consumed");
yield return chunk;
}
}
}
private sealed class DiagnosticTranscriptStore(
ITranscriptStore inner,
TranscriptWriteDiagnostic probe) : ITranscriptStore
{
public Task<TranscriptSession> CreateSessionAsync(CancellationToken cancellationToken) =>
inner.CreateSessionAsync(cancellationToken);
public Task<TranscriptSession> CreateSessionAsync(MeetingAssistantOptions options, DateTimeOffset startedAt, CancellationToken cancellationToken) =>
inner.CreateSessionAsync(options, startedAt, cancellationToken);
public Task ReplaceLinesAsync(TranscriptSession session, IReadOnlyList<string> replacementLines, CancellationToken cancellationToken) =>
inner.ReplaceLinesAsync(session, replacementLines, cancellationToken);
public Task UpdateMetadataAsync(TranscriptSession session, MeetingSessionArtifacts artifacts, MeetingNote meetingNote, CancellationToken cancellationToken) =>
inner.UpdateMetadataAsync(session, artifacts, meetingNote, cancellationToken);
public async Task<TranscriptLineReference> AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
{
probe.Record("append-entered");
try
{
var reference = await inner.AppendLineAsync(session, line, cancellationToken);
probe.Record("append-completed");
return reference;
}
catch (Exception exception)
{
probe.Record("append-failed", exception);
throw;
}
}
public async Task ReplaceLineAsync(TranscriptSession session, TranscriptLineReference lineReference, string replacementLine, CancellationToken cancellationToken)
{
probe.Record("rewrite-entered");
try
{
await inner.ReplaceLineAsync(session, lineReference, replacementLine, cancellationToken);
probe.Record("rewrite-completed");
}
catch (Exception exception)
{
probe.Record("rewrite-failed", exception);
throw;
}
}
}
private sealed class TestSpeechRecognitionPipelineFactory : ISpeechRecognitionPipelineFactory
{
private readonly IStreamingTranscriptionProvider provider;