Generalize settings and logs assistant
PR and Push Build/Test / build-and-test (push) Successful in 16m43s

This commit is contained in:
2026-05-30 12:57:51 +02:00
parent 740f93f185
commit 250d3b7a1e
32 changed files with 2219 additions and 539 deletions
@@ -1,6 +1,8 @@
using MeetingAssistant.Workflow;
using MeetingAssistant.Logging;
using MeetingAssistant.Speakers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
@@ -82,6 +84,239 @@ public sealed class WorkflowRulesEditorTests
Assert.DoesNotContain("other.yaml", searchResult);
}
[Fact]
public async Task SettingsToolsReadWriteAndValidateConfiguredAppsettingsFile()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var configPath = Path.Combine(root, "appsettings.json");
await File.WriteAllTextAsync(
configPath,
"""
{
"MeetingAssistant": {
"Agent": {
"Model": "old-model"
}
}
}
""");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions(),
configPath: configPath);
var readResult = await tools.ReadConfig(from: 3, to: 5);
var writeResult = await tools.WriteConfig("""
{
"MeetingAssistant": {
"Agent": {
"Model": "new-model"
}
}
}
""");
var invalidResult = await tools.WriteConfig("{ invalid json");
Assert.Contains("\"Agent\"", readResult);
Assert.Equal(configPath, writeResult);
Assert.StartsWith("Refused: appsettings JSON is invalid.", invalidResult);
Assert.Contains("new-model", await File.ReadAllTextAsync(configPath));
Assert.DoesNotContain("invalid json", await File.ReadAllTextAsync(configPath));
}
[Fact]
public async Task SettingsToolsReadConfigurationDocumentation()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var docsPath = Path.Combine(root, "meeting-assistant-configuration.md");
await File.WriteAllTextAsync(docsPath, "# Config Docs\n\nAgent model settings.");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions(),
configDocsPath: docsPath);
var docs = await tools.ReadConfigDocs();
Assert.Contains("Agent model settings", docs);
}
[Fact]
public async Task SettingsToolsReadAndSearchApplicationLogs()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
await File.WriteAllTextAsync(
Path.Combine(root, MeetingAssistantLogFiles.CurrentLogFileName),
"first line\nsecond needle\nthird line\n");
await File.WriteAllTextAsync(
Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(1)),
"rotated needle\n");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions(),
logDirectory: root);
var tail = await tools.ReadLogs(tail: 2);
var range = await tools.ReadLogs(from: 2, to: 2);
var search = await tools.SearchLogs("needle");
Assert.DoesNotContain("first line", tail);
Assert.Contains("second needle", tail);
Assert.Equal("second needle", range.Trim());
Assert.Contains("meeting-assistant.log:2 second needle", search);
Assert.Contains("meeting-assistant.log.1:1 rotated needle", search);
}
[Fact]
public async Task SettingsToolsReadAndSearchOnlyCopiedSpecFiles()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var sessionSpec = Path.Combine(root, "meeting-session", "spec.md");
Directory.CreateDirectory(Path.GetDirectoryName(sessionSpec)!);
await File.WriteAllTextAsync(
sessionSpec,
"""
# meeting-session Specification
Meeting Assistant creates notes.
""");
await File.WriteAllTextAsync(
Path.Combine(root, "root-spec.md"),
"Root spec mentions Assistant.");
var outside = Path.Combine(Path.GetDirectoryName(root)!, "outside.md");
await File.WriteAllTextAsync(outside, "outside Assistant");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions(),
specRootPath: root);
var read = await tools.ReadSpecFile("meeting-session/spec.md", from: 3, to: 3);
var escaped = await tools.ReadSpecFile("../outside.md");
var search = await tools.SearchSpec("Assistant");
Assert.Equal("Meeting Assistant creates notes.", read.Trim());
Assert.StartsWith("Refused: spec path must stay inside", escaped);
Assert.Contains("meeting-session/spec.md:3 Meeting Assistant creates notes.", search);
Assert.Contains("root-spec.md:1 Root spec mentions Assistant.", search);
Assert.DoesNotContain("outside.md", search);
}
[Fact]
public async Task SettingsToolsReadTailAndSearchSpecsWithFilePattern()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var sessionSpec = Path.Combine(root, "meeting-session", "spec.md");
var otherSpec = Path.Combine(root, "other", "notes.md");
Directory.CreateDirectory(Path.GetDirectoryName(sessionSpec)!);
Directory.CreateDirectory(Path.GetDirectoryName(otherSpec)!);
await File.WriteAllTextAsync(sessionSpec, "one\nsession needle\nthree\nfour");
await File.WriteAllTextAsync(otherSpec, "other needle");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions(),
specRootPath: root);
var tail = await tools.ReadSpecFile("meeting-session/spec.md", tail: 2);
var search = await tools.SearchSpec("needle", file_pattern: "meeting-session/*.md");
Assert.Equal("three\nfour", tail);
Assert.Contains("meeting-session/spec.md:2 session needle", search);
Assert.DoesNotContain("other/notes.md", search);
}
[Fact]
public async Task SettingsToolsCreateProjectWithRecommendedSeedOrDirectAgentsFile()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var projectsRoot = Path.Combine(root, "Projects");
var agentsTemplatePath = Path.Combine(root, "Project-AGENTS.md");
Directory.CreateDirectory(root);
await File.WriteAllTextAsync(agentsTemplatePath, "Recommended project instructions.");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions
{
Vault =
{
ProjectsFolder = projectsRoot
}
},
projectAgentsTemplatePath: agentsTemplatePath);
var recommended = await tools.CreateProject("Alpha Project");
var direct = await tools.CreateProject(
"Beta Project",
seed: "agents_md",
agents_md: "Direct project instructions.");
var alphaRoot = Path.Combine(projectsRoot, "Alpha Project");
var betaRoot = Path.Combine(projectsRoot, "Beta Project");
Assert.Equal("Alpha Project", recommended);
Assert.Contains("Alpha Project", await tools.ListProjects());
Assert.Equal("Recommended project instructions.", await File.ReadAllTextAsync(Path.Combine(alphaRoot, "AGENTS.md")));
Assert.Contains("# Executive Summary", await File.ReadAllTextAsync(Path.Combine(alphaRoot, "PROJECT.md")));
Assert.Contains("project folder was created", await File.ReadAllTextAsync(Path.Combine(alphaRoot, "JOURNAL.md")));
Assert.Contains("Important decisions", await File.ReadAllTextAsync(Path.Combine(alphaRoot, "DECISIONS.md")));
Assert.Equal("Direct project instructions.", await File.ReadAllTextAsync(Path.Combine(betaRoot, "AGENTS.md")));
Assert.False(File.Exists(Path.Combine(betaRoot, "PROJECT.md")));
Assert.StartsWith("Refused:", await tools.CreateProject("../Escape"));
}
[Fact]
public async Task SettingsToolsReadWriteAndSearchExistingProjectFiles()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
var projectsRoot = Path.Combine(root, "Projects");
var alphaRoot = Path.Combine(projectsRoot, "Alpha");
var betaRoot = Path.Combine(projectsRoot, "Beta");
Directory.CreateDirectory(alphaRoot);
Directory.CreateDirectory(betaRoot);
await File.WriteAllTextAsync(Path.Combine(alphaRoot, "PROJECT.md"), "alpha one\nalpha needle\nalpha three");
await File.WriteAllTextAsync(Path.Combine(alphaRoot, "notes.txt"), "alpha needle hidden from md search");
await File.WriteAllTextAsync(Path.Combine(betaRoot, "PROJECT.md"), "beta needle");
var tools = new WorkflowRulesEditorTools(
new MeetingAssistantOptions
{
Vault =
{
ProjectsFolder = projectsRoot
}
});
var files = await tools.ListProjectFiles("Alpha");
var tail = await tools.ReadProjectFile("Alpha", "PROJECT.md", tail: 2);
var write = await tools.WriteProjectFile("Alpha", "JOURNAL.md", "created");
var search = await tools.SearchProjects("needle", file_pattern: "*.md");
Assert.Equal("notes.txt\nPROJECT.md", files);
Assert.Equal("alpha needle\nalpha three", tail);
Assert.Equal("Alpha/JOURNAL.md", write);
Assert.Equal("created", await File.ReadAllTextAsync(Path.Combine(alphaRoot, "JOURNAL.md")));
Assert.Contains("Alpha/PROJECT.md:2 alpha needle", search);
Assert.Contains("Beta/PROJECT.md:1 beta needle", search);
Assert.DoesNotContain("notes.txt", search);
Assert.StartsWith("Refused:", await tools.ReadProjectFile("Missing", "PROJECT.md"));
Assert.StartsWith("Refused:", await tools.WriteProjectFile("Alpha", "../escape.md", "bad"));
}
[Fact]
public void FileLoggerRotatesCurrentLogAndKeepsFourOlderFiles()
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
File.WriteAllText(Path.Combine(root, MeetingAssistantLogFiles.CurrentLogFileName), "current");
File.WriteAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(1)), "old1");
File.WriteAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(2)), "old2");
File.WriteAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(3)), "old3");
File.WriteAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(4)), "old4");
using (var provider = new MeetingAssistantFileLoggerProvider(root))
{
provider.CreateLogger("Test").LogInformation("fresh");
}
Assert.Contains("fresh", File.ReadAllText(Path.Combine(root, MeetingAssistantLogFiles.CurrentLogFileName)));
Assert.Equal("current", File.ReadAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(1))));
Assert.Equal("old3", File.ReadAllText(Path.Combine(root, MeetingAssistantLogFiles.GetRotatedLogFileName(4))));
Assert.False(File.Exists(Path.Combine(root, "meeting-assistant.log.5")));
}
[Fact]
public async Task RulesEditorToolsAppendByDefaultAndReplaceOnlyWhenRequested()
{
@@ -157,6 +392,9 @@ public sealed class WorkflowRulesEditorTests
Assert.Contains("Meeting Workflow Engine", instructions);
Assert.Contains("Workflow rules reference documentation", instructions);
Assert.Contains("speaker identity database", instructions);
Assert.Contains("read_config", instructions);
Assert.Contains("read_logs", instructions);
Assert.Contains("read_spec_file", instructions);
}
[Fact]