using MeetingAssistant.MeetingNotes; using MeetingAssistant.Summary; namespace MeetingAssistant.Tests; public sealed class ProjectKnowledgeToolTests { [Fact] public async Task ToolsOperateOnProjectsBoundInMeetingFrontmatter() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var projectsRoot = Path.Combine(root, "Projects"); var meetingAssistantRoot = Path.Combine(projectsRoot, "MeetingAssistant"); var ignoredRoot = Path.Combine(projectsRoot, "IgnoredProject"); Directory.CreateDirectory(Path.Combine(meetingAssistantRoot, "notes")); Directory.CreateDirectory(ignoredRoot); await File.WriteAllTextAsync( Path.Combine(meetingAssistantRoot, "README.md"), "First line\nSecond alpha line\nThird beta line"); await File.WriteAllTextAsync(Path.Combine(meetingAssistantRoot, "notes", "context.md"), "Project context"); await File.WriteAllTextAsync(Path.Combine(ignoredRoot, "ignored.md"), "alpha should not be searched"); var artifacts = CreateArtifacts(root); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- title: Project Meeting attendees: [] projects: - MeetingAssistant - MissingProject transcript: "" assistant_context: "" summary: "" --- User notes. """); var tools = new MeetingSummaryTools( artifacts, new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } }); Assert.Equal("MeetingAssistant", await tools.ListProjects()); Assert.Equal("README.md\nnotes/context.md", await tools.ListProjectFiles("MeetingAssistant")); Assert.Equal("Second alpha line\nThird beta line", await tools.ReadProjectFile("MeetingAssistant", "README.md", 2, 99)); var writeResult = await tools.WriteProjectFile("MeetingAssistant", "notes/summary.md", "# Project Update"); Assert.Equal("MeetingAssistant/notes/summary.md", writeResult); Assert.Equal("# Project Update", await File.ReadAllTextAsync(Path.Combine(meetingAssistantRoot, "notes", "summary.md"))); Assert.Equal("IgnoredProject/ignored.md", await tools.WriteProjectFile("IgnoredProject", "ignored.md", "changed")); Assert.Equal("alpha should not be searched\nchanged", await File.ReadAllTextAsync(Path.Combine(ignoredRoot, "ignored.md"))); Assert.Equal( "README.md:2 Second alpha line", await tools.Search("alpha")); } [Fact] public async Task ListProjectsAcceptsScalarProjectFrontmatter() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var projectsRoot = Path.Combine(root, "Projects"); Directory.CreateDirectory(Path.Combine(projectsRoot, "MeetingAssistant")); var artifacts = CreateArtifacts(root); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- projects: MeetingAssistant --- """); var tools = new MeetingSummaryTools( artifacts, new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } }); Assert.Equal("MeetingAssistant", await tools.ListProjects()); } [Fact] public async Task WriteProjectFileSupportsAppendReplaceInsertOverwriteAndCreate() { 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"); Directory.CreateDirectory(projectRoot); var projectFile = Path.Combine(projectRoot, "notes.md"); await File.WriteAllTextAsync(projectFile, "one\ntwo\nthree\nfour"); var tools = new MeetingSummaryTools( CreateArtifacts(root), new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } }); await tools.WriteProjectFile("MeetingAssistant", "notes.md", "TWO\nTHREE", from: 2, to: 3); Assert.Equal("one\nTWO\nTHREE\nfour", await File.ReadAllTextAsync(projectFile)); await tools.WriteProjectFile("MeetingAssistant", "notes.md", "inserted", insert: 2); Assert.Equal("one\ninserted\nTWO\nTHREE\nfour", await File.ReadAllTextAsync(projectFile)); await tools.WriteProjectFile("MeetingAssistant", "notes.md", "appended"); Assert.Equal("one\ninserted\nTWO\nTHREE\nfour\nappended", await File.ReadAllTextAsync(projectFile)); await tools.WriteProjectFile("MeetingAssistant", "notes.md", "replacement", replace_file: true); Assert.Equal("replacement", await File.ReadAllTextAsync(projectFile)); await tools.WriteProjectFile("MeetingAssistant", "created/new.md", "created content"); Assert.Equal("created content", await File.ReadAllTextAsync(Path.Combine(projectRoot, "created", "new.md"))); } [Fact] public async Task WriteProjectFileRefusesMissingProjectsEscapingPathsAndAmbiguousLineArguments() { var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N")); var projectsRoot = Path.Combine(root, "Projects"); Directory.CreateDirectory(Path.Combine(projectsRoot, "MeetingAssistant")); var tools = new MeetingSummaryTools( CreateArtifacts(root), new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } }); Assert.StartsWith("Refused:", await tools.WriteProjectFile("MissingProject", "notes.md", "content")); Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "../outside.md", "content")); Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1)); Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1, to: 1, insert: 1)); Assert.StartsWith("Refused:", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "content", from: 1, to: 1, replace_file: true)); } [Fact] public async Task SearchCanBeLimitedByProjectFilePattern() { 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"); Directory.CreateDirectory(projectRoot); await File.WriteAllTextAsync(Path.Combine(projectRoot, "PROJECT.md"), "durable needle"); await File.WriteAllTextAsync(Path.Combine(projectRoot, "scratch.txt"), "scratch needle"); var artifacts = CreateArtifacts(root); Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!); await File.WriteAllTextAsync( artifacts.MeetingNotePath, """ --- projects: - MeetingAssistant --- """); var tools = new MeetingSummaryTools( artifacts, new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } }); var search = await tools.Search("needle", file_pattern: "*.md"); Assert.Contains("PROJECT.md:1 durable needle", search); Assert.DoesNotContain("scratch.txt", search); } 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", "summary.md")); } }