Files
meeting-assistant/MeetingAssistant.Tests/MeetingWorkflowEngineTests.cs
T
2026-05-27 12:55:18 +02:00

247 lines
9.0 KiB
C#

using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.Tests;
public sealed class MeetingWorkflowEngineTests
{
[Fact]
public async Task CreatedRuleAddsDefaultAttendeeOnceWhenMeetingHasNoAttendees()
{
var fixture = await WorkflowFixture.CreateAsync(
"""
rules:
- name: add-me
on:
- created: {}
if:
- condition: meeting.attendees.count = 0
steps:
- uses: add_attendee
value: Manuel
""");
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.Created(fixture.Artifacts),
fixture.Options,
CancellationToken.None);
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.Created(fixture.Artifacts),
fixture.Options,
CancellationToken.None);
var meeting = await fixture.NoteStore.ReadAsync(fixture.Artifacts.MeetingNotePath, CancellationToken.None);
Assert.Equal(["Manuel"], meeting.Frontmatter.Attendees);
}
[Fact]
public async Task StateTransitionRuleCanCleanTitleAndAddProjectWithNestedConditions()
{
var fixture = await WorkflowFixture.CreateAsync(
"""
rules:
- name: clean-teams-title
on:
- state_transition:
from: collecting metadata
to: transcribing
if:
- and:
- condition: contains(meeting.title, '[Teams]')
- not:
condition: contains(meeting.projects, 'Architecture')
steps:
- uses: set_property
property: title
value: '@Model.Meeting.Title.Replace("[Teams]", "").Trim()'
- uses: add_project
value: Architecture
""",
title: "[Teams] Architecture Sync");
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.StateTransition(
fixture.Artifacts,
AssistantContextState.CollectingMetadata,
AssistantContextState.Transcribing),
fixture.Options,
CancellationToken.None);
var meeting = await fixture.NoteStore.ReadAsync(fixture.Artifacts.MeetingNotePath, CancellationToken.None);
Assert.Equal("Architecture Sync", meeting.Frontmatter.Title);
Assert.Equal(["Architecture"], meeting.Frontmatter.Projects);
}
[Fact]
public async Task SpeakerIdentifiedRuleFiltersByNameAndWritesRazorContext()
{
var fixture = await WorkflowFixture.CreateAsync(
"""
rules:
- name: ada-context
on:
- speaker_identified:
name: Ada
if:
- or:
- condition: contains(meeting.title, 'Architecture')
- condition: meeting.attendees.count > 3
steps:
- uses: add_context
value: 'Speaker @Model.Speaker.Name was identified in @Model.Meeting.Title.'
""",
title: "Architecture Sync");
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.SpeakerIdentified(fixture.Artifacts, "Grace"),
fixture.Options,
CancellationToken.None);
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.SpeakerIdentified(fixture.Artifacts, "Ada"),
fixture.Options,
CancellationToken.None);
var context = await File.ReadAllTextAsync(fixture.Artifacts.AssistantContextPath);
Assert.DoesNotContain("Grace", context);
Assert.Contains("Speaker Ada was identified in Architecture Sync.", context);
}
[Fact]
public async Task RuleCanRemoveAttendeeWhenNestedConditionMatches()
{
var fixture = await WorkflowFixture.CreateAsync(
"""
rules:
- name: remove-placeholder
on:
- created: {}
if:
- and:
- condition: contains(meeting.attendees, 'Placeholder')
- not:
condition: contains(meeting.title, 'Keep Placeholder')
steps:
- uses: remove_attendee
value: Placeholder
""",
attendees: ["Placeholder", "Ada"]);
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.Created(fixture.Artifacts),
fixture.Options,
CancellationToken.None);
var meeting = await fixture.NoteStore.ReadAsync(fixture.Artifacts.MeetingNotePath, CancellationToken.None);
Assert.Equal(["Ada"], meeting.Frontmatter.Attendees);
}
[Fact]
public async Task MissingRulesFileDoesNothing()
{
var fixture = await WorkflowFixture.CreateAsync("", rulesPath: Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing.yaml"));
await fixture.Engine.RunAsync(
MeetingWorkflowEvent.Created(fixture.Artifacts),
fixture.Options,
CancellationToken.None);
var meeting = await fixture.NoteStore.ReadAsync(fixture.Artifacts.MeetingNotePath, CancellationToken.None);
Assert.Empty(meeting.Frontmatter.Attendees);
}
private sealed class WorkflowFixture
{
private WorkflowFixture(
MeetingAssistantOptions options,
MarkdownMeetingNoteStore noteStore,
MeetingSessionArtifacts artifacts,
MeetingWorkflowEngine engine)
{
Options = options;
NoteStore = noteStore;
Artifacts = artifacts;
Engine = engine;
}
public MeetingAssistantOptions Options { get; }
public MarkdownMeetingNoteStore NoteStore { get; }
public MeetingSessionArtifacts Artifacts { get; }
public MeetingWorkflowEngine Engine { get; }
public static async Task<WorkflowFixture> CreateAsync(
string rulesYaml,
string title = "Planning Sync",
IReadOnlyList<string>? attendees = null,
string? rulesPath = null)
{
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
rulesPath ??= Path.Combine(root, "rules.yaml");
if (!string.IsNullOrEmpty(rulesYaml))
{
await File.WriteAllTextAsync(rulesPath, rulesYaml);
}
var options = new MeetingAssistantOptions
{
Vault = new VaultOptions
{
BaseFolder = root,
MeetingNotesFolder = "Notes",
TranscriptsFolder = "Transcripts",
AssistantContextFolder = "Context",
SummariesFolder = "Summaries",
ProjectsFolder = "Projects"
},
Automation = new AutomationOptions
{
RulesPath = rulesPath
}
};
var noteStore = new MarkdownMeetingNoteStore(
Microsoft.Extensions.Options.Options.Create(options),
NullLogger<MarkdownMeetingNoteStore>.Instance);
var artifactStore = new MarkdownMeetingArtifactStore(
NullLogger<MarkdownMeetingArtifactStore>.Instance);
var artifacts = new MeetingSessionArtifacts(
Path.Combine(root, "Notes", "meeting.md"),
Path.Combine(root, "Transcripts", "transcript.md"),
Path.Combine(root, "Context", "context.md"),
Path.Combine(root, "Summaries", "summary.md"));
var meeting = await noteStore.SaveAsync(
MeetingNoteTemplate.Create(
title,
DateTimeOffset.Parse("2026-05-26T10:00:00+02:00"),
attendees: attendees ?? [],
transcriptPath: artifacts.TranscriptPath,
assistantContextPath: artifacts.AssistantContextPath,
summaryPath: artifacts.SummaryPath) with
{
Path = artifacts.MeetingNotePath
},
options,
CancellationToken.None);
await artifactStore.CreateAssistantContextAsync(
artifacts,
meeting,
"",
null,
CancellationToken.None);
var engine = new MeetingWorkflowEngine(
new FileMeetingWorkflowRulesProvider(
NullLogger<FileMeetingWorkflowRulesProvider>.Instance),
noteStore,
artifactStore,
NullLogger<MeetingWorkflowEngine>.Instance);
return new WorkflowFixture(options, noteStore, artifacts, engine);
}
}
}