Files
meeting-assistant/MeetingAssistant.Tests/PastProjectMeetingSummaryToolTests.cs
codex 689df3dbcb
PR and Push Build/Test / build-and-test (push) Successful in 10m54s
Fix past meeting summary tool edge cases
2026-05-29 10:38:31 +02:00

278 lines
12 KiB
C#

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}}
""");
}
}