forked from Manuel/meeting-assistant
ci: observe transcript append failure and Windows reader sharing
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user