Public Access
PR and Push Build/Test / build-and-test (push) Successful in 12m0s
Append successful offline recovery with a duplicate-warning marker and retain failed replay sources. Offer newly available microphones through confirmation notifications. Add project discovery metadata and blocking association approval, plus nonblocking workflow requests that start a separate settings-agent conversation. Include regression tests, OpenSpec changes, and verified local Release startup.
286 lines
14 KiB
C#
286 lines
14 KiB
C#
using MeetingAssistant.MeetingNotes;
|
|
using MeetingAssistant.Summary;
|
|
using System.Text.Json;
|
|
|
|
namespace MeetingAssistant.Tests;
|
|
|
|
public sealed class ProjectKnowledgeToolTests
|
|
{
|
|
[Theory]
|
|
[InlineData("name: Incomplete project")]
|
|
[InlineData("description: Missing name")]
|
|
[InlineData("name: ' '\ndescription: Missing name")]
|
|
public async Task CatalogUsesLegacyFallbackForIncompleteMetadata(string frontmatter)
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
|
var projectRoot = Path.Combine(root, "Projects", "Alpha");
|
|
Directory.CreateDirectory(projectRoot);
|
|
await File.WriteAllTextAsync(Path.Combine(projectRoot, "PROJECT.md"), $"---\n{frontmatter}\n---\nNotes");
|
|
var tools = new MeetingSummaryTools(CreateArtifacts(root),
|
|
new MeetingAssistantOptions { Vault = { ProjectsFolder = Path.Combine(root, "Projects") } });
|
|
|
|
using var catalog = JsonDocument.Parse(await tools.ListProjects());
|
|
|
|
var project = Assert.Single(catalog.RootElement.EnumerateArray());
|
|
Assert.Equal("Alpha", project.GetProperty("displayname").GetString());
|
|
Assert.Equal("", project.GetProperty("description").GetString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProjectMetadataWriteEnforcesDescriptionLimitAndPreservesOtherContent()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
|
var projectRoot = Path.Combine(root, "Projects", "Alpha");
|
|
Directory.CreateDirectory(projectRoot);
|
|
var artifacts = CreateArtifacts(root);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!);
|
|
await File.WriteAllTextAsync(artifacts.MeetingNotePath, "---\nprojects: Alpha\n---");
|
|
var tools = new MeetingSummaryTools(artifacts,
|
|
new MeetingAssistantOptions { Vault = { ProjectsFolder = Path.Combine(root, "Projects") } });
|
|
var original = $"---\nname: Alpha Platform\ndescription: {new string('a', 256)}\nowner: Ada\n---\nUser-authored project notes.";
|
|
Assert.Equal("Alpha/PROJECT.md", await tools.WriteProjectFile("Alpha", "PROJECT.md", original));
|
|
|
|
var rejected = await tools.WriteProjectFile("Alpha", "PROJECT.md",
|
|
$"description: {new string('b', 257)}", from: 3, to: 3);
|
|
|
|
Assert.StartsWith("Refused:", rejected);
|
|
Assert.Equal(original, await File.ReadAllTextAsync(Path.Combine(projectRoot, "PROJECT.md")));
|
|
Assert.StartsWith("Refused:", await tools.WriteProjectFile("Alpha", "PROJECT.md", "No metadata", replace_file: true));
|
|
Assert.Equal("Alpha/PROJECT.md", await tools.WriteProjectFile("Alpha", "PROJECT.md",
|
|
"description: Updated project purpose", from: 3, to: 3));
|
|
var updated = await tools.ReadProjectFile("Alpha", "PROJECT.md");
|
|
Assert.Contains("owner: Ada", updated);
|
|
Assert.Contains("User-authored project notes.", updated);
|
|
Assert.Contains("Updated project purpose", await tools.ListProjects());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CatalogListsAllProjectsWithMetadataAndLegacyFallbackWithoutGrantingReadAccess()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
|
var projectsRoot = Path.Combine(root, "Projects");
|
|
Directory.CreateDirectory(Path.Combine(projectsRoot, "Alpha"));
|
|
Directory.CreateDirectory(Path.Combine(projectsRoot, "Legacy"));
|
|
Directory.CreateDirectory(Path.Combine(projectsRoot, "Malformed"));
|
|
await File.WriteAllTextAsync(Path.Combine(projectsRoot, "Alpha", "PROJECT.md"),
|
|
"---\nname: Alpha Platform\ndescription: Meeting automation\n---\nPrivate project notes");
|
|
await File.WriteAllTextAsync(Path.Combine(projectsRoot, "Alpha", "AGENTS.md"), "Private instructions");
|
|
await File.WriteAllTextAsync(Path.Combine(projectsRoot, "Malformed", "PROJECT.md"), "---\nname: [broken\n---");
|
|
var tools = new MeetingSummaryTools(CreateArtifacts(root),
|
|
new MeetingAssistantOptions { Vault = { ProjectsFolder = projectsRoot } });
|
|
|
|
var catalog = await tools.ListProjects();
|
|
|
|
using var json = JsonDocument.Parse(catalog);
|
|
var projects = json.RootElement.EnumerateArray().ToArray();
|
|
Assert.Equal(3, projects.Length);
|
|
Assert.Equal("Alpha", projects[0].GetProperty("id").GetString());
|
|
Assert.Equal("Alpha Platform", projects[0].GetProperty("displayname").GetString());
|
|
Assert.Equal("Meeting automation", projects[0].GetProperty("description").GetString());
|
|
Assert.Equal("Legacy", projects[1].GetProperty("displayname").GetString());
|
|
Assert.Equal("", projects[1].GetProperty("description").GetString());
|
|
Assert.Equal("Malformed", projects[2].GetProperty("displayname").GetString());
|
|
Assert.Equal("", projects[2].GetProperty("description").GetString());
|
|
Assert.DoesNotContain("Private", catalog);
|
|
Assert.Equal("", await tools.ReadProjectFile("Alpha", "AGENTS.md"));
|
|
}
|
|
|
|
[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.Contains("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.StartsWith("Refused:", await tools.WriteProjectFile("IgnoredProject", "ignored.md", "changed"));
|
|
Assert.Equal("alpha should not be searched", await File.ReadAllTextAsync(Path.Combine(ignoredRoot, "ignored.md")));
|
|
Assert.Equal(
|
|
"README.md:2 Second alpha line",
|
|
await tools.Search("alpha"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProjectFileAccessAcceptsScalarProjectFrontmatter()
|
|
{
|
|
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/notes.md", await tools.WriteProjectFile("MeetingAssistant", "notes.md", "Bound project"));
|
|
Assert.Equal("Bound project", await tools.ReadProjectFile("MeetingAssistant", "notes.md"));
|
|
}
|
|
|
|
[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 artifacts = CreateArtifacts(root);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!);
|
|
await File.WriteAllTextAsync(artifacts.MeetingNotePath, "---\nprojects: MeetingAssistant\n---\n");
|
|
var tools = new MeetingSummaryTools(
|
|
artifacts,
|
|
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 artifacts = CreateArtifacts(root);
|
|
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!);
|
|
await File.WriteAllTextAsync(artifacts.MeetingNotePath, "---\nprojects: MeetingAssistant\n---\n");
|
|
var outsidePath = Path.Combine(projectsRoot, "outside.md");
|
|
await File.WriteAllTextAsync(outsidePath, "Preserve outside content");
|
|
var tools = new MeetingSummaryTools(
|
|
artifacts,
|
|
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.Equal("Preserve outside content", await File.ReadAllTextAsync(outsidePath));
|
|
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"));
|
|
}
|
|
}
|