mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Checkpoint
This commit is contained in:
+23
-24
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+45
-73
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+114
@@ -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);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -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);
|
||||
|
||||
+16
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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);
|
||||
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
-21
@@ -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);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
{
|
||||
|
||||
-40
@@ -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<T> that does NOT implement IReadOnlyList<T>,
|
||||
/// used to test the defensive <c>as IReadOnlyList<T> ?? 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();
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
+10
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+30
-53
@@ -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);
|
||||
}
|
||||
|
||||
+51
-77
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-66
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-60
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user