// 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;
///
/// Unit tests for the class.
///
public class LoggingAgentTests
{
[Fact]
public void Ctor_InvalidArgs_Throws()
{
var mockLogger = new Mock();
Assert.Throws("innerAgent", () => new LoggingAgent(null!, mockLogger.Object));
Assert.Throws("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();
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();
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();
var agent = new LoggingAgent(new TestAIAgent(), mockLogger.Object);
// Act & Assert
Assert.Throws(() => agent.JsonSerializerOptions = null!);
}
[Fact]
public async Task RunAsync_LogsAtDebugLevelAsync()
{
// Arrange
var mockLogger = new Mock();
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 messages = [new(ChatRole.User, "Hello")];
// Act
await agent.RunAsync(messages);
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("RunAsync invoked")),
null,
It.IsAny>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("RunAsync completed")),
null,
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunAsync_LogsAtTraceLevel_IncludesSensitiveDataAsync()
{
// Arrange
var mockLogger = new Mock();
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 messages = [new(ChatRole.User, "Hello")];
// Act
await agent.RunAsync(messages);
// Assert
mockLogger.Verify(
l => l.Log(
LogLevel.Trace,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("RunAsync invoked")),
null,
It.IsAny>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Trace,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("RunAsync completed")),
null,
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunAsync_OnCancellation_LogsCanceledAsync()
{
// Arrange
var mockLogger = new Mock();
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 messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync(() => agent.RunAsync(messages));
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("canceled")),
null,
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunAsync_OnException_LogsFailedAsync()
{
// Arrange
var mockLogger = new Mock();
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 messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync(() => agent.RunAsync(messages));
mockLogger.Verify(
l => l.Log(
LogLevel.Error,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("failed")),
It.IsAny(),
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_LogsAtDebugLevelAsync()
{
// Arrange
var mockLogger = new Mock();
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 CallbackAsync(
IEnumerable 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 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(),
It.Is((v, t) => v.ToString()!.Contains("RunStreamingAsync invoked")),
null,
It.IsAny>()),
Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("RunStreamingAsync completed")),
null,
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_LogsUpdatesAtTraceLevelAsync()
{
// Arrange
var mockLogger = new Mock();
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 CallbackAsync(
IEnumerable 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 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(),
It.Is((v, t) => v.ToString()!.Contains("received update")),
null,
It.IsAny>()),
Times.Exactly(2));
}
[Fact]
public async Task RunStreamingAsync_OnCancellation_LogsCanceledAsync()
{
// Arrange
var mockLogger = new Mock();
mockLogger.Setup(l => l.IsEnabled(LogLevel.Debug)).Returns(true);
var innerAgent = new TestAIAgent
{
RunStreamingAsyncFunc = CallbackAsync
};
static async IAsyncEnumerable CallbackAsync(
IEnumerable 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 messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync(async () =>
{
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
});
mockLogger.Verify(
l => l.Log(
LogLevel.Debug,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("canceled")),
null,
It.IsAny>()),
Times.Once);
}
[Fact]
public async Task RunStreamingAsync_OnException_LogsFailedAsync()
{
// Arrange
var mockLogger = new Mock();
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 CallbackAsync(
IEnumerable 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 messages = [new(ChatRole.User, "Hello")];
// Act & Assert
await Assert.ThrowsAsync(async () =>
{
await foreach (var update in agent.RunStreamingAsync(messages))
{
// Consume the stream
}
});
mockLogger.Verify(
l => l.Log(
LogLevel.Error,
It.IsAny(),
It.Is((v, t) => v.ToString()!.Contains("failed")),
It.IsAny(),
It.IsAny>()),
Times.Once);
}
}