using MeetingAssistant.Calendar; using MeetingAssistant.MacOs; using MeetingAssistant.Notifications; using MeetingAssistant.Recording; using Microsoft.Extensions.Logging.Abstractions; namespace MeetingAssistant.Tests; public sealed class MacOsUserPromptTests { [Theory] [InlineData("yes", true)] [InlineData("no", false)] public async Task ApprovalWaitsForAnExplicitDesktopDecision(string action, bool approved) { using var lifetime = new TestApplicationLifetime(); using var store = CreateStore(lifetime); IAgentApprovalPromptService prompts = new MacOsAgentApprovalPromptService(store); var decision = prompts.ConfirmAsync(new AgentApprovalPrompt("Project requested", "Associate Alpha?", TimeSpan.FromMinutes(10)), CancellationToken.None); Assert.False(decision.IsCompleted); var request = Assert.Single(store.List()); Assert.Equal("agent-approval", request.Kind); Assert.Equal(["yes", "no"], request.Actions.Select(item => item.Id)); Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, action)); Assert.Equal(approved, await decision); Assert.Empty(store.List()); } [Fact] public async Task InactivityPromptReturnsImmediatelyAndOffersPauseWithOriginLifetime() { using var lifetime = new TestApplicationLifetime(); using var origin = new CancellationTokenSource(); var clock = new ManualDesktopRequestTimeProvider(); using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger.Instance); IMeetingInactivityPromptService prompts = new MacOsMeetingInactivityPromptService(store); MeetingInactivityPromptResponse? received = null; CancellationToken receivedToken = default; var showing = prompts.ShowStopPromptAsync( new MeetingInactivityPromptRequest(TimeSpan.FromMinutes(2), clock.GetUtcNow(), 1, TimeSpan.FromMinutes(2)), (response, token) => { received = response; receivedToken = token; return Task.CompletedTask; }, origin.Token); Assert.True(showing.IsCompletedSuccessfully); var request = Assert.Single(store.List()); Assert.Equal("meeting-inactivity", request.Kind); Assert.Equal(["stop", "continue", "pause"], request.Actions.Select(item => item.Id)); Assert.Equal(DateTimeOffset.Parse("2026-10-03T10:01:00Z"), request.ExpiresAt); Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, "pause")); Assert.Equal(MeetingInactivityPromptResponse.Pause, received); Assert.Equal(origin.Token, receivedToken); } [Theory] [InlineData(true, "attach-metadata", MeetingStartPromptResponse.AttachMetadataToCurrentMeeting)] [InlineData(false, "skip", MeetingStartPromptResponse.Skip)] public async Task CalendarPromptReturnsImmediatelyAndPreservesOfferedActions(bool canAttach, string action, MeetingStartPromptResponse expected) { using var lifetime = new TestApplicationLifetime(); var clock = new ManualDesktopRequestTimeProvider(); using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger.Instance); IMeetingStartPromptService prompts = new MacOsMeetingStartPromptService(store, clock); var meeting = new CalendarMeeting("event-1", "Alpha planning", clock.GetUtcNow(), clock.GetUtcNow().AddMinutes(2)); MeetingStartPromptResponse? received = null; var showing = prompts.ShowPromptAsync(new MeetingStartPromptRequest(meeting, canAttach), (response, _) => { received = response; return Task.CompletedTask; }, CancellationToken.None); Assert.True(showing.IsCompletedSuccessfully); var request = Assert.Single(store.List()); Assert.Equal("meeting-start", request.Kind); Assert.Contains("Alpha planning", request.Message); Assert.Equal(DateTimeOffset.Parse("2026-10-03T10:02:00Z"), request.ExpiresAt); Assert.Equal(canAttach, request.Actions.Any(item => item.Id == "attach-metadata")); Assert.Equal(DesktopUserRequestRespondResult.Accepted, await store.RespondAsync(request.Id, action)); Assert.Equal(expected, received); } [Theory] [InlineData(false)] [InlineData(true)] public async Task ApprovalExpirationOrOriginCancellationNeverGrantsConsent(bool cancelOrigin) { using var lifetime = new TestApplicationLifetime(); using var origin = new CancellationTokenSource(); var clock = new ManualDesktopRequestTimeProvider(); using var store = new DesktopUserRequestStore(clock, lifetime, NullLogger.Instance); IAgentApprovalPromptService prompts = new MacOsAgentApprovalPromptService(store); var decision = prompts.ConfirmAsync(new AgentApprovalPrompt("Workflow requested", "Update matching rule?", TimeSpan.FromMinutes(10)), origin.Token); var request = Assert.Single(store.List()); if (cancelOrigin) { await origin.CancelAsync(); await Assert.ThrowsAnyAsync(() => decision); } else { clock.Advance(TimeSpan.FromMinutes(10)); Assert.False(await decision); } Assert.Empty(store.List()); Assert.Equal(DesktopUserRequestRespondResult.NoLongerPending, await store.RespondAsync(request.Id, "yes")); } [Fact] public async Task DismissingInactivityLeavesUnrelatedApprovalPending() { using var lifetime = new TestApplicationLifetime(); using var store = CreateStore(lifetime); IAgentApprovalPromptService approvals = new MacOsAgentApprovalPromptService(store); var decision = approvals.ConfirmAsync(new AgentApprovalPrompt("Project requested", "Associate Alpha?", TimeSpan.FromMinutes(10)), CancellationToken.None); var approval = Assert.Single(store.List()); IMeetingInactivityPromptService inactivity = new MacOsMeetingInactivityPromptService(store); await inactivity.ShowStopPromptAsync(new MeetingInactivityPromptRequest(TimeSpan.FromMinutes(2), DateTimeOffset.UtcNow, 1, TimeSpan.FromMinutes(2)), (_, _) => Task.CompletedTask, CancellationToken.None); Assert.Equal(2, store.List().Count); await inactivity.DismissAllAsync(CancellationToken.None); Assert.Equal(approval.Id, Assert.Single(store.List()).Id); await store.RespondAsync(approval.Id, "no"); Assert.False(await decision); } private static DesktopUserRequestStore CreateStore(TestApplicationLifetime lifetime) => new( TimeProvider.System, lifetime, NullLogger.Instance); }