Public Access
Archive completed meeting assistant changes
PR and Push Build/Test / build-and-test (push) Successful in 9m19s
PR and Push Build/Test / build-and-test (push) Successful in 9m19s
This commit is contained in:
@@ -784,6 +784,88 @@ public sealed class RecordingCoordinatorTests
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartWithPromptedMetadataBypassesLookupAndRunsWorkflowWithMetadata()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\prompted-metadata-meeting.md");
|
||||
var artifactStore = new InMemoryMeetingArtifactStore();
|
||||
var metadataProvider = new CountingMeetingMetadataProvider();
|
||||
var workflowEngine = new MetadataObservingWorkflowEngine(() => noteStore.SavedNote);
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||
new InMemoryTranscriptStore(),
|
||||
noteStore,
|
||||
new CapturingMeetingNoteOpener(),
|
||||
artifactStore,
|
||||
new InMemoryRecordedAudioStore(),
|
||||
new CapturingMeetingSummaryPipeline(),
|
||||
Options.Create(new MeetingAssistantOptions()),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
meetingMetadataProvider: metadataProvider,
|
||||
meetingWorkflowEngine: workflowEngine);
|
||||
var promptedMetadata = new MeetingMetadata(
|
||||
"Prompted Architecture Sync",
|
||||
["Ada <ada@example.com>"],
|
||||
"Prompted agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"));
|
||||
|
||||
await coordinator.StartFromPromptAsync(promptedMetadata, CancellationToken.None);
|
||||
|
||||
await WaitUntilAsync(() => workflowEngine.ObservedEvents.Any(entry =>
|
||||
entry.Type == MeetingWorkflowEventType.StateTransition &&
|
||||
entry.Title == "Prompted Architecture Sync"));
|
||||
Assert.Equal(0, metadataProvider.CallCount);
|
||||
Assert.Equal("Prompted Architecture Sync", noteStore.SavedNote?.Frontmatter.Title);
|
||||
Assert.Equal(["Ada <ada@example.com>"], noteStore.SavedNote?.Frontmatter.Attendees);
|
||||
Assert.Equal("Prompted agenda", artifactStore.Agenda);
|
||||
Assert.Equal(DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"), artifactStore.ScheduledEnd);
|
||||
Assert.Collection(
|
||||
workflowEngine.ObservedEvents.Where(entry =>
|
||||
entry.Type is MeetingWorkflowEventType.Created or MeetingWorkflowEventType.StateTransition),
|
||||
created =>
|
||||
{
|
||||
Assert.Equal(MeetingWorkflowEventType.Created, created.Type);
|
||||
Assert.StartsWith("Meeting ", created.Title, StringComparison.Ordinal);
|
||||
},
|
||||
transition =>
|
||||
{
|
||||
Assert.Equal(MeetingWorkflowEventType.StateTransition, transition.Type);
|
||||
Assert.Equal("Prompted Architecture Sync", transition.Title);
|
||||
Assert.Equal(AssistantContextState.CollectingMetadata, transition.FromState);
|
||||
Assert.Equal(AssistantContextState.Transcribing, transition.ToState);
|
||||
});
|
||||
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartFromPromptWithoutMetadataBypassesLookup()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var metadataProvider = new CountingMeetingMetadataProvider();
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||
new InMemoryTranscriptStore(),
|
||||
new InMemoryMeetingNoteStore(),
|
||||
new CapturingMeetingNoteOpener(),
|
||||
new InMemoryMeetingArtifactStore(),
|
||||
new InMemoryRecordedAudioStore(),
|
||||
new CapturingMeetingSummaryPipeline(),
|
||||
Options.Create(new MeetingAssistantOptions()),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
meetingMetadataProvider: metadataProvider);
|
||||
|
||||
await coordinator.StartFromPromptAsync(null, CancellationToken.None);
|
||||
|
||||
await Task.Delay(100);
|
||||
Assert.Equal(0, metadataProvider.CallCount);
|
||||
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartCanonicalizesOutlookMeetingAttendeesBeforeWritingNote()
|
||||
{
|
||||
@@ -1353,7 +1435,9 @@ public sealed class RecordingCoordinatorTests
|
||||
Assert.Equal([null, "english"], pipelineFactory.ProfileNames);
|
||||
Assert.Contains(transcriptStore.Segments, segment =>
|
||||
segment.Speaker == "System" &&
|
||||
segment.Text.Contains("Transcription profile changed to english", StringComparison.Ordinal));
|
||||
segment.Text.Contains(
|
||||
"Transcription profile changed to english. Speaker recognition and identities reset.",
|
||||
StringComparison.Ordinal));
|
||||
Assert.Null(summaryPipeline.Artifacts);
|
||||
await WaitUntilAsync(() => metadataProvider.CallCount == 1);
|
||||
|
||||
@@ -2121,6 +2205,44 @@ public sealed class RecordingCoordinatorTests
|
||||
Assert.NotNull(summaryPipeline.Artifacts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopProcessesMeetingNoteImageEmbedsBeforeSummarizing()
|
||||
{
|
||||
var audioSource = new CapturedChunkThenCancelAudioSource(new AudioChunk([1, 0, 2, 0], 16000, 1));
|
||||
var artifactStore = new InMemoryMeetingArtifactStore();
|
||||
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
||||
var screenshotService = new BlockingMeetingScreenshotService();
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new FinalSegmentOnAudioCompletionProvider()),
|
||||
new InMemoryTranscriptStore(),
|
||||
new InMemoryMeetingNoteStore(),
|
||||
new CapturingMeetingNoteOpener(),
|
||||
artifactStore,
|
||||
new InMemoryRecordedAudioStore(),
|
||||
summaryPipeline,
|
||||
Options.Create(CreateOptionsWithoutFinalizer()),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
screenshotService: screenshotService,
|
||||
meetingNoteImageOcrService: screenshotService);
|
||||
|
||||
await coordinator.StartAsync(CancellationToken.None);
|
||||
await audioSource.WaitUntilCapturedAsync();
|
||||
var stop = coordinator.StopAsync(CancellationToken.None);
|
||||
|
||||
await screenshotService.WaitUntilMeetingNoteImageProcessingStartedAsync();
|
||||
await screenshotService.WaitUntilOcrWaitStartedAsync();
|
||||
|
||||
Assert.DoesNotContain(AssistantContextState.Summarizing, artifactStore.States);
|
||||
Assert.Null(summaryPipeline.Artifacts);
|
||||
|
||||
screenshotService.ReleaseOcrWait();
|
||||
await stop;
|
||||
|
||||
Assert.Contains(AssistantContextState.Summarizing, artifactStore.States);
|
||||
Assert.NotNull(summaryPipeline.Artifacts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopMarksAssistantContextErrorWhenSummaryFails()
|
||||
{
|
||||
@@ -2866,6 +2988,46 @@ public sealed class RecordingCoordinatorTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MetadataObservingWorkflowEngine : IMeetingWorkflowEngine
|
||||
{
|
||||
private readonly Func<MeetingNote?> getMeetingNote;
|
||||
|
||||
public MetadataObservingWorkflowEngine(Func<MeetingNote?> getMeetingNote)
|
||||
{
|
||||
this.getMeetingNote = getMeetingNote;
|
||||
}
|
||||
|
||||
public List<ObservedWorkflowEvent> ObservedEvents { get; } = [];
|
||||
|
||||
public Task RunAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var note = getMeetingNote();
|
||||
ObservedEvents.Add(new ObservedWorkflowEvent(
|
||||
workflowEvent.Type,
|
||||
note?.Frontmatter.Title,
|
||||
workflowEvent.FromState,
|
||||
workflowEvent.ToState));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> TransformTranscriptLineAsync(
|
||||
MeetingWorkflowEvent workflowEvent,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(workflowEvent.TranscriptLineText ?? "");
|
||||
}
|
||||
|
||||
public sealed record ObservedWorkflowEvent(
|
||||
MeetingWorkflowEventType Type,
|
||||
string? Title,
|
||||
AssistantContextState? FromState,
|
||||
AssistantContextState? ToState);
|
||||
}
|
||||
|
||||
private sealed class FailingFirstTranscriptLineWorkflowEngine : IMeetingWorkflowEngine
|
||||
{
|
||||
public Task RunAsync(
|
||||
@@ -3003,12 +3165,14 @@ public sealed class RecordingCoordinatorTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockingMeetingScreenshotService : IMeetingScreenshotService
|
||||
private sealed class BlockingMeetingScreenshotService : IMeetingScreenshotService, IMeetingNoteImageOcrService
|
||||
{
|
||||
private readonly TaskCompletionSource waitStarted =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly TaskCompletionSource releaseWait =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly TaskCompletionSource meetingNoteImageProcessingStarted =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public Task<MeetingScreenshotCaptureResult> CaptureAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
@@ -3041,6 +3205,20 @@ public sealed class RecordingCoordinatorTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<MeetingNoteImageOcrQueueResult> ProcessMeetingNoteImageEmbedsAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
meetingNoteImageProcessingStarted.TrySetResult();
|
||||
return Task.FromResult(new MeetingNoteImageOcrQueueResult(0));
|
||||
}
|
||||
|
||||
public Task WaitUntilMeetingNoteImageProcessingStartedAsync()
|
||||
{
|
||||
return meetingNoteImageProcessingStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public Task WaitUntilOcrWaitStartedAsync()
|
||||
{
|
||||
return waitStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
@@ -3683,9 +3861,9 @@ public sealed class RecordingCoordinatorTests
|
||||
return Task.FromResult<IReadOnlyList<OfflineTranscriptionBacklogItem>>(Items.ToList());
|
||||
}
|
||||
|
||||
public Task CompleteAsync(string id, CancellationToken cancellationToken)
|
||||
public Task CompleteAsync(OfflineTranscriptionBacklogItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
Items.RemoveAll(item => string.Equals(item.Id, id, StringComparison.Ordinal));
|
||||
Items.RemoveAll(existing => string.Equals(existing.Id, item.Id, StringComparison.Ordinal));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user