Add taskbar controls and summary oneliner

This commit is contained in:
2026-05-27 23:31:40 +02:00
parent 7777b349a5
commit 71f1e6a0b8
22 changed files with 720 additions and 9 deletions
@@ -111,6 +111,23 @@ public sealed class LaunchProfileOptionsProviderTests
Assert.DoesNotContain(hotkeys, hotkey => hotkey.ProfileName == "english");
}
[Fact]
public void GetProfilesReturnsDefaultAndConfiguredNamedProfiles()
{
var provider = CreateProvider(new Dictionary<string, string?>
{
["MeetingAssistant:Hotkey:Toggle"] = "Ctrl+Alt+M",
["MeetingAssistant:Recording:TranscriptionProvider"] = "azure-speech",
["MeetingAssistant:LaunchProfiles:english:AzureSpeech:Language"] = "en-US"
});
var profiles = provider.GetProfiles();
Assert.Equal(["default", "english"], profiles.Select(profile => profile.Name));
Assert.Equal("azure-speech", profiles.Single(profile => profile.Name == "english").Options.Recording.TranscriptionProvider);
Assert.Equal("en-US", profiles.Single(profile => profile.Name == "english").Options.AzureSpeech.Language);
}
private static ConfigurationLaunchProfileOptionsProvider CreateProvider(
IReadOnlyDictionary<string, string?> values)
{
@@ -31,6 +31,9 @@ public sealed class MeetingSummaryInstructionBuilderTests
Assert.Contains("merge=true", instructions);
Assert.Contains("delete_identity", instructions);
Assert.Contains("wrongfully matched", instructions);
Assert.Contains("oneliner", instructions);
Assert.Contains("very short", instructions);
Assert.Contains("conciseness is more important", instructions);
}
[Fact]
@@ -48,12 +48,13 @@ public sealed class MeetingSummaryToolTests
Assert.Equal("", await tools.ReadGlossary());
Assert.False(tools.SummaryWasWritten);
var result = await tools.WriteSummary("# Summary\n\n- Done.");
var result = await tools.WriteSummary("# Summary\n\n- Done.", "Done items");
Assert.Equal(artifacts.SummaryPath, result);
Assert.True(tools.SummaryWasWritten);
var summary = await File.ReadAllTextAsync(artifacts.SummaryPath);
Assert.Contains("title: Meeting", summary);
Assert.Contains("oneliner: Done items", summary);
Assert.Contains("start_time: \"2026-05-20T10:00:00.0000000+02:00\"", summary);
Assert.Contains("end_time: \"2026-05-20T10:30:00.0000000+02:00\"", summary);
Assert.Contains("attendees:", summary);
@@ -101,7 +102,7 @@ public sealed class MeetingSummaryToolTests
""");
var tools = new MeetingSummaryTools(artifacts);
await tools.WriteSummary("# Summary");
await tools.WriteSummary("# Summary", "Preserved attendee summary");
var summary = await File.ReadAllTextAsync(artifacts.SummaryPath);
Assert.Contains("attendees:", summary);
@@ -144,7 +145,7 @@ public sealed class MeetingSummaryToolTests
""");
var tools = new MeetingSummaryTools(artifacts);
await tools.WriteSummary("# Summary");
await tools.WriteSummary("# Summary", "Current project summary");
var summary = await File.ReadAllTextAsync(artifacts.SummaryPath);
Assert.Contains("projects:", summary);
@@ -612,7 +613,7 @@ public sealed class MeetingSummaryToolTests
audit);
await tools.WriteContext("owned context");
await tools.WriteSummary("owned summary");
await tools.WriteSummary("owned summary", "Owned summary");
await tools.WriteProjectFile("MeetingAssistant", "notes.md", "TWO", from: 2, to: 2);
await tools.AddDictationWord("PBI");
await audit.AppendToAssistantContextAsync(CancellationToken.None);
@@ -627,4 +628,30 @@ public sealed class MeetingSummaryToolTests
Assert.DoesNotContain("owned summary", context);
Assert.DoesNotContain("- Context before.", context);
}
[Fact]
public async Task WriteSummaryRefusesOneLinerWithLineBreaks()
{
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.MeetingNotePath)!);
await File.WriteAllTextAsync(
artifacts.MeetingNotePath,
"""
---
title: Meeting
---
""");
var tools = new MeetingSummaryTools(artifacts);
var result = await tools.WriteSummary("# Summary", "First line\nsecond line");
Assert.Equal("Refused: oneliner must be a single line.", result);
Assert.False(tools.SummaryWasWritten);
Assert.False(File.Exists(artifacts.SummaryPath));
}
}
@@ -0,0 +1,96 @@
using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.Recording;
using MeetingAssistant.Taskbar;
namespace MeetingAssistant.Tests;
public sealed class TaskbarIconTests
{
[Fact]
public void IdleMenuOffersStartForEachConfiguredProfile()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(),
[Profile("default"), Profile("english")]);
Assert.Equal(RecordingProcessState.Idle, menu.State);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "default");
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "english");
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.StopRecording);
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.AbortRecording);
}
[Fact]
public void RecordingMenuOffersStopAbortAndOtherProfileSwitches()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "default"),
[Profile("default"), Profile("english"), Profile("french")]);
Assert.Equal(RecordingProcessState.Recording, menu.State);
Assert.Contains(menu.Items, item => item.Action == MeetingTaskbarAction.StopRecording);
Assert.Contains(menu.Items, item => item.Action == MeetingTaskbarAction.AbortRecording);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "english");
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "french");
Assert.DoesNotContain(menu.Items, item =>
item.Action == MeetingTaskbarAction.SwitchProfile &&
item.ProfileName == "default");
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.StartRecording);
}
[Fact]
public void ProcessingMenuShowsSummarizingButAllowsStartingNewRecordings()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(state: RecordingProcessState.Summarizing, profile: "default"),
[Profile("default"), Profile("english")]);
Assert.Equal(RecordingProcessState.Summarizing, menu.State);
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "default");
Assert.Contains(menu.Items, item =>
item.Action == MeetingTaskbarAction.StartRecording &&
item.ProfileName == "english");
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.StopRecording);
Assert.DoesNotContain(menu.Items, item => item.Action == MeetingTaskbarAction.AbortRecording);
}
[Fact]
public void RecordingStateWinsOverOlderSummarizingWork()
{
var menu = MeetingTaskbarMenuBuilder.Build(
Status(isRecording: true, state: RecordingProcessState.Recording, profile: "english"),
[Profile("default"), Profile("english")]);
Assert.Equal(RecordingProcessState.Recording, menu.State);
}
private static LaunchProfile Profile(string name)
{
return new LaunchProfile(name, new MeetingAssistantOptions());
}
private static RecordingStatus Status(
bool isRecording = false,
RecordingProcessState state = RecordingProcessState.Idle,
string? profile = null)
{
return new RecordingStatus(
isRecording,
state == RecordingProcessState.Idle ? null : "transcript.md",
state == RecordingProcessState.Idle ? null : "meeting.md",
state == RecordingProcessState.Idle ? null : "context.md",
state == RecordingProcessState.Idle ? null : "summary.md",
state,
profile);
}
}