using MeetingAssistant.MeetingNotes; using MeetingAssistant.Summary; using MeetingAssistant.Transcription; using Microsoft.Extensions.Options; namespace MeetingAssistant.Tests; public sealed class MeetingSummaryToolTests { [Fact] public async Task ToolsReadMeetingInputsAndWriteSummaryNote() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting start_time: "2026-05-20T10:00:00.0000000+02:00" end_time: "2026-05-20T10:30:00.0000000+02:00" attendees: - Ada projects: - Meeting Assistant transcript: "[[../Transcripts/transcript|Transcript]]" assistant_context: "[[../Assistant Context/context|Assistant Context]]" summary: "[[../Summaries/summary|Summary]]" --- User note line. """); await File.WriteAllTextAsync(artifacts.TranscriptPath, "Ada: Transcript line."); await File.WriteAllTextAsync(artifacts.AssistantContextPath, "Context line."); var tools = new MeetingSummaryTools(artifacts); Assert.Equal("Ada: Transcript line.", await tools.ReadTranscript()); Assert.Contains("title: Meeting", await tools.ReadMeetingNote()); Assert.Equal("Context line.", await tools.ReadContext()); Assert.Equal("User note line.", await tools.ReadUserNotes()); Assert.Equal("", await tools.ReadGlossary()); Assert.False(tools.SummaryWasWritten); var result = await tools.WriteSummary("# Summary\n\n- Done.", "Done items"); Assert.Equal(Path.GetFileName(artifacts.SummaryPath), result); Assert.True(tools.SummaryWasWritten); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("title: Meeting", summary); Assert.Contains("oneliner: Done items", summary); Assert.Contains("start_time: \"2026-05-20T10:00:00.0000000+02:00\"", summary); Assert.Contains("end_time: \"2026-05-20T10:30:00.0000000+02:00\"", summary); Assert.Contains("attendees:", summary); Assert.Contains("- Ada", summary); Assert.Contains("projects:", summary); Assert.Contains("- Meeting Assistant", summary); Assert.Contains("meeting: \"[[../Notes/meeting|Meeting Note]]\"", summary); Assert.Contains("transcript: \"[[../Transcripts/transcript|Transcript]]\"", summary); Assert.Contains("assistant_context: \"[[../Assistant Context/context|Assistant Context]]\"", summary); Assert.DoesNotContain("summary:", summary); Assert.Contains("# Summary\n\n- Done.", summary); } [Fact] public async Task WriteSummaryRenamesMeetingWhenTitleIsProvided() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting 2026-06-03 07:26 start_time: "2026-06-03T07:26:00.0000000+02:00" attendees: [] projects: [] --- User note. """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteSummary("# Summary", "Renamed summary", "Notification Safeguard Test"); var meetingNote = await File.ReadAllTextAsync(artifacts.MeetingNotePath); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("title: Notification Safeguard Test", meetingNote); Assert.Contains("title: Notification Safeguard Test", summary); Assert.Contains("User note.", meetingNote); } [Fact] public async Task WriteSummaryKeepsMeetingTitleWhenTitleIsOmittedOrBlank() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Architecture Review attendees: [] projects: [] --- """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteSummary("# Summary", "Original title summary"); await tools.WriteSummary("# Summary", "Blank title summary", " "); var meetingNote = await File.ReadAllTextAsync(artifacts.MeetingNotePath); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("title: Architecture Review", meetingNote); Assert.Contains("title: Architecture Review", summary); Assert.DoesNotContain("Meeting Summary", summary); } [Fact] public async Task WriteSummaryPreservesExistingSummaryAttendees() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.SummaryPath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting attendees: - Ada - Grace --- """); await File.WriteAllTextAsync( artifacts.SummaryPath, """ --- title: Existing attendees: - Preserved --- Old summary. """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteSummary("# Summary", "Preserved attendee summary"); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("attendees:", summary); Assert.Contains("- Preserved", summary); Assert.DoesNotContain("- Ada", summary); Assert.DoesNotContain("- Grace", summary); } [Fact] public async Task WriteSummaryCopiesCurrentMeetingProjectsOverExistingSummaryProjects() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.SummaryPath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting projects: - Current Project - Second Project --- """); await File.WriteAllTextAsync( artifacts.SummaryPath, """ --- title: Existing projects: - Stale Project --- Old summary. """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteSummary("# Summary", "Current project summary"); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("projects:", summary); Assert.Contains("- Current Project", summary); Assert.Contains("- Second Project", summary); Assert.DoesNotContain("- Stale Project", summary); } [Fact] public async Task WriteSummaryCopiesOnlyDistinctNonBlankMeetingProjects() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting projects: - " Current Project " - current project - "" - Second Project --- """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteSummary("# Summary", "Project summary"); var summary = await File.ReadAllTextAsync(artifacts.SummaryPath); Assert.Contains("projects:", summary); Assert.Contains("- Current Project", summary); Assert.Contains("- Second Project", summary); Assert.DoesNotContain("- current project", summary); Assert.DoesNotContain("- \"\"", summary); } [Fact] public async Task ToolsCanAddAndRemoveMeetingAttendees() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting attendees: - Ada - Grace Hopper projects: - Meeting Assistant transcript: "[[../Transcripts/transcript|Transcript]]" assistant_context: "[[../Assistant Context/context|Assistant Context]]" summary: "[[../Summaries/summary|Summary]]" --- User note line. """); var tools = new MeetingSummaryTools(artifacts); Assert.Equal("Added attendee Manuel.", await tools.AddAttendee("Manuel")); Assert.Equal("Attendee Ada already exists.", await tools.AddAttendee("Ada")); Assert.Equal("Removed attendee Grace Hopper.", await tools.RemoveAttendee("Grace Hopper")); var meetingNote = await File.ReadAllTextAsync(artifacts.MeetingNotePath); Assert.Contains("- Ada", meetingNote); Assert.Contains("- Manuel", meetingNote); Assert.DoesNotContain("Grace Hopper", meetingNote); Assert.Contains("User note line.", meetingNote); Assert.Contains("transcript: \"[[../Transcripts/transcript|Transcript]]\"", meetingNote); } [Fact] public async Task ToolsTransformAddedAttendeeBeforeWritingMeetingNote() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting attendees: [] projects: [] transcript: "[[../Transcripts/transcript|Transcript]]" assistant_context: "[[../Assistant Context/context|Assistant Context]]" summary: "[[../Summaries/summary|Summary]]" --- User note line. """); var workflowEngine = new TransformingAttendeeWorkflowEngine("Ada Lovelace (Contoso)", "Ada Lovelace"); var tools = new MeetingSummaryTools( artifacts, new MeetingAssistantOptions(), meetingWorkflowEngine: workflowEngine); Assert.Equal("Added attendee Ada Lovelace.", await tools.AddAttendee("Ada Lovelace (Contoso)")); var meetingNote = await File.ReadAllTextAsync(artifacts.MeetingNotePath); Assert.Contains("- Ada Lovelace", meetingNote); Assert.DoesNotContain("Ada Lovelace (Contoso)", meetingNote); Assert.Equal(["Ada Lovelace (Contoso)"], workflowEngine.AttendeeRequests); } [Fact] public async Task ToolsOverrideSpeakerInTranscriptAndRecordOverride() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Guest-01: Hello from Sabrina. [00:00:02] Guest-02: Hello from someone else. """); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- state: summarizing --- Existing context. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.OverrideSpeaker("Guest-01", "Sabrina"); Assert.Equal("Overrode speaker Guest-01 with Sabrina.", result); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Sabrina: Hello from Sabrina.", transcript); Assert.Contains("[00:00:02] Guest-02: Hello from someone else.", transcript); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("state: summarizing", context); Assert.Contains("meeting-assistant:speaker-override", context); Assert.Contains("\"from\":\"Guest-01\"", context); Assert.Contains("\"to\":\"Sabrina\"", context); } [Fact] public async Task ToolsRefuseOverrideWhenAssistantContextIsMissing() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Guest-01: Hello from Sabrina. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.OverrideSpeaker("Guest-01", "Sabrina"); Assert.StartsWith("Refused: assistant context file does not exist", result, StringComparison.Ordinal); Assert.False(File.Exists(artifacts.AssistantContextPath)); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Guest-01: Hello from Sabrina.", transcript); } [Fact] public async Task ToolsRefuseOverrideWhenReplacementSpeakerAlreadyExistsWithoutMerge() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Guest-01: I am probably Sabrina. [00:00:02] Sabrina: I am already in the transcript. """); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- state: summarizing --- Existing context. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.OverrideSpeaker("Guest-01", "Sabrina"); Assert.Equal( "Refused: transcript already contains speaker Sabrina. Call override_speaker with merge=true only when you are certain both speakers are the same identity.", result); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Guest-01: I am probably Sabrina.", transcript); Assert.Contains("[00:00:02] Sabrina: I am already in the transcript.", transcript); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.DoesNotContain("meeting-assistant:speaker-override", context); } [Fact] public async Task ToolsMergeOverrideWhenReplacementSpeakerAlreadyExistsWithMerge() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Guest-01: I am Sabrina. [00:00:02] Sabrina: I am already in the transcript. """); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- state: summarizing --- Existing context. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.OverrideSpeaker("Guest-01", "Sabrina", merge: true); Assert.Equal("Merged speaker Guest-01 into Sabrina.", result); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Sabrina: I am Sabrina.", transcript); Assert.Contains("[00:00:02] Sabrina: I am already in the transcript.", transcript); Assert.DoesNotContain("Guest-01:", transcript); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("meeting-assistant:speaker-override", context); Assert.Contains("\"from\":\"Guest-01\"", context); Assert.Contains("\"to\":\"Sabrina\"", context); Assert.Contains("\"merge\":true", context); } [Fact] public async Task ToolsDeleteIdentityInTranscriptAndRecordDeletion() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Sabrina: This was incorrectly matched. [00:00:02] Removed-1: An earlier removed speaker. [00:00:03] Guest-02: Hello from someone else. [00:00:04] Sabrina: Another incorrect line. """); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- state: summarizing --- Existing context. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.DeleteIdentity("Sabrina"); Assert.Equal("Deleted identity Sabrina and relabeled transcript speaker mentions to Removed-2.", result); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Removed-2: This was incorrectly matched.", transcript); Assert.Contains("[00:00:02] Removed-1: An earlier removed speaker.", transcript); Assert.Contains("[00:00:03] Guest-02: Hello from someone else.", transcript); Assert.Contains("[00:00:04] Removed-2: Another incorrect line.", transcript); Assert.DoesNotContain("Sabrina:", transcript); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("state: summarizing", context); Assert.Contains("meeting-assistant:speaker-identity-deletion", context); Assert.Contains("\"identity\":\"Sabrina\"", context); Assert.Contains("\"replacement\":\"Removed-2\"", context); } [Fact] public async Task ToolsRefuseDeleteIdentityWhenAssistantContextIsMissing() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); await File.WriteAllTextAsync( artifacts.TranscriptPath, """ # Meeting Transcript [00:00:01] Sabrina: This was incorrectly matched. """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.DeleteIdentity("Sabrina"); Assert.StartsWith("Refused: assistant context file does not exist", result, StringComparison.Ordinal); Assert.False(File.Exists(artifacts.AssistantContextPath)); var transcript = await File.ReadAllTextAsync(artifacts.TranscriptPath); Assert.Contains("[00:00:01] Sabrina: This was incorrectly matched.", transcript); } [Fact] public async Task ReadToolsSupportClampedLineRanges() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); var glossaryPath = Path.Combine(root, "Meetings", "dictation-words.md"); var projectRoot = Path.Combine(root, "Projects", "Project A"); var projectFile = Path.Combine(projectRoot, "notes.md"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); Directory.CreateDirectory(Path.GetDirectoryName(glossaryPath)!); Directory.CreateDirectory(projectRoot); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- projects: - Project A --- note one note two note three """); await File.WriteAllTextAsync(artifacts.TranscriptPath, "transcript one\ntranscript two\ntranscript three"); await File.WriteAllTextAsync(artifacts.AssistantContextPath, "context one\ncontext two\ncontext three"); await File.WriteAllTextAsync(glossaryPath, "word one\nword two\nword three"); await File.WriteAllTextAsync(projectFile, "project one\nproject two\nproject three"); var tools = new MeetingSummaryTools( artifacts, new MeetingAssistantOptions { Vault = { DictationWordsPath = glossaryPath, ProjectsFolder = Path.Combine(root, "Projects") } }); Assert.Equal("transcript two", await tools.ReadTranscript(from: 2, to: 2)); Assert.Equal("context two\ncontext three", await tools.ReadContext(from: 2, to: 9)); Assert.Equal("note two\nnote three", await tools.ReadUserNotes(from: 2, to: 99)); Assert.Equal("word two", await tools.ReadGlossary(from: 2, to: 2)); Assert.Equal("project two", await tools.ReadProjectFile("Project A", "notes.md", from: 2, to: 2)); Assert.Equal("", await tools.ReadTranscript(from: 3, to: 2)); } [Fact] public async Task ReadToolsSupportTail() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!); await File.WriteAllTextAsync(artifacts.TranscriptPath, "one\ntwo\nthree"); var tools = new MeetingSummaryTools(artifacts); Assert.Equal("two\nthree", await tools.ReadTranscript(tail: 2)); } [Fact] public async Task ToolsWriteAssistantContextBodyWithLineModes() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- meeting: "[[../Notes/meeting|Meeting Note]]" transcript: "[[../Transcripts/transcript|Transcript]]" summary: "[[../Summaries/summary|Summary]]" state: summarizing --- line one line two """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteContext("appended"); await tools.WriteContext("inserted", insert: 2); await tools.WriteContext("replacement", from: 1, to: 1); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("state: summarizing", context); Assert.Contains("replacement\ninserted\nline two\nappended", context); } [Fact] public async Task ToolsReplaceAssistantContextBodyOnlyWhenRequested() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- state: summarizing --- line one """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteContext("replacement", replace_file: true); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("state: summarizing", context); Assert.Contains("replacement", context); Assert.DoesNotContain("line one", context); } [Fact] public async Task ToolsRefuseWriteContextWhenAssistantContextIsMissing() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); var tools = new MeetingSummaryTools(artifacts); var result = await tools.WriteContext("new context"); Assert.StartsWith("Refused: assistant context file does not exist", result, StringComparison.Ordinal); Assert.False(File.Exists(artifacts.AssistantContextPath)); } [Fact] public async Task ToolsWriteAssistantContextStripsAccidentalFrontmatterFromReplacementBody() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync( artifacts.AssistantContextPath, """ --- meeting: "[[../Notes/meeting|Meeting Note]]" transcript: "[[../Transcripts/transcript|Transcript]]" summary: "[[../Summaries/summary|Summary]]" state: summarizing --- line one """); var tools = new MeetingSummaryTools(artifacts); await tools.WriteContext( """ --- state: finished --- # Assistant Context ## Live Context """, replace_file: true); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("state: summarizing", context); Assert.DoesNotContain("state: finished", context); Assert.Equal(2, context.Split("---", StringSplitOptions.None).Length - 1); Assert.Contains("# Assistant Context", context); } [Fact] public async Task ExternalAgentWritesAreAppendedToAssistantContextAsDiffs() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var projectsRoot = Path.Combine(root, "Projects"); var projectRoot = Path.Combine(projectsRoot, "MeetingAssistant"); var projectFile = Path.Combine(projectRoot, "notes.md"); var dictionaryPath = Path.Combine(root, "Meetings", "Dictionary.md"); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(projectRoot); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.AssistantContextPath)!); await File.WriteAllTextAsync(projectFile, "one\ntwo\nthree"); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting projects: - MeetingAssistant --- """); await File.WriteAllTextAsync(artifacts.AssistantContextPath, "Context before."); var options = new MeetingAssistantOptions { Vault = { DictationWordsPath = dictionaryPath, ProjectsFolder = projectsRoot } }; var audit = new SummaryAgentWriteAudit(artifacts); var tools = new MeetingSummaryTools( artifacts, options, new MarkdownDictationWordStore(Options.Create(options)), audit); await tools.WriteContext("owned context"); await tools.WriteSummary("owned summary", "Owned summary"); await tools.WriteProjectFile("MeetingAssistant", "notes.md", "TWO", from: 2, to: 2); await tools.AddDictationWord("PBI"); await audit.AppendToAssistantContextAsync(CancellationToken.None); var context = await File.ReadAllTextAsync(artifacts.AssistantContextPath); Assert.Contains("## Summary Agent External File Changes", context); Assert.Contains(projectFile, context); Assert.Contains("- two", context); Assert.Contains("+ TWO", context); Assert.Contains(dictionaryPath, context); Assert.Contains("+ - PBI", context); Assert.DoesNotContain("owned summary", context); Assert.DoesNotContain("- Context before.", context); } [Fact] public async Task WriteSummaryRefusesOneLinerWithLineBreaks() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var artifacts = new MeetingSessionArtifacts( MeetingNotePath: Path.Combine(root, "Meetings", "Notes", "meeting.md"), TranscriptPath: Path.Combine(root, "Meetings", "Transcripts", "transcript.md"), AssistantContextPath: Path.Combine(root, "Meetings", "Assistant Context", "context.md"), SummaryPath: Path.Combine(root, "Meetings", "Summaries", "summary.md")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Meeting --- """); var tools = new MeetingSummaryTools(artifacts); var result = await tools.WriteSummary("# Summary", "First line\nsecond line"); Assert.Equal("Refused: oneliner must be a single line.", result); Assert.False(tools.SummaryWasWritten); Assert.False(File.Exists(artifacts.SummaryPath)); } }