Checkpoint

This commit is contained in:
Chris Rickman
2026-03-02 22:00:21 -08:00
Unverified
parent 42d424587d
commit d6c4dbea96
30 changed files with 740 additions and 525 deletions
@@ -14,11 +14,14 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task EmptyStrategies_ReturnsUnmodifiedAsync()
{
// Arrange
ChatHistoryCompactionPipeline pipeline = new([]);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
// Assert
Assert.False(result.AnyApplied);
Assert.Equal(1, result.Before.MessageCount);
Assert.Equal(1, result.After.MessageCount);
@@ -28,6 +31,7 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ChainsStrategies_InOrderAsync()
{
// Arrange
ChatHistoryCompactionStrategy[] strategies =
[
new NeverCompactStrategy(),
@@ -40,8 +44,10 @@ public class ChatHistoryCompactionPipelineTests
new(ChatRole.User, "Second"),
];
// Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
// Assert
Assert.True(result.AnyApplied);
Assert.Equal(2, result.StrategyResults.Count);
Assert.False(result.StrategyResults[0].Applied);
@@ -52,6 +58,7 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ReportsOverallMetricsAsync()
{
// Arrange
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
List<ChatMessage> messages =
[
@@ -60,8 +67,10 @@ public class ChatHistoryCompactionPipelineTests
new(ChatRole.User, "Third"),
];
// Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
// Assert
Assert.Equal(3, result.Before.MessageCount);
Assert.Equal(2, result.After.MessageCount);
}
@@ -69,51 +78,39 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task CustomMetricsCalculator_IsUsedAsync()
{
// Arrange
Moq.Mock<IChatHistoryMetricsCalculator> calcMock = new();
calcMock
.Setup(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()))
.Returns(new CompactionMetric { MessageCount = 42 });
.Returns(new ChatHistoryMetric { MessageCount = 42 });
ChatHistoryCompactionPipeline pipeline = new(calcMock.Object, []);
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
// Assert
Assert.Equal(42, result.Before.MessageCount);
calcMock.Verify(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()), Moq.Times.AtLeast(2));
}
[Fact]
public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
{
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
NonReadOnlyList<ChatMessage> messages = new(
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
]);
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
Assert.True(result.AnyApplied);
Assert.Single(messages);
calcMock.Verify(c => c.Calculate(Moq.It.IsAny<IReadOnlyList<ChatMessage>>()), Moq.Times.Once);
}
[Fact]
public async Task ReduceAsync_DelegatesCompactionAsync()
{
// Arrange
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
ChatMessage[] messages =
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
new(ChatRole.User, "Third"),
];
IEnumerable<ChatMessage> result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
// Act
IEnumerable<ChatMessage> result = await pipeline.ReduceAsync(messages, default);
List<ChatMessage> resultList = result.ToList();
// Assert
Assert.Equal(2, resultList.Count);
Assert.Equal("Second", resultList[0].Text);
Assert.Equal("Third", resultList[1].Text);
@@ -122,16 +119,18 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ReduceAsync_EmptyStrategies_ReturnsAllMessagesAsync()
{
// Arrange
ChatHistoryCompactionPipeline pipeline = new([]);
ChatMessage[] messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.User, "World"),
];
IEnumerable<ChatMessage> result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
// Act
IEnumerable<ChatMessage> result = await pipeline.ReduceAsync(messages, default);
// Assert
Assert.Equal(2, result.Count());
}
}
@@ -17,28 +17,32 @@ public class ChatHistoryCompactionStrategyTests
[Fact]
public async Task ShouldCompactReturnsFalse_SkipsAsync()
{
NeverCompactStrategy strategy = new();
// Arrange
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
DefaultChatHistoryMetricsCalculator calculator = new();
NeverCompactStrategy strategy = new();
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
// Assert
Assert.False(result.Applied);
}
[Fact]
public async Task ShouldCompactReturnsTrue_RunsCompactionAsync()
{
RemoveFirstMessageStrategy strategy = new();
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
RemoveFirstMessageStrategy strategy = new();
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
// Assert
Assert.True(result.Applied);
Assert.Single(messages);
Assert.Equal("Second", messages[0].Text);
@@ -47,23 +51,24 @@ public class ChatHistoryCompactionStrategyTests
}
[Fact]
public async Task DelegatesToIChatReducerAsync()
public async Task DelegatesToReducerAsync()
{
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs.Skip(1));
TestCompactionStrategy strategy = new(reducerMock.Object);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> messages, CancellationToken _) => messages.Skip(1));
TestCompactionStrategy strategy = new(reducerMock.Object);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
// Assert
Assert.True(result.Applied);
Assert.Single(messages);
Assert.Equal("Second", messages[0].Text);
@@ -73,88 +78,55 @@ public class ChatHistoryCompactionStrategyTests
[Fact]
public async Task ReducerNoChange_ReturnsFalseAsync()
{
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
TestCompactionStrategy strategy = new(reducerMock.Object);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act
CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
// Assert
Assert.False(result.Applied);
Assert.Single(messages);
}
[Fact]
public void ExposesReducer()
public void ReducerLifecycle()
{
// Arrange
Mock<IChatReducer> reducerMock = new();
// Act
TestCompactionStrategy strategy = new(reducerMock.Object);
// Assert
Assert.Same(reducerMock.Object, strategy.Reducer);
}
[Fact]
public void DefaultName_IsReducerTypeName()
{
Mock<IChatReducer> reducerMock = new();
TestCompactionStrategy strategy = new(reducerMock.Object);
// Moq proxy type name is used since we're using a mock
Assert.NotNull(strategy.Name);
Assert.NotEmpty(strategy.Name);
}
[Fact]
public void ConditionDelegate_ReturnsTrue_ShouldCompactReturnsTrue()
{
Mock<IChatReducer> reducerMock = new();
TestCompactionStrategy strategy = new(reducerMock.Object);
CompactionMetric metrics = new() { TokenCount = 100 };
Assert.True(strategy.ShouldCompact(metrics));
}
[Fact]
public void ConditionDelegate_ReturnsFalse_ShouldCompactReturnsFalse()
{
Mock<IChatReducer> reducerMock = new();
TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
CompactionMetric metrics = new() { TokenCount = 100 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
{
RemoveFirstMessageStrategy strategy = new();
NonReadOnlyList<ChatMessage> messages = new(
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
]);
DefaultChatHistoryMetricsCalculator calculator = new();
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.True(result.Applied);
Assert.Single(messages);
Assert.Equal("Second", messages[0].Text);
Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
}
[Fact]
public void CurrentMetrics_OutsideStrategy_Throws()
{
// Act & Assert
Assert.Throws<InvalidOperationException>(() => TestCompactionStrategy.GetCurrentMetrics());
}
public static async ValueTask<CompactionResult> RunCompactionStrategyAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages)
{
// Act
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
return await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
}
private sealed class TestCompactionStrategy : ChatHistoryCompactionStrategy
{
private readonly bool _shouldCompact;
@@ -165,8 +137,8 @@ public class ChatHistoryCompactionStrategyTests
this._shouldCompact = shouldCompact;
}
public override bool ShouldCompact(CompactionMetric metrics) => this._shouldCompact;
protected override bool ShouldCompact(ChatHistoryMetric metrics) => this._shouldCompact;
public static CompactionMetric GetCurrentMetrics() => CurrentMetrics;
public static ChatHistoryMetric GetCurrentMetrics() => CurrentMetrics;
}
}
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public class ChatReducerCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public async Task ConditionFalse_SkipsAsync()
{
// Arrange
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
Mock<IChatReducer> reducerMock = new();
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => false);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
// Assert
reducerMock.Verify(
r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task ConditionTrue_RunsReducerAsync()
{
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
];
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs.Skip(1));
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
// Assert
Assert.Equal("Second", messages[0].Text);
reducerMock.Verify(
r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task ConditionReceivesMetricsAsync()
{
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
ChatHistoryMetric? capturedMetrics = null;
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
ChatReducerCompactionStrategy strategy = new(
reducerMock.Object,
metrics =>
{
capturedMetrics = metrics;
return false;
});
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
// Assert
Assert.NotNull(capturedMetrics);
Assert.Equal(2, capturedMetrics!.MessageCount);
}
[Fact]
public async Task ReducerNoChange_AppliedFalseAsync()
{
// Arrange
List<ChatMessage> messages = [new(ChatRole.User, "Hello")];
Mock<IChatReducer> reducerMock = new();
reducerMock
.Setup(r => r.ReduceAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((IEnumerable<ChatMessage> msgs, CancellationToken _) => msgs);
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public void Name_ReturnsReducerTypeName()
{
// Arrange
Mock<IChatReducer> reducerMock = new();
// Act
ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
// Assert
Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
}
}
@@ -9,7 +9,10 @@ public class CompactionMetricTests
[Fact]
public void DefaultValues_AreZero()
{
CompactionMetric metrics = new();
// Arrange & Act
ChatHistoryMetric metrics = new();
// Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -21,7 +24,8 @@ public class CompactionMetricTests
[Fact]
public void InitProperties_SetCorrectly()
{
CompactionMetric metrics = new()
// Arrange & Act
ChatHistoryMetric metrics = new()
{
TokenCount = 100,
ByteCount = 500,
@@ -30,6 +34,7 @@ public class CompactionMetricTests
UserTurnCount = 3
};
// Assert
Assert.Equal(100, metrics.TokenCount);
Assert.Equal(500L, metrics.ByteCount);
Assert.Equal(5, metrics.MessageCount);
@@ -11,13 +11,16 @@ public class CompactionPipelineResultTests
[Fact]
public void Properties_AreReadable()
{
CompactionMetric before = new() { MessageCount = 10 };
CompactionMetric after = new() { MessageCount = 5 };
// Arrange
ChatHistoryMetric before = new() { MessageCount = 10 };
ChatHistoryMetric after = new() { MessageCount = 5 };
CompactionResult strategyResult = new("Test", applied: true, before, after);
List<CompactionResult> results = [strategyResult];
// Act
CompactionPipelineResult pipelineResult = new(before, after, results);
// Assert
Assert.Same(before, pipelineResult.Before);
Assert.Same(after, pipelineResult.After);
Assert.Single(pipelineResult.StrategyResults);
@@ -26,21 +29,29 @@ public class CompactionPipelineResultTests
[Fact]
public void AnyApplied_AllFalse_ReturnsFalse()
{
CompactionMetric metrics = new() { MessageCount = 5 };
// Arrange
ChatHistoryMetric metrics = new() { MessageCount = 5 };
CompactionResult skipped = CompactionResult.Skipped("Skip", metrics);
// Act
CompactionPipelineResult result = new(metrics, metrics, [skipped]);
// Assert
Assert.False(result.AnyApplied);
}
[Fact]
public void AnyApplied_SomeTrue_ReturnsTrue()
{
CompactionMetric before = new() { MessageCount = 10 };
CompactionMetric after = new() { MessageCount = 5 };
// Arrange
ChatHistoryMetric before = new() { MessageCount = 10 };
ChatHistoryMetric after = new() { MessageCount = 5 };
CompactionResult applied = new("Applied", applied: true, before, after);
// Act
CompactionPipelineResult result = new(before, after, [applied]);
// Assert
Assert.True(result.AnyApplied);
}
}
@@ -9,9 +9,13 @@ public class CompactionResultTests
[Fact]
public void Skipped_HasSameBeforeAndAfter()
{
CompactionMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
// Arrange
ChatHistoryMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
// Act
CompactionResult result = CompactionResult.Skipped("Test", metrics);
// Assert
Assert.Equal("Test", result.StrategyName);
Assert.False(result.Applied);
Assert.Same(metrics, result.Before);
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public abstract class CompactionStrategyTestBase
{
public static async ValueTask<CompactionResult> RunCompactionStrategyReducedAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages, int expectedCount)
{
// Act
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
// Assert
Assert.True(result.Applied);
Assert.NotEqual(result.Before, result.After);
Assert.Equal(expectedCount, messages.Count);
return result;
}
public static async ValueTask<CompactionResult> RunCompactionStrategySkippedAsync(ChatHistoryCompactionStrategy strategy, List<ChatMessage> messages)
{
// Act
int initialCount = messages.Count;
ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
// Assert
Assert.False(result.Applied);
Assert.Equal(result.Before, result.After);
Assert.Equal(initialCount, messages.Count);
return result;
}
}
@@ -11,9 +11,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void EmptyList_ReturnsZeroMetrics()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
CompactionMetric metrics = calculator.Calculate([]);
// Act
ChatHistoryMetric metrics = calculator.Calculate([]);
// Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -24,6 +28,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void CountsMessages()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
@@ -31,14 +36,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Hi there"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(2, metrics.MessageCount);
}
[Fact]
public void CountsUserTurns()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
@@ -48,14 +56,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Good"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(2, metrics.UserTurnCount);
}
[Fact]
public void CountsToolCalls()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("call1", "get_weather", new Dictionary<string, object?> { ["city"] = "NYC" }),
@@ -67,14 +78,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
assistantMsg,
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(2, metrics.ToolCallCount);
}
[Fact]
public void ConsecutiveUserMessages_CountAsOneTurn()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
@@ -83,22 +97,27 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Reply"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(1, metrics.UserTurnCount);
}
[Fact]
public void TokenCount_IsPositive()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello world"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.True(metrics.TokenCount > 0);
Assert.True(metrics.ByteCount > 0);
}
@@ -106,9 +125,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void NullInput_ReturnsZeroMetrics()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
CompactionMetric metrics = calculator.Calculate(null!);
// Act
ChatHistoryMetric metrics = calculator.Calculate(null!);
// Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -118,45 +141,50 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void InvalidCharsPerToken_UsesDefault()
{
// A non-positive charsPerToken should fall back to the default (4)
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new(charsPerToken: 0);
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello world"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// With default 4 chars/token: "Hello world" = 11 chars → 11/4=2 + 4 overhead = 6 tokens
// Assert
Assert.True(metrics.TokenCount > 0);
}
[Fact]
public void NullMessageText_HandledGracefully()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
// Message with no text content — Text returns null
ChatMessage msg = new() { Role = ChatRole.User };
List<ChatMessage> messages = [msg];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(1, metrics.MessageCount);
// Null text → empty string → 0 bytes, only overhead tokens
Assert.True(metrics.TokenCount > 0); // per-message overhead
Assert.True(metrics.TokenCount > 0);
Assert.Equal(0L, metrics.ByteCount);
}
[Fact]
public void NullContents_SkipsToolCounting()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage msg = new(ChatRole.User, "text");
msg.Contents = null!;
List<ChatMessage> messages = [msg];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(1, metrics.MessageCount);
Assert.Equal(0, metrics.ToolCallCount);
}
@@ -164,16 +192,18 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void MessageWithOnlyNonTextContent_NullTextHandled()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
// FunctionCallContent-only message has null Text
ChatMessage msg = new(ChatRole.Assistant,
[
new FunctionCallContent("c1", "func"),
]);
List<ChatMessage> messages = [msg];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(1, metrics.MessageCount);
Assert.Equal(1, metrics.ToolCallCount);
}
@@ -181,6 +211,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void Calculate_PopulatesGroupIndex()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
@@ -189,8 +220,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Hi there"),
];
CompactionMetric metrics = calculator.Calculate(messages);
// Act
ChatHistoryMetric metrics = calculator.Calculate(messages);
// Assert
Assert.Equal(3, metrics.Groups.Count);
Assert.Equal(ChatMessageGroupKind.System, metrics.Groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, metrics.Groups[1].Kind);
@@ -200,23 +233,30 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void EmptyList_GroupIndexIsEmpty()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
CompactionMetric metrics = calculator.Calculate([]);
// Act
ChatHistoryMetric metrics = calculator.Calculate([]);
// Assert
Assert.Empty(metrics.Groups);
}
[Fact]
public void GroupIndex_SystemMessage_IdentifiedCorrectly()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant"),
];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
Assert.Equal(0, groups[0].StartIndex);
@@ -226,6 +266,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_AssistantWithToolCalls_GroupedWithResults()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("call1", "get_weather", new Dictionary<string, object?> { ["city"] = "NYC" }),
@@ -240,18 +281,21 @@ public class DefaultChatHistoryMetricsCalculatorTests
toolResult,
];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Equal(2, groups.Count);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[1].Kind);
Assert.Equal(1, groups[1].StartIndex);
Assert.Equal(2, groups[1].Count); // assistant + tool result
Assert.Equal(2, groups[1].Count);
}
[Fact]
public void GroupIndex_MultipleToolResults_GroupedTogether()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("c1", "func1"),
@@ -261,8 +305,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
ChatMessage tool2 = new(ChatRole.Tool, [new FunctionResultContent("c2", "result2")]);
List<ChatMessage> messages = [assistantMsg, tool1, tool2];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[0].Kind);
Assert.Equal(3, groups[0].Count);
@@ -271,6 +317,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_ComplexConversation_CorrectGrouping()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
@@ -283,29 +330,34 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "It's sunny!"),
];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Equal(6, groups.Count);
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[1].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[2].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[3].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[4].Kind);
Assert.Equal(2, groups[4].Count); // assistant + tool
Assert.Equal(2, groups[4].Count);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[5].Kind);
}
[Fact]
public void GroupIndex_OrphanToolResult_IdentifiedCorrectly()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "orphan result")]),
];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.ToolResult, groups[0].Kind);
}
@@ -313,14 +365,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_UnknownRole_IdentifiedAsOther()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage> messages =
[
new(new ChatRole("custom"), "custom message"),
];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.Other, groups[0].Kind);
}
@@ -328,13 +383,16 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_AssistantWithNullContents_ClassifiedAsPlain()
{
// Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage msg = new(ChatRole.Assistant, "reply");
msg.Contents = null!;
List<ChatMessage> messages = [msg];
// Act
IReadOnlyList<ChatMessageGroup> groups = calculator.Calculate(messages).Groups;
// Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[0].Kind);
}
@@ -16,7 +16,8 @@ internal sealed class NeverCompactStrategy : ChatHistoryCompactionStrategy
}
public override string Name => "NeverCompact";
public override bool ShouldCompact(CompactionMetric metrics) => false;
protected override bool ShouldCompact(ChatHistoryMetric metrics) => false;
private sealed class NoOpReducer : IChatReducer
{
@@ -1,40 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction.Internal;
/// <summary>
/// An IList&lt;T&gt; that does NOT implement IReadOnlyList&lt;T&gt;,
/// used to test the defensive <c>as IReadOnlyList&lt;T&gt; ?? fallback</c> patterns.
/// </summary>
internal sealed class NonReadOnlyList<T> : IList<T>
{
private readonly List<T> _inner;
public NonReadOnlyList(IEnumerable<T> items)
{
this._inner = items.ToList();
}
public T this[int index]
{
get => this._inner[index];
set => this._inner[index] = value;
}
public int Count => this._inner.Count;
public bool IsReadOnly => false;
public void Add(T item) => this._inner.Add(item);
public void Clear() => this._inner.Clear();
public bool Contains(T item) => this._inner.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => this._inner.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => this._inner.GetEnumerator();
public int IndexOf(T item) => this._inner.IndexOf(item);
public void Insert(int index, T item) => this._inner.Insert(index, item);
public bool Remove(T item) => this._inner.Remove(item);
public void RemoveAt(int index) => this._inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
@@ -18,7 +18,7 @@ internal sealed class RemoveFirstMessageStrategy : ChatHistoryCompactionStrategy
public override string Name => "RemoveFirst";
public override bool ShouldCompact(CompactionMetric metrics) => metrics.MessageCount > 0;
protected override bool ShouldCompact(ChatHistoryMetric metrics) => metrics.MessageCount > 0;
private sealed class RemoveFirstReducer : IChatReducer
{
@@ -9,10 +9,12 @@ public class MessageGroupTests
[Fact]
public void Equality_Works()
{
// Arrange
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup c = new(1, 2, ChatMessageGroupKind.AssistantToolGroup);
// Act & Assert
Assert.Equal(a, b);
Assert.True(a == b);
Assert.NotEqual(a, c);
@@ -22,34 +24,42 @@ public class MessageGroupTests
[Fact]
public void Equals_Object_NullReturnsFalse()
{
// Arrange
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
// Act & Assert
Assert.False(group.Equals(null));
}
[Fact]
public void Equals_Object_BoxedMessageGroupReturnsTrue()
{
// Arrange
ChatMessageGroup group = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
object boxed = new ChatMessageGroup(0, 2, ChatMessageGroupKind.AssistantToolGroup);
// Act & Assert
Assert.True(group.Equals(boxed));
}
[Fact]
public void Equals_Object_WrongTypeReturnsFalse()
{
// Arrange
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
// Act & Assert
Assert.False(group.Equals("not a MessageGroup"));
}
[Fact]
public void GetHashCode_ConsistentForEqualInstances()
{
// Arrange
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
// Act & Assert
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
}
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -7,47 +7,27 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public class SlidingWindowCompactionStrategyTests
public class SlidingWindowCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public void ShouldCompact_UnderLimit_ReturnsFalse()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
CompactionMetric metrics = new() { UserTurnCount = 3 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public void ShouldCompact_OverLimit_ReturnsTrue()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
CompactionMetric metrics = new() { UserTurnCount = 5 };
Assert.True(strategy.ShouldCompact(metrics));
}
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.False(result.Applied);
Assert.Equal(2, messages.Count);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task KeepsLastNTurnsAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Turn 1"),
@@ -57,12 +37,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 3"),
new(ChatRole.Assistant, "Reply 3"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
Assert.True(result.Applied);
Assert.Equal(4, messages.Count);
// Assert
Assert.Equal("Turn 2", messages[0].Text);
Assert.Equal("Reply 2", messages[1].Text);
Assert.Equal("Turn 3", messages[2].Text);
@@ -72,7 +52,7 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task PreservesSystemMessagesAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helper"),
@@ -81,12 +61,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
Assert.True(result.Applied);
Assert.Equal(3, messages.Count);
// Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
Assert.Equal("Turn 2", messages[1].Text);
@@ -96,7 +76,7 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task PreservesToolGroupsWithinKeptTurnsAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Turn 1"),
@@ -106,37 +86,34 @@ public class SlidingWindowCompactionStrategyTests
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
new(ChatRole.Assistant, "It's sunny!"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
Assert.True(result.Applied);
// Turn 1 dropped, Turn 2 kept (user + assistant-tool-group + plain assistant)
Assert.Equal(4, messages.Count);
// Assert
Assert.Equal("Get weather", messages[0].Text);
}
[Fact]
public async Task SingleTurn_AtLimit_NoChangeAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.False(result.Applied);
Assert.Equal(2, messages.Count);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task DropsResponseGroupsFromOldTurnsAsync()
{
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Turn 1"),
@@ -146,12 +123,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
Assert.True(result.Applied);
Assert.Equal(2, messages.Count);
// Assert
Assert.Equal("Turn 2", messages[0].Text);
Assert.Equal("Reply 2", messages[1].Text);
}
@@ -9,44 +9,24 @@ using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public class SummarizationCompactionStrategyTests
public class SummarizationCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public void ShouldCompact_UnderLimit_ReturnsFalse()
{
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
CompactionMetric metrics = new() { TokenCount = 500 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public void ShouldCompact_OverLimit_ReturnsTrue()
{
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100);
CompactionMetric metrics = new() { TokenCount = 500 };
Assert.True(strategy.ShouldCompact(metrics));
}
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
Assert.False(result.Applied);
Assert.Equal(2, messages.Count);
// Assert
chatClientMock.Verify(
c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()),
Times.Never);
@@ -55,13 +35,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task SummarizesOldGroupsAsync()
{
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
// preserveRecentGroups=2 means keep last 2 non-system groups
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "What's the weather?"),
@@ -71,14 +45,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Thanks!"),
new(ChatRole.Assistant, "You're welcome!"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
Assert.True(result.Applied);
// 6 groups (3 user + 3 assistant), protect last 2 → summarize first 4 groups
// Result: summary + 2 protected groups = 3 messages
Assert.Equal(3, messages.Count);
// Assert
Assert.Contains("[Summary]", messages[0].Text);
Assert.Contains("sunny", messages[0].Text);
Assert.Equal("Thanks!", messages[1].Text);
@@ -88,12 +64,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task PreservesSystemMessagesAsync()
{
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helper"),
@@ -102,11 +73,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
Assert.True(result.Applied);
// Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
Assert.Contains("[Summary]", messages[1].Text);
@@ -115,20 +91,19 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task AllGroupsProtected_NoChangeAsync()
{
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatClient> chatClientMock = new();
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
// All groups protected → nothing to summarize → no change
Assert.False(result.Applied);
Assert.Equal(2, messages.Count);
// Assert
chatClientMock.Verify(
c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()),
Times.Never);
@@ -137,16 +112,8 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task CustomPrompt_UsedInRequestAsync()
{
// Arrange
const string CustomPrompt = "Summarize briefly.";
List<ChatMessage>? capturedMessages = null;
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
@@ -154,12 +121,19 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Second"),
new(ChatRole.Assistant, "Reply 2"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
List<ChatMessage>? capturedMessages = null;
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
// Assert
Assert.NotNull(capturedMessages);
// First message in request should be the custom system prompt
Assert.Equal(ChatRole.System, capturedMessages![0].Role);
Assert.Equal(CustomPrompt, capturedMessages[0].Text);
}
@@ -167,12 +141,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task NullResponseText_UsesFallbackAsync()
{
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "First"),
@@ -180,11 +149,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Second"),
new(ChatRole.Assistant, "Reply 2"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
Mock<IChatClient> chatClientMock = new();
chatClientMock
.Setup(c => c.GetResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
Assert.True(result.Applied);
// Assert
Assert.Contains("[Summary unavailable]", messages[0].Text);
}
}
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -7,57 +7,28 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public class ToolResultCompactionStrategyTests
public class ToolResultCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public void ShouldCompact_UnderLimit_ReturnsFalse()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 3 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public void ShouldCompact_OverLimitNoToolCalls_ReturnsFalse()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 100);
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 0 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public void ShouldCompact_OverLimitWithToolCalls_ReturnsTrue()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 100);
CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 2 };
Assert.True(strategy.ShouldCompact(metrics));
}
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "get_weather")]),
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.False(result.Applied);
Assert.Equal(3, messages.Count);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task CollapsesOldToolGroupAsync()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Check weather"),
@@ -66,13 +37,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
Assert.True(result.Applied);
// The old tool group (assistant+tool = 2 messages) should be collapsed to 1
Assert.Equal(4, messages.Count); // user + [collapsed] + user + assistant
// Assert
Assert.Contains("[Tool calls: get_weather]", messages[1].Text);
Assert.Equal(ChatRole.Assistant, messages[1].Role);
}
@@ -80,8 +50,7 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task ProtectsRecentGroupsAsync()
{
// With preserveRecentGroups=4, all groups are protected (5 groups, protect 4 non-system)
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Check weather"),
@@ -90,19 +59,16 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// All groups protected, so no collapse
Assert.False(result.Applied);
Assert.Equal(5, messages.Count);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task PreservesSystemMessagesAsync()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helper"),
@@ -112,11 +78,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 5);
Assert.True(result.Applied);
// Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
}
@@ -124,7 +91,7 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task MultipleToolCalls_ListedInSummaryAsync()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Do research"),
@@ -137,13 +104,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Summarize"),
new(ChatRole.Assistant, "Here's the summary."),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
Assert.True(result.Applied);
// Old tool group (1 assistant + 2 tools = 3 messages) collapsed to 1
Assert.Equal(4, messages.Count);
// Assert
Assert.Contains("search", messages[1].Text);
Assert.Contains("fetch_page", messages[1].Text);
}
@@ -151,18 +117,15 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task NoToolGroups_NoChangeAsync()
{
ToolResultCompactionStrategy strategy = new(maxTokens: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
ToolResultCompactionStrategy strategy = new(maxTokens: 1);
// ToolCallCount is 0, so ShouldCompact returns false
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.False(result.Applied);
Assert.Equal(2, messages.Count);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
}
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -7,29 +7,26 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
public class TruncationCompactionStrategyTests
public class TruncationCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
TruncationCompactionStrategy strategy = new(maxTokens: 100000);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Hello"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
TruncationCompactionStrategy strategy = new(maxTokens: 100000);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.False(result.Applied);
Assert.Single(messages);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task OverLimit_RemovesOldestGroupsAsync()
{
// Use a very low max to trigger compaction
TruncationCompactionStrategy strategy = new(maxTokens: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "First message"),
@@ -37,77 +34,45 @@ public class TruncationCompactionStrategyTests
new(ChatRole.User, "Second message"),
new(ChatRole.Assistant, "Second reply"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
TruncationCompactionStrategy strategy = new(maxTokens: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.True(result.Applied);
// Should have removed old groups, keeping at least the last one
Assert.True(messages.Count < 4);
Assert.True(messages.Count > 0);
}
[Fact]
public void ShouldCompact_ReturnsFalseWhenUnderLimit()
{
TruncationCompactionStrategy strategy = new(maxTokens: 10000);
CompactionMetric metrics = new() { TokenCount = 500 };
Assert.False(strategy.ShouldCompact(metrics));
}
[Fact]
public void ShouldCompact_ReturnsTrueWhenOverLimit()
{
TruncationCompactionStrategy strategy = new(maxTokens: 100);
CompactionMetric metrics = new() { TokenCount = 500 };
Assert.True(strategy.ShouldCompact(metrics));
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
}
[Fact]
public async Task SystemOnlyMessages_NoChangeAsync()
{
// Only system messages → removableGroups.Count == 0 → no change
TruncationCompactionStrategy strategy = new(maxTokens: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helper"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
TruncationCompactionStrategy strategy = new(maxTokens: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// ShouldCompact triggers but reducer finds nothing removable
Assert.Single(messages);
Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task SingleNonSystemGroup_NoChangeAsync()
{
// Only one non-system group → maxRemovable <= 0 → no change
TruncationCompactionStrategy strategy = new(maxTokens: 1);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.System, "System prompt"),
new(ChatRole.User, "Only user message"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
TruncationCompactionStrategy strategy = new(maxTokens: 1);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
Assert.Equal(2, messages.Count);
Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
// Act & Assert
await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task PreserveRecentGroups_KeepsMultipleGroupsAsync()
{
// preserveRecentGroups=2 means keep at least the last 2 non-system groups
TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
// Arrange
List<ChatMessage> messages =
[
new(ChatRole.User, "Turn 1"),
@@ -117,14 +82,12 @@ public class TruncationCompactionStrategyTests
new(ChatRole.User, "Turn 3"),
new(ChatRole.Assistant, "Reply 3"),
];
DefaultChatHistoryMetricsCalculator calculator = new();
TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
CompactionResult result = await strategy.CompactAsync(messages, calculator);
// Act & Assert
await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
Assert.True(result.Applied);
// 6 groups (3 user + 3 assistant), protect last 2 → 4 removable
// Should keep at least: Turn 3 user + Reply 3 assistant (last 2 groups)
Assert.True(messages.Count >= 2);
// Assert
Assert.Equal("Reply 3", messages[^1].Text);
}
}