Test update

This commit is contained in:
Chris Rickman
2026-03-05 08:25:19 -08:00
parent 06f55c0494
commit f42863e354
7 changed files with 285 additions and 5 deletions
@@ -20,6 +20,12 @@ public static class CompactionTriggers
public static readonly CompactionTrigger Always =
_ => true;
/// <summary>
/// Always trigger compaction, regardless of the message index state.
/// </summary>
public static readonly CompactionTrigger Never =
_ => false;
/// <summary>
/// Creates a trigger that fires when the included token count is below the specified maximum.
/// </summary>
@@ -1,8 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
// 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;
@@ -348,7 +347,7 @@ public sealed class CompactingChatClientTests : IDisposable
SetRunContext(session);
// Use an IEnumerable (not a List) to trigger the copy path
IEnumerable<ChatMessage> messages = new ChatMessage[] { new(ChatRole.User, "Hello") };
IEnumerable<ChatMessage> messages = [new(ChatRole.User, "Hello")];
// Act
ChatResponse response = await client.GetResponseAsync(messages);
@@ -380,7 +379,7 @@ public sealed class CompactingChatClientTests : IDisposable
AgentRunContext context = new(
mockAgent.Object,
session,
new List<ChatMessage> { new(ChatRole.User, "test") },
[new(ChatRole.User, "test")],
null);
SetCurrentRunContext(context);
}
@@ -800,6 +800,68 @@ public class MessageIndexTests
Assert.Equal(4, inserted.TokenCount);
}
[Fact]
public void CreateWithStandaloneToolMessageGroupsAsAssistantText()
{
// A Tool message not preceded by an assistant tool-call falls through to the else branch
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.Tool, "Orphaned tool result"),
];
MessageIndex index = MessageIndex.Create(messages);
// The Tool message should be grouped as AssistantText (the default fallback)
Assert.Single(index.Groups);
Assert.Equal(MessageGroupKind.AssistantText, index.Groups[0].Kind);
}
[Fact]
public void CreateWithAssistantNonSummaryWithPropertiesFallsToAssistantText()
{
// Assistant message with AdditionalProperties but NOT a summary
ChatMessage assistant = new(ChatRole.Assistant, "Regular response");
(assistant.AdditionalProperties ??= [])["someOtherKey"] = "value";
MessageIndex index = MessageIndex.Create([assistant]);
Assert.Single(index.Groups);
Assert.Equal(MessageGroupKind.AssistantText, index.Groups[0].Kind);
}
[Fact]
public void ComputeByteCountHandlesNullAndNonNullText()
{
// Mix of messages: one with text (non-null), one without (null Text)
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
];
int byteCount = MessageIndex.ComputeByteCount(messages);
// Only "Hello" contributes bytes (5 bytes UTF-8)
Assert.Equal(5, byteCount);
}
[Fact]
public void ComputeTokenCountHandlesNullAndNonNullText()
{
// Mix: one with text, one without
SimpleWordTokenizer tokenizer = new();
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.User, "Hello world"),
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
];
int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer);
// Only "Hello world" contributes tokens (2 words)
Assert.Equal(2, tokenCount);
}
/// <summary>
/// A simple tokenizer that counts whitespace-separated words as tokens.
/// </summary>
@@ -221,4 +221,30 @@ public class SlidingWindowCompactionStrategyTests
Assert.False(index.Groups[4].IsExcluded); // Q3
Assert.False(index.Groups[5].IsExcluded); // A3
}
[Fact]
public async Task CompactAsyncSkipsExcludedAndSystemGroupsInEnumerationAsync()
{
// Arrange — includes system and pre-excluded groups that must be skipped
SlidingWindowCompactionStrategy strategy = new(
CompactionTriggers.TurnsExceed(1),
minimumPreserved: 0);
MessageIndex index = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "System prompt"),
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
]);
// Pre-exclude one group
index.Groups[1].IsExcluded = true;
// Act
bool result = await strategy.CompactAsync(index);
// Assert — system preserved, pre-excluded skipped
Assert.True(result);
Assert.False(index.Groups[0].IsExcluded); // System preserved
}
}
@@ -246,7 +246,7 @@ public class SummarizationCompactionStrategyTests
{
// Arrange — 4 non-system groups, preserve 1, target met after 1 exclusion
int exclusionCount = 0;
CompactionTrigger TargetAfterOne = _ => ++exclusionCount >= 1;
bool TargetAfterOne(MessageIndex _) => ++exclusionCount >= 1;
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Partial summary."),
@@ -297,4 +297,114 @@ public class SummarizationCompactionStrategyTests
Assert.Equal("Q2", included[1].Text);
Assert.Equal("A2", included[2].Text);
}
[Fact]
public async Task CompactAsyncWithSystemBetweenSummarizableGroupsAsync()
{
// Arrange — system group between user/assistant groups to exercise skip logic in loop
IChatClient mockClient = CreateMockChatClient("[Summary]");
SummarizationCompactionStrategy strategy = new(
mockClient,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.System, "System note"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — summary inserted at 0, system group shifted to index 2
Assert.True(result);
Assert.Equal(MessageGroupKind.Summary, index.Groups[0].Kind);
Assert.Equal(MessageGroupKind.System, index.Groups[2].Kind);
Assert.False(index.Groups[2].IsExcluded); // System never excluded
}
[Fact]
public async Task CompactAsyncMaxSummarizableBoundsLoopExitAsync()
{
// Arrange — large MinimumPreserved so maxSummarizable is small, target never stops
IChatClient mockClient = CreateMockChatClient("[Summary]");
SummarizationCompactionStrategy strategy = new(
mockClient,
CompactionTriggers.Always,
minimumPreserved: 3,
target: _ => false);
MessageIndex index = MessageIndex.Create(
[
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"),
new ChatMessage(ChatRole.Assistant, "A3"),
]);
// Act — should only summarize 6-3 = 3 groups (not all 6)
bool result = await strategy.CompactAsync(index);
// Assert — 3 preserved + 1 summary = 4 included
Assert.True(result);
Assert.Equal(4, index.IncludedGroupCount);
}
[Fact]
public async Task CompactAsyncWithPreExcludedGroupAsync()
{
// Arrange — pre-exclude a group so the count and loop both must skip it
IChatClient mockClient = CreateMockChatClient("[Summary]");
SummarizationCompactionStrategy strategy = new(
mockClient,
CompactionTriggers.Always,
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.Assistant, "A2"),
]);
index.Groups[0].IsExcluded = true; // Pre-exclude Q1
// Act
bool result = await strategy.CompactAsync(index);
// Assert
Assert.True(result);
Assert.True(index.Groups[0].IsExcluded); // Still excluded
}
[Fact]
public async Task CompactAsyncWithEmptyTextMessageInGroupAsync()
{
// Arrange — a message with null text (FunctionCallContent) in a summarized group
IChatClient mockClient = CreateMockChatClient("[Summary]");
SummarizationCompactionStrategy strategy = new(
mockClient,
CompactionTriggers.Always,
minimumPreserved: 1);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
];
MessageIndex index = MessageIndex.Create(messages);
// Act — the tool-call group's message has null text
bool result = await strategy.CompactAsync(index);
// Assert — compaction succeeded despite null text
Assert.True(result);
}
}
@@ -233,4 +233,30 @@ public class ToolResultCompactionStrategyTests
Assert.Equal(1, collapsedToolGroups);
}
[Fact]
public async Task CompactAsyncSkipsPreExcludedAndSystemGroupsAsync()
{
// Arrange — pre-excluded and system groups in the enumeration
ToolResultCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 0);
List<ChatMessage> messages =
[
new ChatMessage(ChatRole.System, "System prompt"),
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
new ChatMessage(ChatRole.Tool, "Result 1"),
new ChatMessage(ChatRole.User, "Q1"),
];
MessageIndex index = MessageIndex.Create(messages);
// Pre-exclude the user group
index.Groups[index.Groups.Count - 1].IsExcluded = true;
// Act
bool result = await strategy.CompactAsync(index);
// Assert — system never excluded, pre-excluded skipped
Assert.True(result);
Assert.False(index.Groups[0].IsExcluded); // System stays
}
}
@@ -274,4 +274,55 @@ public class TruncationCompactionStrategyTests
Assert.True(result);
Assert.Equal(2, groups.IncludedGroupCount);
}
[Fact]
public async Task CompactAsyncLoopExitsWhenMaxRemovableReachedAsync()
{
// Arrange — target never stops (always false), so the loop must exit via removed >= maxRemovable
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 2, target: CompactionTriggers.Never);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.Assistant, "A2"),
]);
// Act
bool result = await strategy.CompactAsync(groups);
// Assert — only 2 removed (maxRemovable = 4 - 2 = 2), 2 preserved
Assert.True(result);
Assert.Equal(2, groups.IncludedGroupCount);
Assert.True(groups.Groups[0].IsExcluded);
Assert.True(groups.Groups[1].IsExcluded);
Assert.False(groups.Groups[2].IsExcluded);
Assert.False(groups.Groups[3].IsExcluded);
}
[Fact]
public async Task CompactAsyncSkipsPreExcludedAndSystemGroupsAsync()
{
// Arrange — has excluded + system groups that the loop must skip
TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "System"),
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
]);
// Pre-exclude one group
groups.Groups[1].IsExcluded = true;
// Act
bool result = await strategy.CompactAsync(groups);
// Assert — system preserved, pre-excluded skipped, A1 removed, Q2 preserved
Assert.True(result);
Assert.False(groups.Groups[0].IsExcluded); // System
Assert.True(groups.Groups[1].IsExcluded); // Pre-excluded Q1
Assert.True(groups.Groups[2].IsExcluded); // Newly excluded A1
Assert.False(groups.Groups[3].IsExcluded); // Preserved Q2
}
}