// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; using Moq.Protected; namespace Microsoft.Agents.AI.UnitTests; /// /// Shared helpers used by the LoopAgent and LoopEvaluator unit tests. /// internal static class LoopTestHelpers { /// /// Creates a that re-invokes the agent (without feedback) while the /// supplied predicate returns . /// public static DelegateLoopEvaluator While(Func shouldReinvoke) => new((context, _) => new ValueTask( shouldReinvoke(context) ? LoopEvaluation.Continue() : LoopEvaluation.Stop())); /// /// Creates a mocked judge that always returns the supplied response text. /// public static IChatClient CreateJudgeClient(string responseText) { var mock = new Mock(); mock.Setup(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, responseText))); return mock.Object; } /// /// Creates a mocked judge that always returns the supplied response text and captures the /// messages it was invoked with via . /// public static IChatClient CreateCapturingJudgeClient(string responseText, out List capturedMessages) { var captured = new List(); capturedMessages = captured; var mock = new Mock(); mock.Setup(c => c.GetResponseAsync( It.IsAny>(), It.IsAny(), It.IsAny())) .Callback, ChatOptions?, CancellationToken>((messages, _, _) => { captured.Clear(); captured.AddRange(messages); }) .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, responseText))); return mock.Object; } public static async IAsyncEnumerable ToAsyncEnumerableAsync( IEnumerable items, [EnumeratorCancellation] CancellationToken cancellationToken = default) { foreach (var item in items) { cancellationToken.ThrowIfCancellationRequested(); yield return item; await Task.Yield(); } } } /// /// Captures the messages sent to a mocked non-streaming inner agent and produces responses by call index. /// internal sealed class InnerAgentCapture { public InnerAgentCapture(Func responseFactory) { this.Mock .Protected() .Setup>("RunCoreAsync", ItExpr.IsAny>(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny()) .Callback, AgentSession?, AgentRunOptions?, CancellationToken>((msgs, session, _, _) => { this.CallCount++; this.MessagesPerCall.Add(msgs.ToList()); this.SessionsPerCall.Add(session); }) .ReturnsAsync(() => responseFactory(this.CallCount)); } public Mock Mock { get; } = new(); public AIAgent Agent => this.Mock.Object; public int CallCount { get; private set; } public List> MessagesPerCall { get; } = []; public List SessionsPerCall { get; } = []; } /// /// Captures the messages sent to a mocked streaming inner agent and produces updates by call index. /// internal sealed class InnerStreamingCapture { public InnerStreamingCapture(Func updatesFactory) { this.Mock .Protected() .Setup>("RunCoreStreamingAsync", ItExpr.IsAny>(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny()) .Returns, AgentSession?, AgentRunOptions?, CancellationToken>((msgs, _, _, ct) => { this.CallCount++; this.MessagesPerCall.Add(msgs.ToList()); return LoopTestHelpers.ToAsyncEnumerableAsync(updatesFactory(this.CallCount), ct); }); } public Mock Mock { get; } = new(); public AIAgent Agent => this.Mock.Object; public int CallCount { get; private set; } public List> MessagesPerCall { get; } = []; }