Public Access
feat: attach calendar metadata to active meetings
PR and Push Build/Test / build-and-test (push) Successful in 15m2s
PR and Push Build/Test / build-and-test (push) Successful in 15m2s
This commit is contained in:
@@ -900,6 +900,185 @@ public sealed class RecordingCoordinatorTests
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachPromptedMetadataUpdatesTheActiveMeetingWithoutInterruptingRecording()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var transcriptStore = new InMemoryTranscriptStore();
|
||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\active-metadata-meeting.md");
|
||||
var artifactStore = new InMemoryMeetingArtifactStore();
|
||||
var workflowEngine = new TransformingAttendeeWorkflowEngine(
|
||||
"Ada Lovelace (Contoso)",
|
||||
"Ada Lovelace");
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||
transcriptStore,
|
||||
noteStore,
|
||||
new CapturingMeetingNoteOpener(),
|
||||
artifactStore,
|
||||
new InMemoryRecordedAudioStore(),
|
||||
new CapturingMeetingSummaryPipeline(),
|
||||
Options.Create(new MeetingAssistantOptions()),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
meetingMetadataProvider: new CountingMeetingMetadataProvider(),
|
||||
meetingWorkflowEngine: workflowEngine);
|
||||
var started = await coordinator.StartAsync(CancellationToken.None);
|
||||
noteStore.UpdateSavedNote(noteStore.SavedNote! with { UserNotes = "Keep this user note." });
|
||||
var promptedMetadata = new MeetingMetadata(
|
||||
"Selected architecture sync",
|
||||
["Ada Lovelace (Contoso)"],
|
||||
"Review the selected architecture",
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"));
|
||||
|
||||
var attached = await coordinator.AttachMetadataToCurrentMeetingAsync(
|
||||
promptedMetadata,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(started.IsRecording);
|
||||
Assert.True(attached.IsRecording);
|
||||
Assert.Equal(started.TranscriptPath, attached.TranscriptPath);
|
||||
Assert.Equal("Selected architecture sync", noteStore.SavedNote?.Frontmatter.Title);
|
||||
Assert.Equal(["Ada Lovelace"], noteStore.SavedNote?.Frontmatter.Attendees);
|
||||
Assert.Equal("Keep this user note.", noteStore.SavedNote?.UserNotes);
|
||||
Assert.Equal("Selected architecture sync", artifactStore.ContextMeetingNote?.Frontmatter.Title);
|
||||
Assert.Equal("Review the selected architecture", artifactStore.Agenda);
|
||||
Assert.Equal(
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"),
|
||||
artifactStore.ScheduledEnd);
|
||||
Assert.Equal(2, transcriptStore.MetadataUpdateCount);
|
||||
Assert.Equal("Selected architecture sync", transcriptStore.MetadataMeetingNote?.Frontmatter.Title);
|
||||
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachedPromptMetadataWinsOverAStandaloneLookupThatCompletesLater()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\explicit-metadata-meeting.md");
|
||||
var artifactStore = new InMemoryMeetingArtifactStore();
|
||||
var metadataProvider = new BlockingMeetingMetadataProvider(new MeetingMetadata(
|
||||
"Background calendar match",
|
||||
["Grace"],
|
||||
"Background agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T12:00:00+02:00")));
|
||||
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);
|
||||
var promptedMetadata = new MeetingMetadata(
|
||||
"Explicit prompted appointment",
|
||||
["Ada"],
|
||||
"Explicit agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"));
|
||||
|
||||
await coordinator.StartAsync(CancellationToken.None);
|
||||
await metadataProvider.WaitUntilRequestedAsync();
|
||||
await coordinator.AttachMetadataToCurrentMeetingAsync(promptedMetadata, CancellationToken.None);
|
||||
metadataProvider.Release();
|
||||
await WaitUntilAsync(() => artifactStore.States.Contains(AssistantContextState.Transcribing));
|
||||
|
||||
Assert.Equal("Explicit prompted appointment", noteStore.SavedNote?.Frontmatter.Title);
|
||||
Assert.Equal(["Ada"], noteStore.SavedNote?.Frontmatter.Attendees);
|
||||
Assert.Equal("Explicit agenda", artifactStore.Agenda);
|
||||
Assert.Equal(
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00"),
|
||||
artifactStore.ScheduledEnd);
|
||||
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttachPromptedMetadataDoesNothingAfterTheActiveRecordingStops()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var transcriptStore = new InMemoryTranscriptStore();
|
||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\stopped-metadata-meeting.md");
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||
transcriptStore,
|
||||
noteStore,
|
||||
new CapturingMeetingNoteOpener(),
|
||||
new InMemoryMeetingArtifactStore(),
|
||||
new InMemoryRecordedAudioStore(),
|
||||
new CapturingMeetingSummaryPipeline(),
|
||||
Options.Create(new MeetingAssistantOptions()),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
meetingMetadataProvider: new CountingMeetingMetadataProvider());
|
||||
|
||||
await coordinator.StartAsync(CancellationToken.None);
|
||||
var stopped = await coordinator.StopAsync(CancellationToken.None);
|
||||
var metadataUpdatesBeforeAttach = transcriptStore.MetadataUpdateCount;
|
||||
var titleBeforeAttach = noteStore.SavedNote?.Frontmatter.Title;
|
||||
|
||||
var result = await coordinator.AttachMetadataToCurrentMeetingAsync(
|
||||
new MeetingMetadata(
|
||||
"Stale prompted appointment",
|
||||
["Ada"],
|
||||
"Stale agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00")),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(stopped.IsRecording);
|
||||
Assert.False(result.IsRecording);
|
||||
Assert.Equal(titleBeforeAttach, noteStore.SavedNote?.Frontmatter.Title);
|
||||
Assert.Equal(metadataUpdatesBeforeAttach, transcriptStore.MetadataUpdateCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FailedPromptMetadataAttachmentDoesNotSuppressTheBackgroundLookup()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var noteStore = new InMemoryMeetingNoteStore("C:\\Vault\\Meetings\\Notes\\failed-attach-metadata-meeting.md");
|
||||
var artifactStore = new InMemoryMeetingArtifactStore(failFirstMetadataUpdate: true);
|
||||
var metadataProvider = new BlockingMeetingMetadataProvider(new MeetingMetadata(
|
||||
"Background calendar fallback",
|
||||
["Grace"],
|
||||
"Background fallback agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T12:00:00+02:00")));
|
||||
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);
|
||||
|
||||
await coordinator.StartAsync(CancellationToken.None);
|
||||
await metadataProvider.WaitUntilRequestedAsync();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
coordinator.AttachMetadataToCurrentMeetingAsync(
|
||||
new MeetingMetadata(
|
||||
"Prompt attachment that fails",
|
||||
["Ada"],
|
||||
"Prompt agenda",
|
||||
DateTimeOffset.Parse("2026-05-19T11:00:00+02:00")),
|
||||
CancellationToken.None));
|
||||
metadataProvider.Release();
|
||||
await WaitUntilAsync(() => artifactStore.States.Contains(AssistantContextState.Transcribing));
|
||||
|
||||
Assert.Equal("Background calendar fallback", noteStore.SavedNote?.Frontmatter.Title);
|
||||
Assert.Equal(["Grace"], noteStore.SavedNote?.Frontmatter.Attendees);
|
||||
Assert.Equal("Background fallback agenda", artifactStore.Agenda);
|
||||
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartTransformsMetadataAttendeesBeforeWritingNote()
|
||||
{
|
||||
@@ -2825,6 +3004,8 @@ public sealed class RecordingCoordinatorTests
|
||||
|
||||
public MeetingNote? MetadataMeetingNote { get; private set; }
|
||||
|
||||
public int MetadataUpdateCount { get; private set; }
|
||||
|
||||
public Task ReplaceLinesAsync(
|
||||
TranscriptSession session,
|
||||
IReadOnlyList<string> replacementLines,
|
||||
@@ -2840,6 +3021,7 @@ public sealed class RecordingCoordinatorTests
|
||||
MeetingNote meetingNote,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
MetadataUpdateCount++;
|
||||
MetadataMeetingNote = meetingNote;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -3266,10 +3448,14 @@ public sealed class RecordingCoordinatorTests
|
||||
private sealed class InMemoryMeetingArtifactStore : IMeetingArtifactStore
|
||||
{
|
||||
private readonly bool createAssistantContextFile;
|
||||
private bool failNextMetadataUpdate;
|
||||
|
||||
public InMemoryMeetingArtifactStore(bool createAssistantContextFile = false)
|
||||
public InMemoryMeetingArtifactStore(
|
||||
bool createAssistantContextFile = false,
|
||||
bool failFirstMetadataUpdate = false)
|
||||
{
|
||||
this.createAssistantContextFile = createAssistantContextFile;
|
||||
failNextMetadataUpdate = failFirstMetadataUpdate;
|
||||
}
|
||||
|
||||
public MeetingSessionArtifacts? CreatedArtifacts { get; private set; }
|
||||
@@ -3327,6 +3513,12 @@ public sealed class RecordingCoordinatorTests
|
||||
DateTimeOffset? scheduledEnd,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (failNextMetadataUpdate)
|
||||
{
|
||||
failNextMetadataUpdate = false;
|
||||
throw new InvalidOperationException("Metadata artifact update failed.");
|
||||
}
|
||||
|
||||
ContextMeetingNote = meetingNote;
|
||||
Agenda = agenda;
|
||||
ScheduledEnd = scheduledEnd;
|
||||
@@ -3431,6 +3623,7 @@ public sealed class RecordingCoordinatorTests
|
||||
private sealed class BlockingMeetingMetadataProvider : IMeetingMetadataProvider
|
||||
{
|
||||
private readonly MeetingMetadata metadata;
|
||||
private readonly TaskCompletionSource requested = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly TaskCompletionSource release = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public BlockingMeetingMetadataProvider(MeetingMetadata metadata)
|
||||
@@ -3443,10 +3636,16 @@ public sealed class RecordingCoordinatorTests
|
||||
release.TrySetResult();
|
||||
}
|
||||
|
||||
public Task WaitUntilRequestedAsync()
|
||||
{
|
||||
return requested.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public async Task<MeetingMetadata?> GetCurrentMeetingAsync(
|
||||
DateTimeOffset startedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
requested.TrySetResult();
|
||||
await release.Task.WaitAsync(cancellationToken);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user