// 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; } = [];
}