Public Access
Generalize settings and logs assistant
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
PR and Push Build/Test / build-and-test (push) Successful in 16m43s
This commit is contained in:
@@ -50,7 +50,7 @@ public sealed class MeetingSummaryToolTests
|
||||
Assert.False(tools.SummaryWasWritten);
|
||||
var result = await tools.WriteSummary("# Summary\n\n- Done.", "Done items");
|
||||
|
||||
Assert.Equal(artifacts.SummaryPath, result);
|
||||
Assert.Equal(Path.GetFileName(artifacts.SummaryPath), result);
|
||||
Assert.True(tools.SummaryWasWritten);
|
||||
var summary = await File.ReadAllTextAsync(artifacts.SummaryPath);
|
||||
Assert.Contains("title: Meeting", summary);
|
||||
@@ -478,6 +478,22 @@ public sealed class MeetingSummaryToolTests
|
||||
Assert.Equal("", await tools.ReadTranscript(from: 3, to: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadToolsSupportTail()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
||||
var artifacts = 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"));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.TranscriptPath)!);
|
||||
await File.WriteAllTextAsync(artifacts.TranscriptPath, "one\ntwo\nthree");
|
||||
var tools = new MeetingSummaryTools(artifacts);
|
||||
|
||||
Assert.Equal("two\nthree", await tools.ReadTranscript(tail: 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToolsWriteAssistantContextBodyWithLineModes()
|
||||
{
|
||||
|
||||
@@ -121,6 +121,41 @@ public sealed class ProjectKnowledgeToolTests
|
||||
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(
|
||||
|
||||
@@ -145,6 +145,40 @@ public sealed class RecordingCoordinatorTests
|
||||
await coordinator.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopTreatsRunsShorterThanConfiguredMinimumAsAbort()
|
||||
{
|
||||
var audioSource = new ControlledAudioSource();
|
||||
var artifactCleaner = new CapturingMeetingRunArtifactCleaner();
|
||||
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
||||
var coordinator = new MeetingRecordingCoordinator(
|
||||
audioSource,
|
||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||
new InMemoryTranscriptStore(),
|
||||
new InMemoryMeetingNoteStore(),
|
||||
new CapturingMeetingNoteOpener(),
|
||||
new InMemoryMeetingArtifactStore(),
|
||||
new InMemoryRecordedAudioStore(),
|
||||
summaryPipeline,
|
||||
Options.Create(new MeetingAssistantOptions
|
||||
{
|
||||
Recording =
|
||||
{
|
||||
MinimumCompletedMeetingDuration = TimeSpan.FromMinutes(1)
|
||||
}
|
||||
}),
|
||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
||||
artifactCleaner: artifactCleaner);
|
||||
|
||||
var started = await coordinator.StartAsync(CancellationToken.None);
|
||||
var stopped = await coordinator.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.False(stopped.IsRecording);
|
||||
Assert.Null(stopped.MeetingNotePath);
|
||||
Assert.False(summaryPipeline.WasRun);
|
||||
Assert.Equal(started.SummaryPath, artifactCleaner.DeletedArtifacts?.SummaryPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartUsesCurrentOutlookMeetingMetadataWhenAvailable()
|
||||
{
|
||||
@@ -2240,6 +2274,8 @@ public sealed class RecordingCoordinatorTests
|
||||
|
||||
public MeetingAssistantOptions? Options { get; private set; }
|
||||
|
||||
public bool WasRun => Artifacts is not null;
|
||||
|
||||
public Task<MeetingSummaryRunResult> RunAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -2262,6 +2298,23 @@ public sealed class RecordingCoordinatorTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CapturingMeetingRunArtifactCleaner : IMeetingRunArtifactCleaner
|
||||
{
|
||||
public MeetingSessionArtifacts? DeletedArtifacts { get; private set; }
|
||||
|
||||
public MeetingAssistantOptions? Options { get; private set; }
|
||||
|
||||
public Task DeleteRunArtifactsAsync(
|
||||
MeetingSessionArtifacts artifacts,
|
||||
MeetingAssistantOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
DeletedArtifacts = artifacts;
|
||||
Options = options;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingSummaryPipeline : IMeetingSummaryPipeline
|
||||
{
|
||||
public List<MeetingSessionArtifacts> ArtifactHistory { get; } = [];
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class TaskbarIconTests
|
||||
Assert.Equal(RecordingProcessState.Idle, menu.State);
|
||||
Assert.Contains(menu.Items, item =>
|
||||
item.Action == MeetingTaskbarAction.EditRules &&
|
||||
item.Text == "Edit rules and identities");
|
||||
item.Text == "Settings and logs");
|
||||
Assert.Contains(menu.Items, item =>
|
||||
item.Action == MeetingTaskbarAction.StartRecording &&
|
||||
item.ProfileName == "default" &&
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user