Update tests

This commit is contained in:
Chris Rickman
2026-03-05 08:10:16 -08:00
Unverified
parent 7e2c5ad4e6
commit 06f55c0494
8 changed files with 606 additions and 54 deletions
@@ -67,11 +67,12 @@ public sealed class MessageIndex
this.Groups = groups;
this.Tokenizer = tokenizer;
// Restore turn counter from the last group that has a TurnIndex
for (int index = groups.Count - 1; index >= 0; --index)
{
if (this.Groups[0].TurnIndex.HasValue)
if (this.Groups[index].TurnIndex.HasValue)
{
this._currentTurn = this.Groups[0].TurnIndex!.Value;
this._currentTurn = this.Groups[index].TurnIndex!.Value;
break;
}
}
@@ -353,11 +354,6 @@ public sealed class MessageIndex
private static bool HasToolCalls(ChatMessage message)
{
if (message.Contents is null)
{
return false;
}
foreach (AIContent content in message.Contents)
{
if (content is FunctionCallContent)
@@ -157,11 +157,6 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
}
}
if (summarized == 0)
{
return false;
}
// Generate summary using the chat client (single LLM call for all marked groups)
ChatResponse response = await this.ChatClient.GetResponseAsync(
[
@@ -180,14 +175,7 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
ChatMessage summaryMessage = new(ChatRole.Assistant, $"[Summary]\n{summaryText}");
(summaryMessage.AdditionalProperties ??= [])[MessageGroup.SummaryPropertyKey] = true;
if (insertIndex >= 0)
{
index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]);
}
else
{
index.AddGroup(MessageGroupKind.Summary, [summaryMessage]);
}
index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]);
return true;
}
@@ -0,0 +1,400 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.UnitTests.Compaction;
/// <summary>
/// Contains tests for the <see cref="CompactingChatClient"/> class.
/// </summary>
public sealed class CompactingChatClientTests : IDisposable
{
/// <summary>
/// Restores the static <see cref="AIAgent.CurrentRunContext"/> after each test.
/// </summary>
public void Dispose()
{
SetCurrentRunContext(null);
}
[Fact]
public void ConstructorThrowsOnNullStrategyAsync()
{
Mock<IChatClient> mockInner = new();
Assert.Throws<ArgumentNullException>(() => new CompactingChatClient(mockInner.Object, null!));
}
[Fact]
public async Task GetResponseAsyncNoContextPassesThroughAsync()
{
// Arrange — no CurrentRunContext set → passthrough
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResponse);
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
CompactingChatClient client = new(mockInner.Object, strategy);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello"),
];
// Act
ChatResponse response = await client.GetResponseAsync(messages);
// Assert
Assert.Same(expectedResponse, response);
mockInner.Verify(c => c.GetResponseAsync(
messages,
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetResponseAsyncWithContextAppliesCompactionAsync()
{
// Arrange — set CurrentRunContext so compaction runs
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Done")]);
List<ChatMessage>? capturedMessages = null;
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) =>
capturedMessages = [.. msgs])
.ReturnsAsync(expectedResponse);
// Strategy that always triggers and keeps only 1 group
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
CompactingChatClient client = new(mockInner.Object, strategy);
TestAgentSession session = new();
SetRunContext(session);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
];
// Act
ChatResponse response = await client.GetResponseAsync(messages);
// Assert — compaction should have removed oldest groups
Assert.Same(expectedResponse, response);
Assert.NotNull(capturedMessages);
Assert.True(capturedMessages!.Count < messages.Count);
}
[Fact]
public async Task GetResponseAsyncNoCompactionNeededReturnsOriginalMessagesAsync()
{
// Arrange — trigger never fires → no compaction
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
List<ChatMessage>? capturedMessages = null;
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions?, CancellationToken>((msgs, _, _) =>
capturedMessages = [.. msgs])
.ReturnsAsync(expectedResponse);
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
CompactingChatClient client = new(mockInner.Object, strategy);
TestAgentSession session = new();
SetRunContext(session);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello"),
];
// Act
await client.GetResponseAsync(messages);
// Assert — original messages passed through
Assert.NotNull(capturedMessages);
Assert.Single(capturedMessages!);
Assert.Equal("Hello", capturedMessages[0].Text);
}
[Fact]
public async Task GetResponseAsyncWithExistingIndexUpdatesAsync()
{
// Arrange — call twice to exercise the "existing index" path (state.MessageIndex.Count > 0)
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "OK")]));
// Strategy that always triggers, keeping 1 group
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
CompactingChatClient client = new(mockInner.Object, strategy);
TestAgentSession session = new();
SetRunContext(session);
List<ChatMessage> messages1 =
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
];
// First call — initializes state
await client.GetResponseAsync(messages1);
List<ChatMessage> messages2 =
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.Assistant, "A2"),
new ChatMessage(ChatRole.User, "Q3"),
];
// Act — second call exercises the update path
ChatResponse response = await client.GetResponseAsync(messages2);
// Assert
Assert.NotNull(response);
}
[Fact]
public async Task GetResponseAsyncNullSessionReturnsOriginalAsync()
{
// Arrange — CurrentRunContext exists but Session is null
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResponse);
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
CompactingChatClient client = new(mockInner.Object, strategy);
// Set context with null session
SetRunContext(null);
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
// Act
ChatResponse response = await client.GetResponseAsync(messages);
// Assert
Assert.Same(expectedResponse, response);
}
[Fact]
public async Task GetStreamingResponseAsyncNoContextPassesThroughAsync()
{
// Arrange — no CurrentRunContext
Mock<IChatClient> mockInner = new();
ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Hi")];
mockInner.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(updates));
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
CompactingChatClient client = new(mockInner.Object, strategy);
List<ChatMessage> messages = [new ChatMessage(ChatRole.User, "Hello")];
// Act
List<ChatResponseUpdate> results = [];
await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages))
{
results.Add(update);
}
// Assert
Assert.Single(results);
Assert.Equal("Hi", results[0].Text);
}
[Fact]
public async Task GetStreamingResponseAsyncWithContextAppliesCompactionAsync()
{
// Arrange
Mock<IChatClient> mockInner = new();
ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Done")];
mockInner.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(updates));
TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1);
CompactingChatClient client = new(mockInner.Object, strategy);
TestAgentSession session = new();
SetRunContext(session);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
];
// Act
List<ChatResponseUpdate> results = [];
await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages))
{
results.Add(update);
}
// Assert
Assert.Single(results);
Assert.Equal("Done", results[0].Text);
}
[Fact]
public void GetServiceReturnsStrategyForMatchingType()
{
// Arrange
Mock<IChatClient> mockInner = new();
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
CompactingChatClient client = new(mockInner.Object, strategy);
// Act — typeof(Type).IsInstanceOfType(typeof(CompactionStrategy)) is true
object? result = client.GetService(typeof(Type));
// Assert
Assert.Same(strategy, result);
}
[Fact]
public void GetServiceDelegatesToBaseForNonMatchingType()
{
// Arrange
Mock<IChatClient> mockInner = new();
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
CompactingChatClient client = new(mockInner.Object, strategy);
// Act — typeof(string) doesn't match
object? result = client.GetService(typeof(string));
// Assert — delegates to base (which returns null for unregistered types)
Assert.Null(result);
}
[Fact]
public void GetServiceThrowsOnNullType()
{
Mock<IChatClient> mockInner = new();
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
CompactingChatClient client = new(mockInner.Object, strategy);
Assert.Throws<ArgumentNullException>(() => client.GetService(null!));
}
[Fact]
public void GetServiceWithServiceKeyDelegatesToBase()
{
// Arrange — non-null serviceKey always delegates
Mock<IChatClient> mockInner = new();
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
CompactingChatClient client = new(mockInner.Object, strategy);
// Act
object? result = client.GetService(typeof(Type), serviceKey: "mykey");
// Assert — delegates to base because serviceKey is non-null
Assert.Null(result);
}
[Fact]
public async Task GetResponseAsyncMessagesNotListCreatesListCopyAsync()
{
// Arrange — pass IEnumerable (not List<ChatMessage>) to exercise the list copy branch
ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]);
Mock<IChatClient> mockInner = new();
mockInner.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResponse);
TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000));
CompactingChatClient client = new(mockInner.Object, strategy);
TestAgentSession session = new();
SetRunContext(session);
// Use an IEnumerable (not a List) to trigger the copy path
IEnumerable<ChatMessage> messages = new ChatMessage[] { new(ChatRole.User, "Hello") };
// Act
ChatResponse response = await client.GetResponseAsync(messages);
// Assert
Assert.Same(expectedResponse, response);
}
/// <summary>
/// Sets <see cref="AIAgent.CurrentRunContext"/> via reflection.
/// </summary>
private static void SetCurrentRunContext(AgentRunContext? context)
{
FieldInfo? field = typeof(AIAgent).GetField("s_currentContext", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(field);
object? asyncLocal = field!.GetValue(null);
Assert.NotNull(asyncLocal);
PropertyInfo? valueProp = asyncLocal!.GetType().GetProperty("Value");
Assert.NotNull(valueProp);
valueProp!.SetValue(asyncLocal, context);
}
/// <summary>
/// Creates an <see cref="AgentRunContext"/> with the given session and sets it as the current context.
/// </summary>
private static void SetRunContext(AgentSession? session)
{
Mock<AIAgent> mockAgent = new() { CallBase = true };
AgentRunContext context = new(
mockAgent.Object,
session,
new List<ChatMessage> { new(ChatRole.User, "test") },
null);
SetCurrentRunContext(context);
}
private static async IAsyncEnumerable<ChatResponseUpdate> ToAsyncEnumerableAsync(
ChatResponseUpdate[] updates, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
foreach (ChatResponseUpdate update in updates)
{
cancellationToken.ThrowIfCancellationRequested();
yield return update;
await Task.CompletedTask;
}
}
private sealed class TestAgentSession : AgentSession;
}
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Buffers;
using System.Collections.Generic;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
@@ -675,4 +676,174 @@ public class MessageIndexTests
Assert.Equal(5, inserted.ByteCount);
Assert.Equal(1, inserted.TokenCount); // 5 / 4 = 1 (integer division)
}
[Fact]
public void ConstructorWithGroupsRestoresTurnIndex()
{
// Arrange — pre-existing groups with turn indices
MessageGroup group1 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q1")], 2, 1, turnIndex: 1);
MessageGroup group2 = new(MessageGroupKind.AssistantText, [new ChatMessage(ChatRole.Assistant, "A1")], 2, 1, turnIndex: 1);
MessageGroup group3 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q2")], 2, 1, turnIndex: 2);
List<MessageGroup> groups = [group1, group2, group3];
// Act — constructor should restore _currentTurn from the last group's TurnIndex
MessageIndex index = new(groups);
// Assert — adding a new user message should get turn 3 (restored 2 + 1)
index.Update(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.User, "Q3"),
]);
// The new user group should have TurnIndex 3
MessageGroup lastGroup = index.Groups[index.Groups.Count - 1];
Assert.Equal(MessageGroupKind.User, lastGroup.Kind);
Assert.NotNull(lastGroup.TurnIndex);
}
[Fact]
public void ConstructorWithEmptyGroupsHandlesGracefully()
{
// Arrange & Act — constructor with empty list
MessageIndex index = new([]);
// Assert
Assert.Empty(index.Groups);
}
[Fact]
public void ConstructorWithGroupsWithoutTurnIndexSkipsRestore()
{
// Arrange — groups without turn indices (system messages)
MessageGroup systemGroup = new(MessageGroupKind.System, [new ChatMessage(ChatRole.System, "Be helpful")], 10, 3, turnIndex: null);
List<MessageGroup> groups = [systemGroup];
// Act — constructor won't find a TurnIndex to restore
MessageIndex index = new(groups);
// Assert
Assert.Single(index.Groups);
}
[Fact]
public void ComputeTokenCountReturnsTokenCount()
{
// Arrange — call the public static method directly
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello world"),
new ChatMessage(ChatRole.Assistant, "Greetings"),
];
// Act — use a simple tokenizer that counts words (each word = 1 token)
SimpleWordTokenizer tokenizer = new();
int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer);
// Assert — "Hello world" = 2, "Greetings" = 1 → 3 total
Assert.Equal(3, tokenCount);
}
[Fact]
public void ComputeTokenCountEmptyTextReturnsZero()
{
// Arrange — message with no text content
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, [new FunctionCallContent("c1", "fn")]),
];
SimpleWordTokenizer tokenizer = new();
int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer);
// Assert — no text content → 0 tokens
Assert.Equal(0, tokenCount);
}
[Fact]
public void CreateWithTokenizerUsesTokenizerForCounts()
{
// Arrange
SimpleWordTokenizer tokenizer = new();
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello world test"),
];
// Act
MessageIndex index = MessageIndex.Create(messages, tokenizer);
// Assert — tokenizer counts words: "Hello world test" = 3 tokens
Assert.Single(index.Groups);
Assert.Equal(3, index.Groups[0].TokenCount);
Assert.NotNull(index.Tokenizer);
}
[Fact]
public void InsertGroupWithTokenizerUsesTokenizer()
{
// Arrange
SimpleWordTokenizer tokenizer = new();
MessageIndex index = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
], tokenizer);
// Act
ChatMessage msg = new(ChatRole.Assistant, "Hello world test message");
MessageGroup inserted = index.InsertGroup(0, MessageGroupKind.AssistantText, [msg]);
// Assert — tokenizer counts words: "Hello world test message" = 4 tokens
Assert.Equal(4, inserted.TokenCount);
}
/// <summary>
/// A simple tokenizer that counts whitespace-separated words as tokens.
/// </summary>
private sealed class SimpleWordTokenizer : Microsoft.ML.Tokenizers.Tokenizer
{
public override Microsoft.ML.Tokenizers.PreTokenizer? PreTokenizer => null;
public override Microsoft.ML.Tokenizers.Normalizer? Normalizer => null;
protected override Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken> EncodeToTokens(string? text, System.ReadOnlySpan<char> textSpan, Microsoft.ML.Tokenizers.EncodeSettings settings)
{
// Simple word-based encoding
string input = text ?? textSpan.ToString();
if (string.IsNullOrWhiteSpace(input))
{
return new Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken>
{
Tokens = System.Array.Empty<Microsoft.ML.Tokenizers.EncodedToken>(),
CharsConsumed = 0,
NormalizedText = null,
};
}
string[] words = input.Split(' ');
List<Microsoft.ML.Tokenizers.EncodedToken> tokens = [];
int offset = 0;
for (int i = 0; i < words.Length; i++)
{
tokens.Add(new Microsoft.ML.Tokenizers.EncodedToken(i, words[i], new System.Range(offset, offset + words[i].Length)));
offset += words[i].Length + 1;
}
return new Microsoft.ML.Tokenizers.EncodeResults<Microsoft.ML.Tokenizers.EncodedToken>
{
Tokens = tokens,
CharsConsumed = input.Length,
NormalizedText = null,
};
}
public override OperationStatus Decode(System.Collections.Generic.IEnumerable<int> ids, System.Span<char> destination, out int idsConsumed, out int charsWritten)
{
idsConsumed = 0;
charsWritten = 0;
return OperationStatus.Done;
}
}
}
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -163,12 +163,12 @@ public class SlidingWindowCompactionStrategyTests
{
// Arrange — trigger on > 1 turn, custom target stops after removing 1 turn
int removeCount = 0;
CompactionTrigger targetAfterOne = _ => ++removeCount >= 1;
bool TargetAfterOne(MessageIndex _) => ++removeCount >= 1;
SlidingWindowCompactionStrategy strategy = new(
CompactionTriggers.TurnsExceed(1),
minimumPreserved: 0,
target: targetAfterOne);
target: TargetAfterOne);
MessageIndex index = MessageIndex.Create(
[
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
@@ -15,8 +15,6 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction;
/// </summary>
public class SummarizationCompactionStrategyTests
{
private static readonly CompactionTrigger AlwaysTrigger = _ => true;
/// <summary>
/// Creates a mock <see cref="IChatClient"/> that returns the specified summary text.
/// </summary>
@@ -60,7 +58,7 @@ public class SummarizationCompactionStrategyTests
// Arrange — always trigger, preserve 1 recent group
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Key facts from earlier."),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
@@ -91,7 +89,7 @@ public class SummarizationCompactionStrategyTests
// Arrange
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
@@ -118,7 +116,7 @@ public class SummarizationCompactionStrategyTests
// Arrange
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Summary text."),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
@@ -145,7 +143,7 @@ public class SummarizationCompactionStrategyTests
// Arrange — LLM returns whitespace
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(" "),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
@@ -168,7 +166,7 @@ public class SummarizationCompactionStrategyTests
// Arrange — preserve 5 but only 2 non-system groups
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 5);
MessageIndex index = MessageIndex.Create(
@@ -198,12 +196,12 @@ public class SummarizationCompactionStrategyTests
capturedMessages = [.. msgs])
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Custom summary.")]));
const string customPrompt = "Summarize in bullet points only.";
const string CustomPrompt = "Summarize in bullet points only.";
SummarizationCompactionStrategy strategy = new(
mockClient.Object,
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1,
summarizationPrompt: customPrompt);
summarizationPrompt: CustomPrompt);
MessageIndex index = MessageIndex.Create(
[
@@ -216,7 +214,7 @@ public class SummarizationCompactionStrategyTests
// Assert — the custom prompt should be the first message sent to the LLM
Assert.NotNull(capturedMessages);
Assert.Equal(customPrompt, capturedMessages![0].Text);
Assert.Equal(CustomPrompt, capturedMessages![0].Text);
}
[Fact]
@@ -225,7 +223,7 @@ public class SummarizationCompactionStrategyTests
// Arrange
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
@@ -248,13 +246,13 @@ public class SummarizationCompactionStrategyTests
{
// Arrange — 4 non-system groups, preserve 1, target met after 1 exclusion
int exclusionCount = 0;
CompactionTrigger targetAfterOne = _ => ++exclusionCount >= 1;
CompactionTrigger TargetAfterOne = _ => ++exclusionCount >= 1;
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Partial summary."),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1,
target: targetAfterOne);
target: TargetAfterOne);
MessageIndex index = MessageIndex.Create(
[
@@ -278,7 +276,7 @@ public class SummarizationCompactionStrategyTests
// Arrange — preserve 2
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Summary."),
AlwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 2);
MessageIndex index = MessageIndex.Create(
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
@@ -12,13 +12,11 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction;
/// </summary>
public class TruncationCompactionStrategyTests
{
private static readonly CompactionTrigger s_alwaysTrigger = _ => true;
[Fact]
public async Task CompactAsyncAlwaysTriggerCompactsToPreserveRecentAsync()
{
// Arrange — always-trigger means always compact
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "First"),
@@ -89,7 +87,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncPreservesSystemMessagesAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "You are helpful."),
@@ -117,9 +115,9 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncPreservesToolCallGroupAtomicityAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
ChatMessage assistantToolCall= new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
ChatMessage finalResponse = new(ChatRole.User, "Thanks!");
@@ -141,7 +139,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncSetsExcludeReasonAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Old"),
@@ -160,7 +158,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncSkipsAlreadyExcludedGroupsAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Already excluded"),
@@ -183,7 +181,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncMinimumPreservedKeepsMultipleAsync()
{
// Arrange — keep 2 most recent
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 2);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 2);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
@@ -207,7 +205,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncNothingToRemoveReturnsFalseAsync()
{
// Arrange — preserve 5 but only 2 groups
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 5);
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 5);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
@@ -226,12 +224,12 @@ public class TruncationCompactionStrategyTests
{
// Arrange — always trigger, custom target stops after 1 exclusion
int targetChecks = 0;
CompactionTrigger targetAfterOne = _ => ++targetChecks >= 1;
bool TargetAfterOne(MessageIndex _) => ++targetChecks >= 1;
TruncationCompactionStrategy strategy = new(
s_alwaysTrigger,
CompactionTriggers.Always,
minimumPreserved: 1,
target: targetAfterOne);
target: TargetAfterOne);
MessageIndex groups = MessageIndex.Create(
[
@@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.ML.Tokenizers" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
<PackageReference Include="System.Linq.AsyncEnumerable" />