Files
meeting-assistant/MeetingAssistant.Tests/MacOsWorkflowRulesEditorSessionTests.cs
T
dh 728e66dd72
PR and Push Build/Test / windows-build-and-test (push) Failing after 38m25s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m9s
PR and Push Build/Test / macos-native-full (push) Skipped
ci: consolidate platform pipelines and test prerequisites on macOS support
2026-10-06 09:02:19 +02:00

223 lines
9.7 KiB
C#

using MeetingAssistant.MacOs;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace MeetingAssistant.Tests;
public sealed class MacOsWorkflowRulesEditorSessionTests
{
[Fact]
public async Task ApprovedWorkflowStartsOnceInOwnSessionWithoutReplacingDefaultConversation()
{
var pipeline = new ReplyPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
Assert.True(sessions.TryGetSession(null, out var normal));
await normal.SendAsync("My existing conversation", CancellationToken.None);
const string prompt = "Given Alpha in the title,\nwhen metadata arrives, then associate project Alpha.";
sessions.OpenWithPrompt(prompt);
var window = Assert.Single(sessions.GetAgentWindows());
Assert.EndsWith($"/diagnostics/settings-and-logs?session={window.Id}", window.Url);
Assert.True(sessions.TryGetSession(window.Id, out var requested));
var snapshot = await WaitForCompletionAsync(requested);
Assert.Equal(
[new MacOsWorkflowRulesEditorMessage("user", prompt), new MacOsWorkflowRulesEditorMessage("agent", "Agent response.")],
snapshot.Messages);
for (var index = 0; index < 3; index++)
{
Assert.Equal(snapshot.Messages, requested.GetSnapshot().Messages);
Assert.False(requested.GetSnapshot().IsThinking);
Assert.Single(sessions.GetAgentWindows());
}
Assert.Equal(
[new MacOsWorkflowRulesEditorMessage("user", "My existing conversation"), new MacOsWorkflowRulesEditorMessage("agent", "Agent response.")],
normal.GetSnapshot().Messages);
Assert.NotSame(normal, requested);
}
[Fact]
public async Task ClosingAResponseConnectionKeepsAcceptedChatWorkingAndSnapshotsRemainReadable()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
Assert.True(sessions.TryGetSession(null, out var session));
using var responseConnection = new CancellationTokenSource();
var response = session.SendAsync("Continue the approved change", responseConnection.Token);
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var pending = session.GetSnapshot();
Assert.True(pending.IsThinking);
Assert.Equal([new MacOsWorkflowRulesEditorMessage("user", "Continue the approved change")], pending.Messages);
responseConnection.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => response);
Assert.True(session.GetSnapshot().IsThinking);
pipeline.Release.TrySetResult();
var completed = await WaitForCompletionAsync(session);
Assert.False(completed.IsThinking);
Assert.Equal("Agent response.", completed.Messages.Last().Content);
Assert.Single(pending.Messages);
Assert.True(pending.IsThinking);
}
[Fact]
public async Task ApprovedWorkflowWindowOpensWithoutWaitingForItsAgentResponse()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
IWorkflowRulesEditorWindowService editor = new MacOsWorkflowRulesEditorWindowService(
Options.Create(new MeetingAssistantOptions()), () => sessions);
try
{
await Task.Run(() => editor.ShowWithPrompt("Apply the approved workflow scenario."))
.WaitAsync(TimeSpan.FromSeconds(2));
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var window = Assert.Single(sessions.GetAgentWindows());
Assert.True(sessions.TryGetSession(window.Id, out var session));
Assert.True(session.GetSnapshot().IsThinking);
}
finally
{
pipeline.Release.TrySetResult();
}
}
[Fact]
public void ConstructingTheWindowServiceDoesNotStartOrResolveAnAgentConversation()
{
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(new ReplyPipeline(), lifetime);
var resolutions = 0;
IWorkflowRulesEditorWindowService editor = new MacOsWorkflowRulesEditorWindowService(
Options.Create(new MeetingAssistantOptions()), () =>
{
resolutions++;
return sessions;
});
Assert.Equal(0, resolutions);
Assert.Empty(sessions.GetAgentWindows());
editor.ShowWithPrompt("An explicitly approved scenario.");
Assert.Equal(1, resolutions);
Assert.Single(sessions.GetAgentWindows());
}
[Fact]
public async Task TwoApprovedWorkflowsAndTheirFollowUpsRemainIndependent()
{
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(new ReplyPipeline(), lifetime);
sessions.OpenWithPrompt("First approved scenario.");
sessions.OpenWithPrompt("Second approved scenario.");
var windows = sessions.GetAgentWindows();
Assert.Equal(2, windows.Count);
Assert.Equal(2, windows.Select(window => window.Id).Distinct().Count());
var conversations = new List<MacOsWorkflowRulesEditorSession>();
foreach (var window in windows)
{
Assert.True(sessions.TryGetSession(window.Id, out var conversation));
conversations.Add(conversation);
}
var snapshots = await Task.WhenAll(conversations.Select(WaitForCompletionAsync));
Assert.Equal(
["First approved scenario.", "Second approved scenario."],
snapshots.Select(snapshot => snapshot.Messages[0].Content).Order());
var updated = await conversations[0].SendAsync("A follow-up in just this conversation.", CancellationToken.None);
Assert.Equal(4, updated.Messages.Count);
Assert.Equal(2, conversations[1].GetSnapshot().Messages.Count);
Assert.True(sessions.TryGetSession(null, out var normal));
Assert.Empty(normal.GetSnapshot().Messages);
}
[Fact]
public async Task ApplicationShutdownCancelsBackgroundConversationAndPreventsNewWindows()
{
var pipeline = new BlockingPipeline();
using var lifetime = new TestApplicationLifetime();
var sessions = CreateSessions(pipeline, lifetime);
sessions.OpenWithPrompt("Apply the approved scenario.");
await pipeline.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
var window = Assert.Single(sessions.GetAgentWindows());
Assert.True(sessions.TryGetSession(window.Id, out var conversation));
lifetime.StopApplication();
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(2));
while (conversation.GetSnapshot().IsThinking) await Task.Delay(10, deadline.Token);
Assert.Empty(sessions.GetAgentWindows());
Assert.Single(conversation.GetSnapshot().Messages);
Assert.ThrowsAny<OperationCanceledException>(() => sessions.OpenWithPrompt("A stale approval."));
}
private static MacOsWorkflowRulesEditorSessions CreateSessions(
IWorkflowRulesEditorChatPipeline pipeline,
IHostApplicationLifetime lifetime) => new(
pipeline,
Options.Create(new MeetingAssistantOptions()),
lifetime,
NullLogger<MacOsWorkflowRulesEditorSessions>.Instance);
private static async Task<MacOsWorkflowRulesEditorChatResponse> WaitForCompletionAsync(
MacOsWorkflowRulesEditorSession session)
{
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(2));
while (true)
{
var snapshot = session.GetSnapshot();
if (snapshot.Messages.Count >= 2 && !snapshot.IsThinking) return snapshot;
await Task.Delay(10, deadline.Token);
}
}
private sealed class ReplyPipeline : IWorkflowRulesEditorChatPipeline
{
public Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null) =>
Task.FromResult(new WorkflowRulesEditorChatResult("Agent response."));
}
private sealed class BlockingPipeline : IWorkflowRulesEditorChatPipeline
{
public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public async Task<WorkflowRulesEditorChatResult> SendAsync(
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
string userMessage,
CancellationToken cancellationToken,
Action<WorkflowRulesEditorActivityUpdate>? activityChanged = null)
{
Started.TrySetResult();
await Release.Task.WaitAsync(cancellationToken);
return new WorkflowRulesEditorChatResult("Agent response.");
}
}
private sealed class TestApplicationLifetime : IHostApplicationLifetime, IDisposable
{
private readonly CancellationTokenSource stopping = new();
public CancellationToken ApplicationStarted => CancellationToken.None;
public CancellationToken ApplicationStopping => stopping.Token;
public CancellationToken ApplicationStopped => stopping.Token;
public void StopApplication() => stopping.Cancel();
public void Dispose()
{
stopping.Cancel();
stopping.Dispose();
}
}
}