forked from Manuel/meeting-assistant
223 lines
9.7 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|