mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
a3a9147e61
* Rename AgentThread to AgentSession * Add more renames * Update readme files * Revert nullable variable change and further fixes. * Revert change in header name * Fix some comments and tests * Update changelog. * Address PR feedback. * Fixing code review comments. * Fix new errors after merging latest code.
401 lines
14 KiB
C#
401 lines
14 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Extensions.Logging;
|
|
using Moq;
|
|
|
|
namespace Microsoft.Agents.AI.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the <see cref="LoggingAgent"/> class.
|
|
/// </summary>
|
|
public class LoggingAgentTests
|
|
{
|
|
[Fact]
|
|
public void Ctor_InvalidArgs_Throws()
|
|
{
|
|
var mockLogger = new Mock<ILogger>();
|
|
Assert.Throws<ArgumentNullException>("innerAgent", () => new LoggingAgent(null!, mockLogger.Object));
|
|
Assert.Throws<ArgumentNullException>("logger", () => new LoggingAgent(new TestAIAgent(), null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Properties_DelegateToInnerAgent()
|
|
{
|
|
// Arrange
|
|
TestAIAgent innerAgent = new()
|
|
{
|
|
NameFunc = () => "TestAgent",
|
|
DescriptionFunc = () => "This is a test agent.",
|
|
};
|
|
|
|
var mockLogger = new Mock<ILogger>();
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
|
|
// Act & Assert
|
|
Assert.Equal("TestAgent", agent.Name);
|
|
Assert.Equal("This is a test agent.", agent.Description);
|
|
Assert.Equal(innerAgent.Id, agent.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void JsonSerializerOptions_Roundtrips()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
var agent = new LoggingAgent(new TestAIAgent(), mockLogger.Object);
|
|
JsonSerializerOptions options = new();
|
|
|
|
// Act
|
|
agent.JsonSerializerOptions = options;
|
|
|
|
// Assert
|
|
Assert.Same(options, agent.JsonSerializerOptions);
|
|
}
|
|
|
|
[Fact]
|
|
public void JsonSerializerOptions_SetNull_Throws()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
var agent = new LoggingAgent(new TestAIAgent(), mockLogger.Object);
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() => agent.JsonSerializerOptions = null!);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_LogsAtDebugLevelAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(false);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunAsyncFunc = async (messages, session, options, cancellationToken) =>
|
|
{
|
|
await Task.Yield();
|
|
return new AgentResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
|
|
}
|
|
};
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act
|
|
await agent.RunAsync(messages);
|
|
|
|
// Assert
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync invoked")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync completed")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_LogsAtTraceLevel_IncludesSensitiveDataAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunAsyncFunc = async (messages, session, options, cancellationToken) =>
|
|
{
|
|
await Task.Yield();
|
|
return new AgentResponse(new ChatMessage(ChatRole.Assistant, "Test response"));
|
|
}
|
|
};
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act
|
|
await agent.RunAsync(messages);
|
|
|
|
// Assert
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Trace,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync invoked")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Trace,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunAsync completed")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_OnCancellation_LogsCanceledAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunAsyncFunc = (messages, session, options, cancellationToken) =>
|
|
throw new OperationCanceledException()
|
|
};
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<OperationCanceledException>(() => agent.RunAsync(messages));
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("canceled")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunAsync_OnException_LogsFailedAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Error)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunAsyncFunc = (messages, session, options, cancellationToken) =>
|
|
throw new InvalidOperationException("Test exception")
|
|
};
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(messages));
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Error,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("failed")),
|
|
It.IsAny<Exception>(),
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunStreamingAsync_LogsAtDebugLevelAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(false);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunStreamingAsyncFunc = CallbackAsync
|
|
};
|
|
|
|
static async IAsyncEnumerable<AgentResponseUpdate> CallbackAsync(
|
|
IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.Yield();
|
|
yield return new AgentResponseUpdate(ChatRole.Assistant, "Test");
|
|
}
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act
|
|
await foreach (var update in agent.RunStreamingAsync(messages))
|
|
{
|
|
// Consume the stream
|
|
}
|
|
|
|
// Assert
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunStreamingAsync invoked")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("RunStreamingAsync completed")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunStreamingAsync_LogsUpdatesAtTraceLevelAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Trace)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunStreamingAsyncFunc = CallbackAsync
|
|
};
|
|
|
|
static async IAsyncEnumerable<AgentResponseUpdate> CallbackAsync(
|
|
IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.Yield();
|
|
yield return new AgentResponseUpdate(ChatRole.Assistant, "Update 1");
|
|
yield return new AgentResponseUpdate(ChatRole.Assistant, "Update 2");
|
|
}
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act
|
|
await foreach (var update in agent.RunStreamingAsync(messages))
|
|
{
|
|
// Consume the stream
|
|
}
|
|
|
|
// Assert
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Trace,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("received update")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Exactly(2));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunStreamingAsync_OnCancellation_LogsCanceledAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunStreamingAsyncFunc = CallbackAsync
|
|
};
|
|
|
|
static async IAsyncEnumerable<AgentResponseUpdate> CallbackAsync(
|
|
IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.Yield();
|
|
throw new OperationCanceledException();
|
|
// The following yield statement is required for async iterator methods but is unreachable.
|
|
// This pattern is intentional for testing exception scenarios in async iterators.
|
|
#pragma warning disable CS0162 // Unreachable code detected
|
|
yield break;
|
|
#pragma warning restore CS0162 // Unreachable code detected
|
|
}
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
|
|
{
|
|
await foreach (var update in agent.RunStreamingAsync(messages))
|
|
{
|
|
// Consume the stream
|
|
}
|
|
});
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Debug,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("canceled")),
|
|
null,
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RunStreamingAsync_OnException_LogsFailedAsync()
|
|
{
|
|
// Arrange
|
|
var mockLogger = new Mock<ILogger>();
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
|
|
mockLogger.Setup(l => l.IsEnabled(LogLevel.Error)).Returns(true);
|
|
|
|
var innerAgent = new TestAIAgent
|
|
{
|
|
RunStreamingAsyncFunc = CallbackAsync
|
|
};
|
|
|
|
static async IAsyncEnumerable<AgentResponseUpdate> CallbackAsync(
|
|
IEnumerable<ChatMessage> messages, AgentSession? session, AgentRunOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken)
|
|
{
|
|
await Task.Yield();
|
|
throw new InvalidOperationException("Test exception");
|
|
// The following yield statement is required for async iterator methods but is unreachable.
|
|
// This pattern is intentional for testing exception scenarios in async iterators.
|
|
#pragma warning disable CS0162 // Unreachable code detected
|
|
yield break;
|
|
#pragma warning restore CS0162 // Unreachable code detected
|
|
}
|
|
|
|
var agent = new LoggingAgent(innerAgent, mockLogger.Object);
|
|
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
|
|
|
|
// Act & Assert
|
|
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
|
{
|
|
await foreach (var update in agent.RunStreamingAsync(messages))
|
|
{
|
|
// Consume the stream
|
|
}
|
|
});
|
|
|
|
mockLogger.Verify(
|
|
l => l.Log(
|
|
LogLevel.Error,
|
|
It.IsAny<EventId>(),
|
|
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("failed")),
|
|
It.IsAny<Exception>(),
|
|
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
|
Times.Once);
|
|
}
|
|
}
|