Public Access
247 lines
9.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|