diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs index e2855bf6ad..fa57050433 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/MagenticOrchestrationTests.cs @@ -52,6 +52,135 @@ public class MagenticOrchestrationTests runResult.PendingRequests.Should().BeEmpty(); } + [Fact] + public async Task PlanReview_Approved_Proceeds() + { + // Arrange: Human approves initial plan + List factsResponse = CreatePlanResponse("Facts about executing the plan"); + List planResponse = CreatePlanResponse("Step 1: Execute the plan"); + List progressLedgerResponse = CreateProgressLedgerResponse( + isRequestSatisfied: true, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: "Execute"); + List finalAnswerResponse = CreateFinalAnswerResponse("Plan executed successfully"); + + TestReplayAgent manager = new( + [factsResponse, planResponse, progressLedgerResponse, finalAnswerResponse], + name: "Manager"); + TestEchoAgent worker = new(name: "Worker"); + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(worker) + .RequirePlanSignoff(true) + .Build(); + + CheckpointManager checkpointManager = CheckpointManager.CreateInMemory(); + + // Act: First run - should pause for plan review + WorkflowRunResult firstResult = await RunMagenticWorkflowAsync( + workflow, + [new ChatMessage(ChatRole.User, "Execute plan")], + checkpointManager: checkpointManager); + + firstResult.PendingRequests.Should().ContainSingle(); + ExternalRequest request = firstResult.PendingRequests[0].Request; + MagenticPlanReviewRequest? reviewRequest = request.Data.As(); + reviewRequest.Should().NotBeNull(); + reviewRequest!.Plan.Text.Should().Contain("Execute the plan"); + + // Act: Resume with approval + MagenticPlanReviewResponse approval = reviewRequest.Approve(); + ExternalResponse response = request.CreateResponse(approval); + WorkflowRunResult secondResult = await ResumeMagenticWorkflowAsync( + workflow, + response, + checkpointManager, + firstResult.LastCheckpoint); + + // Assert + secondResult.Result.Should().NotBeNull(); + secondResult.Result![0].Text.Should().Contain("Plan executed successfully"); + } + + [Fact] + public async Task Initial_Plan_Emits_PlanCreatedEvent() + { + // Arrange + List factsResponse = CreatePlanResponse("Facts about the task"); + List planResponse = CreatePlanResponse("Step 1: Initial plan"); + List progressLedgerResponse = CreateProgressLedgerResponse( + isRequestSatisfied: true, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: "Execute"); + List finalAnswerResponse = CreateFinalAnswerResponse("Done"); + + TestReplayAgent manager = new( + [factsResponse, planResponse, progressLedgerResponse, finalAnswerResponse], + name: "Manager"); + TestEchoAgent worker = new(name: "Worker"); + + List collectedEvents = []; + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(worker) + .RequirePlanSignoff(false) + .Build(); + + // Act + await RunMagenticWorkflowAsync( + workflow, + [new ChatMessage(ChatRole.User, "Do task")], + eventCollector: collectedEvents); + + // Assert + collectedEvents.OfType().Should().NotBeEmpty(); + MagenticPlanCreatedEvent planEvent = collectedEvents.OfType().First(); + planEvent.FullTaskLedger.Should().NotBeNull(); + } + + [Fact] + public async Task ProgressLedger_Updated_Event_Emitted() + { + // Arrange + List factsResponse = CreatePlanResponse("Facts about the task"); + List planResponse = CreatePlanResponse("Step 1: Execute"); + List progressLedgerResponse = CreateProgressLedgerResponse( + isRequestSatisfied: true, + isInLoop: false, + isProgressBeingMade: true, + nextSpeaker: "Worker", + instructionOrQuestion: "Execute"); + List finalAnswerResponse = CreateFinalAnswerResponse("Done"); + + TestReplayAgent manager = new( + [factsResponse, planResponse, progressLedgerResponse, finalAnswerResponse], + name: "Manager"); + TestEchoAgent worker = new(name: "Worker"); + + List collectedEvents = []; + + Workflow workflow = new MagenticWorkflowBuilder(manager) + .AddParticipants(worker) + .RequirePlanSignoff(false) + .Build(); + + // Act + await RunMagenticWorkflowAsync( + workflow, + [new ChatMessage(ChatRole.User, "Do task")], + eventCollector: collectedEvents); + + // Assert + collectedEvents.OfType().Should().NotBeEmpty(); + MagenticProgressLedgerUpdatedEvent ledgerEvent = collectedEvents.OfType().First(); + ledgerEvent.ProgressLedger.Should().NotBeNull(); + ledgerEvent.ProgressLedger.IsRequestSatisfied.Should().BeTrue(); + } + #region Helper Methods private sealed record WorkflowRunResult( @@ -115,6 +244,26 @@ public class MagenticOrchestrationTests ]; } + private static async Task ResumeMagenticWorkflowAsync( + Workflow workflow, + ExternalResponse response, + CheckpointManager checkpointManager, + CheckpointInfo? fromCheckpoint, + List? eventCollector = null) + { + InProcessExecutionEnvironment environment = ExecutionEnvironment.InProcess_Lockstep + .ToWorkflowExecutionEnvironment() + .WithCheckpointing(checkpointManager); + + await using StreamingRun run = fromCheckpoint != null + ? await environment.ResumeStreamingAsync(workflow, fromCheckpoint) + : await environment.OpenStreamingAsync(workflow); + + await run.SendResponseAsync(response); + + return await ProcessWorkflowRunAsync(run, eventCollector); + } + private static async Task RunMagenticWorkflowAsync( Workflow workflow, List input,