using MeetingAssistant.MeetingNotes; using MeetingAssistant.Summary; namespace MeetingAssistant.Tests; public sealed class PastProjectMeetingSummaryToolTests { [Fact] public async Task ListPastProjectMeetingsReturnsPagedSummariesForCurrentMeetingProjects() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X", "Project Y"); await WriteSummaryAsync(Path.Combine(summariesRoot, "current-summary.md"), "Current", "2026-05-29T09:00:00+02:00", "Project X"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-x-new.md"), "Project X New", "2026-05-28T09:00:00+02:00", "Project X"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-y.md"), "Project Y", "2026-05-27T09:00:00+02:00", "Project Y"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-z.md"), "Project Z", "2026-05-26T09:00:00+02:00", "Project Z"); await File.WriteAllTextAsync(Path.Combine(summariesRoot, "not-a-summary.md"), "No frontmatter"); artifacts = artifacts with { SummaryPath = Path.Combine(summariesRoot, "current-summary.md") }; var tools = CreateTools(artifacts, root); var firstPage = await tools.ListPastProjectMeetings(page: 1, page_size: 1); var secondPage = await tools.ListPastProjectMeetings(page: 2, page_size: 1); Assert.Contains("page 1/2", firstPage); Assert.Contains("project-x-new.md | 2026-05-28T09:00:00.0000000+02:00 | Project X New | projects: Project X", firstPage); Assert.DoesNotContain("project-y.md", firstPage); Assert.Contains("page 2/2", secondPage); Assert.Contains("project-y.md", secondPage); Assert.DoesNotContain("current-summary.md", firstPage + secondPage); Assert.DoesNotContain("project-z.md", firstPage + secondPage); Assert.DoesNotContain("not-a-summary.md", firstPage + secondPage); } [Fact] public async Task ListPastProjectMeetingsFiltersAndRejectsOutOfScopeProjects() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X", "Project Y"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-y.md"), "Project Y", "2026-05-27T09:00:00+02:00", "Project Y"); var tools = CreateTools(artifacts, root); var projectX = await tools.ListPastProjectMeetings(["Project X"]); var refused = await tools.ListPastProjectMeetings(["Project Z"]); Assert.Contains("project-x.md", projectX); Assert.DoesNotContain("project-y.md", projectX); Assert.Equal("Refused: project(s) not assigned to current meeting: Project Z.", refused); } [Fact] public async Task ListPastProjectMeetingsRejectsOutOfScopeProjectsWhenMeetingHasNoProjects() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(Path.Combine(root, "Meetings", "Summaries")); await WriteMeetingNoteAsync(artifacts.MeetingNotePath); var tools = CreateTools(artifacts, root); var refused = await tools.ListPastProjectMeetings(["Project Z"]); Assert.Equal("Refused: project(s) not assigned to current meeting: Project Z.", refused); } [Fact] public async Task ReadPastProjectMeetingSummaryReadsOnlyScopedPastSummaryFiles() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X"); await WriteSummaryAsync( Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X", "line one\nline two\nline three"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-z.md"), "Project Z", "2026-05-27T09:00:00+02:00", "Project Z"); await WriteSummaryAsync(artifacts.SummaryPath, "Current", "2026-05-29T09:00:00+02:00", "Project X"); var outside = Path.Combine(root, "outside.md"); await File.WriteAllTextAsync(outside, "outside"); var tools = CreateTools(artifacts, root); Assert.Equal("line two\nline three", await tools.ReadPastProjectMeetingSummary("project-x.md", from: 9, to: 99)); Assert.StartsWith("Refused:", await tools.ReadPastProjectMeetingSummary("project-z.md")); Assert.StartsWith("Refused:", await tools.ReadPastProjectMeetingSummary("current-summary.md")); Assert.StartsWith("Refused:", await tools.ReadPastProjectMeetingSummary(outside)); } [Fact] public async Task ReadPastProjectMeetingSummaryAcceptsSearchResultPastMeetingPrefix() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X"); await WriteSummaryAsync( Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X", "searchable past meeting"); var tools = CreateTools(artifacts, root); var search = await tools.Search("searchable", ["Project X"]); var content = await tools.ReadPastProjectMeetingSummary("past-meetings/project-x.md"); Assert.Contains("past-meetings/project-x.md:", search); Assert.Contains("searchable past meeting", search); Assert.Contains("searchable past meeting", content); } [Fact] public async Task ListPastProjectMeetingsIgnoresMalformedSummaryFrontmatter() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X"); await File.WriteAllTextAsync( Path.Combine(summariesRoot, "broken.md"), """ --- projects: [not valid --- broken """); var tools = CreateTools(artifacts, root); var result = await tools.ListPastProjectMeetings(); Assert.Contains("project-x.md", result); Assert.DoesNotContain("broken.md", result); } [Fact] public async Task ListPastProjectMeetingsClampsRequestedPageToLastPage() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X"); var tools = CreateTools(artifacts, root); var result = await tools.ListPastProjectMeetings(page: 99, page_size: 1); Assert.Contains("page 1/1", result); Assert.Contains("project-x.md", result); } [Fact] public async Task SearchIncludesProjectFilesAndScopedPastMeetingSummaries() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); var projectsRoot = Path.Combine(root, "Projects"); var summariesRoot = Path.Combine(root, "Meetings", "Summaries"); Directory.CreateDirectory(Path.Combine(projectsRoot, "Project X")); Directory.CreateDirectory(Path.Combine(projectsRoot, "Project Y")); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); Directory.CreateDirectory(summariesRoot); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X", "Project Y"); await File.WriteAllTextAsync(Path.Combine(projectsRoot, "Project X", "notes.md"), "alpha project file"); await File.WriteAllTextAsync(Path.Combine(projectsRoot, "Project Y", "notes.md"), "alpha other project"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-x.md"), "Project X", "2026-05-28T09:00:00+02:00", "Project X", "alpha past meeting"); await WriteSummaryAsync(Path.Combine(summariesRoot, "project-z.md"), "Project Z", "2026-05-27T09:00:00+02:00", "Project Z", "alpha hidden meeting"); var tools = CreateTools(artifacts, root); var projectX = await tools.Search("alpha", ["Project X"]); var refused = await tools.Search("alpha", ["Project Z"]); Assert.Contains("notes.md:1 alpha project file", projectX); Assert.Contains("past-meetings/project-x.md:", projectX); Assert.DoesNotContain("Project Y/notes.md", projectX); Assert.DoesNotContain("project-z.md", projectX); Assert.Equal("Refused: project(s) not assigned to current meeting: Project Z.", refused); } [Fact] public async Task SearchReportsInvalidPatternInsteadOfNoMatches() { var root = CreateRoot(); var artifacts = CreateArtifacts(root); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await WriteMeetingNoteAsync(artifacts.MeetingNotePath, "Project X"); var tools = CreateTools(artifacts, root); var result = await tools.Search("[", ["Project X"]); Assert.StartsWith("Search failed: invalid .NET regular expression:", result); } private static string CreateRoot() { return Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); } private static MeetingSessionArtifacts CreateArtifacts(string root) { return 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", "current-summary.md")); } private static MeetingSummaryTools CreateTools(MeetingSessionArtifacts artifacts, string root) { return new MeetingSummaryTools( artifacts, new MeetingAssistantOptions { Vault = { ProjectsFolder = Path.Combine(root, "Projects"), SummariesFolder = Path.Combine(root, "Meetings", "Summaries") } }); } private static Task WriteMeetingNoteAsync(string path, params string[] projects) { var projectLines = string.Join('\n', projects.Select(project => $"- {project}")); return File.WriteAllTextAsync( path, $""" --- title: Current Meeting projects: {projectLines} --- Current notes. """); } private static Task WriteSummaryAsync( string path, string title, string startTime, string project, string body = "summary body") { return File.WriteAllTextAsync( path, $$""" --- title: {{title}} start_time: "{{startTime}}" projects: - {{project}} --- {{body}} """); } }