using System.Net; using System.Net.Http.Json; using MeetingAssistant.Notifications; using MeetingAssistant.MacOs; using MeetingAssistant.Workflow; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace MeetingAssistant.Tests; #if !WINDOWS public sealed class DesktopCommunicationEndpointTests { [Fact] public async Task PendingRequestCanBeReadThroughDesktopEndpoint() { await using var factory = CreateFactory(); using var client = factory.CreateClient(); var requests = factory.Services.GetRequiredService(); var registration = requests.Add(new DesktopUserRequestSpec( "agent-approval", "Project association", "Allow access to project Apollo?", TimeSpan.FromMinutes(10), [new("yes", "Yes"), new("no", "No")]), CancellationToken.None); using var response = await client.GetAsync("/desktop/requests"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var request = Assert.Single((await response.Content.ReadFromJsonAsync())!); Assert.Equal(registration.Id, request.Id); Assert.Equal("Allow access to project Apollo?", request.Message); Assert.Equal(new[] { "yes", "no" }, request.Actions.Select(action => action.Id)); } [Fact] public async Task OfferedHttpResponseDeliversOneDecisionAndRejectsDuplicate() { await using var factory = CreateFactory(); using var client = factory.CreateClient(); var requests = factory.Services.GetRequiredService(); var calls = 0; var registration = requests.Add(new DesktopUserRequestSpec( "agent-approval", "Approval", "Apply proposed change?", TimeSpan.FromMinutes(10), [new("yes", "Yes"), new("no", "No")]), CancellationToken.None, (_, _) => { Interlocked.Increment(ref calls); return Task.CompletedTask; }); var path = $"/desktop/requests/{registration.Id}/respond"; using var accepted = await client.PostAsJsonAsync(path, new { action = "yes" }); using var duplicate = await client.PostAsJsonAsync(path, new { action = "yes" }); Assert.Equal(HttpStatusCode.OK, accepted.StatusCode); Assert.Equal(HttpStatusCode.Gone, duplicate.StatusCode); Assert.Equal("yes", await registration.Decision); Assert.Equal(1, calls); Assert.Empty((await client.GetFromJsonAsync("/desktop/requests"))!); } [Fact] public async Task ApprovedWindowAndRepeatedSnapshotsKeepOneIndependentPrompt() { await using var factory = CreateFactory(); using var client = factory.CreateClient(); var sessions = factory.Services.GetRequiredService(); sessions.OpenWithPrompt("Add the requested workflow scenario in full."); using var response = await client.GetAsync("/desktop/agent-windows"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var window = Assert.Single((await response.Content.ReadFromJsonAsync())!); Assert.EndsWith($"?session={window.Id}", window.Url); var path = $"/diagnostics/settings-and-logs/chat?session={window.Id}"; MacOsWorkflowRulesEditorChatResponse? first = null; for (var attempt = 0; attempt < 100; attempt++) { first = await client.GetFromJsonAsync(path); if (!first!.IsThinking && first.Messages.Count > 0) break; await Task.Delay(10); } Assert.False(first!.IsThinking); var second = await client.GetFromJsonAsync(path); Assert.Equal("Add the requested workflow scenario in full.", first.Messages[0].Content); Assert.Equal(first.Messages, second!.Messages); Assert.Single(factory.Services.GetRequiredService().Calls); Assert.Empty((await client.GetFromJsonAsync( "/diagnostics/settings-and-logs/chat"))!.Messages); } [Fact] public async Task UnknownChatSessionCannotSendIntoTheDefaultConversation() { await using var factory = CreateFactory(); using var client = factory.CreateClient(); using var response = await client.PostAsJsonAsync( "/diagnostics/settings-and-logs/chat?session=unknown-session", new { message = "Do not route this elsewhere" }); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Empty(factory.Services.GetRequiredService().Calls); Assert.Empty((await client.GetFromJsonAsync( "/diagnostics/settings-and-logs/chat"))!.Messages); } [MacOsFact] public async Task MacOsCompositionWaitsForApprovalAndUsesTheDesktopDecision() { await using var factory = CreateFactory(); using var client = factory.CreateClient(); using var origin = new CancellationTokenSource(); var approval = factory.Services.GetRequiredService(); var requests = factory.Services.GetRequiredService(); var decision = approval.ConfirmAsync("Test approval", "Isolated composition check", origin.Token); try { var pending = Assert.Single(requests.List()); Assert.False(decision.IsCompleted); using var response = await client.PostAsJsonAsync( $"/desktop/requests/{pending.Id}/respond", new { action = "no" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.False(await decision.WaitAsync(TimeSpan.FromSeconds(5))); Assert.IsType( factory.Services.GetRequiredService()); } finally { origin.Cancel(); } } internal static WebApplicationFactory CreateFactory() { return new IsolatedApplicationFactory().WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((_, configuration) => configuration.AddInMemoryCollection(new Dictionary { ["MeetingAssistant:FunAsr:Backend:Enabled"] = "false", ["MeetingAssistant:CalendarRecordingPrompts:Enabled"] = "false" })); builder.ConfigureTestServices(services => { services.RemoveAll(); services.AddSingleton(); services.AddSingleton(provider => provider.GetRequiredService()); }); }); } private sealed class EndpointVerificationPipeline : IWorkflowRulesEditorChatPipeline { public List Calls { get; } = []; public Task SendAsync( IReadOnlyList conversation, string userMessage, CancellationToken cancellationToken, Action? activityChanged = null) { Calls.Add(userMessage); return Task.FromResult(new WorkflowRulesEditorChatResult("Verification reply")); } } } #endif