mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Tests
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="CompactionStrategy"/> abstract base class.
|
||||
/// </summary>
|
||||
public class CompactionStrategyTests
|
||||
{
|
||||
[Fact]
|
||||
public void ConstructorNullTriggerThrows()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new TestStrategy(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncTriggerNotMetReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger never fires
|
||||
TestStrategy strategy = new(_ => false);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Equal(0, strategy.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncTriggerMetCallsApplyAsync()
|
||||
{
|
||||
// Arrange — trigger always fires
|
||||
TestStrategy strategy = new(_ => true, applyFunc: _ => true);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, strategy.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncReturnsFalseWhenApplyReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger fires but Apply does nothing
|
||||
TestStrategy strategy = new(_ => true, applyFunc: _ => false);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Equal(1, strategy.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncDefaultTargetIsInverseOfTriggerAsync()
|
||||
{
|
||||
// Arrange — trigger fires when groups > 2
|
||||
// Default target should be: stop when groups <= 2 (i.e., !trigger)
|
||||
CompactionTrigger trigger = CompactionTriggers.GroupsExceed(2);
|
||||
TestStrategy strategy = new(trigger, applyFunc: index =>
|
||||
{
|
||||
// Exclude oldest non-system group one at a time
|
||||
foreach (MessageGroup group in index.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
// Target (default = !trigger) returns true when groups <= 2
|
||||
// So the strategy would check Target after this exclusion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
MessageIndex index = 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(index);
|
||||
|
||||
// Assert — trigger fires (4 > 2), Apply is called
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, strategy.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncCustomTargetIsPassedToStrategyAsync()
|
||||
{
|
||||
// Arrange — custom target that always signals stop
|
||||
bool targetCalled = false;
|
||||
CompactionTrigger customTarget = _ =>
|
||||
{
|
||||
targetCalled = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
TestStrategy strategy = new(_ => true, customTarget, _ =>
|
||||
{
|
||||
// Access the target from within the strategy
|
||||
return true;
|
||||
});
|
||||
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — the custom target is accessible (verified by TestStrategy checking it)
|
||||
Assert.Equal(1, strategy.ApplyCallCount);
|
||||
// The target is accessible to derived classes via the protected property
|
||||
Assert.True(strategy.InvokeTarget(index));
|
||||
Assert.True(targetCalled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A concrete test implementation of <see cref="CompactionStrategy"/> for testing the base class.
|
||||
/// </summary>
|
||||
private sealed class TestStrategy : CompactionStrategy
|
||||
{
|
||||
private readonly Func<MessageIndex, bool>? _applyFunc;
|
||||
|
||||
public TestStrategy(
|
||||
CompactionTrigger trigger,
|
||||
CompactionTrigger? target = null,
|
||||
Func<MessageIndex, bool>? applyFunc = null)
|
||||
: base(trigger, target)
|
||||
{
|
||||
this._applyFunc = applyFunc;
|
||||
}
|
||||
|
||||
public int ApplyCallCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the protected Target property for test verification.
|
||||
/// </summary>
|
||||
public bool InvokeTarget(MessageIndex index) => this.Target(index);
|
||||
|
||||
protected override Task<bool> ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken)
|
||||
{
|
||||
this.ApplyCallCount++;
|
||||
bool result = this._applyFunc?.Invoke(index) ?? false;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,4 +152,29 @@ public class CompactionTriggersTests
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
Assert.False(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokensBelowReturnsTrueWhenBelowThreshold()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.TokensBelow(999_999);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hi")]);
|
||||
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokensBelowReturnsFalseWhenAboveThreshold()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.TokensBelow(0);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello world")]);
|
||||
|
||||
Assert.False(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlwaysReturnsTrue()
|
||||
{
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
Assert.True(CompactionTriggers.Always(index));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,4 +521,158 @@ public class MessageIndexTests
|
||||
Assert.Equal(1, groups.TotalTurnCount);
|
||||
Assert.Equal(1, groups.IncludedTurnCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateAppendsNewMessagesIncrementally()
|
||||
{
|
||||
// Arrange — create with 2 messages
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
];
|
||||
MessageIndex index = MessageIndex.Create(messages);
|
||||
Assert.Equal(2, index.Groups.Count);
|
||||
Assert.Equal(2, index.ProcessedMessageCount);
|
||||
|
||||
// Act — add 2 more messages and update
|
||||
messages.Add(new ChatMessage(ChatRole.User, "Q2"));
|
||||
messages.Add(new ChatMessage(ChatRole.Assistant, "A2"));
|
||||
index.Update(messages);
|
||||
|
||||
// Assert — should have 4 groups total, processed count updated
|
||||
Assert.Equal(4, index.Groups.Count);
|
||||
Assert.Equal(4, index.ProcessedMessageCount);
|
||||
Assert.Equal(MessageGroupKind.User, index.Groups[2].Kind);
|
||||
Assert.Equal(MessageGroupKind.AssistantText, index.Groups[3].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateNoOpWhenNoNewMessages()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
];
|
||||
MessageIndex index = MessageIndex.Create(messages);
|
||||
int originalCount = index.Groups.Count;
|
||||
|
||||
// Act — update with same count
|
||||
index.Update(messages);
|
||||
|
||||
// Assert — nothing changed
|
||||
Assert.Equal(originalCount, index.Groups.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateRebuildsWhenMessagesShrink()
|
||||
{
|
||||
// Arrange — create with 3 messages
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
];
|
||||
MessageIndex index = MessageIndex.Create(messages);
|
||||
Assert.Equal(3, index.Groups.Count);
|
||||
|
||||
// Exclude a group to verify rebuild clears state
|
||||
index.Groups[0].IsExcluded = true;
|
||||
|
||||
// Act — update with fewer messages (simulates storage compaction)
|
||||
List<ChatMessage> shortened =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
];
|
||||
index.Update(shortened);
|
||||
|
||||
// Assert — rebuilt from scratch
|
||||
Assert.Single(index.Groups);
|
||||
Assert.False(index.Groups[0].IsExcluded);
|
||||
Assert.Equal(1, index.ProcessedMessageCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdatePreservesExistingGroupExclusionState()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
];
|
||||
MessageIndex index = MessageIndex.Create(messages);
|
||||
index.Groups[0].IsExcluded = true;
|
||||
index.Groups[0].ExcludeReason = "Test exclusion";
|
||||
|
||||
// Act — append new messages
|
||||
messages.Add(new ChatMessage(ChatRole.User, "Q2"));
|
||||
index.Update(messages);
|
||||
|
||||
// Assert — original exclusion state preserved
|
||||
Assert.True(index.Groups[0].IsExcluded);
|
||||
Assert.Equal("Test exclusion", index.Groups[0].ExcludeReason);
|
||||
Assert.Equal(3, index.Groups.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertGroupInsertsAtSpecifiedIndex()
|
||||
{
|
||||
// Arrange
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act — insert between Q1 and Q2
|
||||
ChatMessage summaryMsg = new(ChatRole.Assistant, "[Summary]");
|
||||
MessageGroup inserted = index.InsertGroup(1, MessageGroupKind.Summary, [summaryMsg], turnIndex: 1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, index.Groups.Count);
|
||||
Assert.Same(inserted, index.Groups[1]);
|
||||
Assert.Equal(MessageGroupKind.Summary, index.Groups[1].Kind);
|
||||
Assert.Equal("[Summary]", index.Groups[1].Messages[0].Text);
|
||||
Assert.Equal(1, inserted.TurnIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddGroupAppendsToEnd()
|
||||
{
|
||||
// Arrange
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
ChatMessage msg = new(ChatRole.Assistant, "Appended");
|
||||
MessageGroup added = index.AddGroup(MessageGroupKind.AssistantText, [msg], turnIndex: 1);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, index.Groups.Count);
|
||||
Assert.Same(added, index.Groups[1]);
|
||||
Assert.Equal("Appended", index.Groups[1].Messages[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InsertGroupComputesByteAndTokenCounts()
|
||||
{
|
||||
// Arrange
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
]);
|
||||
|
||||
// Act — insert a group with known text
|
||||
ChatMessage msg = new(ChatRole.Assistant, "Hello"); // 5 bytes, ~1 token (5/4)
|
||||
MessageGroup inserted = index.InsertGroup(0, MessageGroupKind.AssistantText, [msg]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, inserted.ByteCount);
|
||||
Assert.Equal(1, inserted.TokenCount); // 5 / 4 = 1 (integer division)
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -157,4 +157,38 @@ public class SlidingWindowCompactionStrategyTests
|
||||
Assert.Equal("Q2", included[1].Text);
|
||||
Assert.Equal("A2", included[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncCustomTargetStopsExcludingEarlyAsync()
|
||||
{
|
||||
// Arrange — 4 turns, maxTurns=1 means 3 should be excluded
|
||||
// But custom target stops after removing 1 turn
|
||||
int removeCount = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++removeCount >= 1;
|
||||
|
||||
SlidingWindowCompactionStrategy strategy = new(
|
||||
maximumTurns: 1,
|
||||
target: targetAfterOne);
|
||||
|
||||
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"),
|
||||
new ChatMessage(ChatRole.User, "Q4"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — only turn 1 excluded (target stopped after 1 removal)
|
||||
Assert.True(result);
|
||||
Assert.True(index.Groups[0].IsExcluded); // Q1 (turn 1)
|
||||
Assert.True(index.Groups[1].IsExcluded); // A1 (turn 1)
|
||||
Assert.False(index.Groups[2].IsExcluded); // Q2 (turn 2) — kept
|
||||
Assert.False(index.Groups[3].IsExcluded); // A2 (turn 2)
|
||||
}
|
||||
}
|
||||
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
// 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.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="SummarizationCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public class SummarizationCompactionStrategyTests
|
||||
{
|
||||
private static readonly CompactionTrigger AlwaysTrigger = _ => true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock <see cref="IChatClient"/> that returns the specified summary text.
|
||||
/// </summary>
|
||||
private static IChatClient CreateMockChatClient(string summaryText = "Summary of conversation.")
|
||||
{
|
||||
Mock<IChatClient> mock = new();
|
||||
mock.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, summaryText)]));
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncTriggerNotMetReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger requires > 100000 tokens
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
CompactionTriggers.TokensExceed(100000),
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Equal(2, index.IncludedGroupCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncSummarizesOldGroupsAsync()
|
||||
{
|
||||
// Arrange — always trigger, preserve 1 recent group
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Key facts from earlier."),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First question"),
|
||||
new ChatMessage(ChatRole.Assistant, "First answer"),
|
||||
new ChatMessage(ChatRole.User, "Second question"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
|
||||
List<ChatMessage> included = [.. index.GetIncludedMessages()];
|
||||
|
||||
// Should have: summary + preserved recent group (Second question)
|
||||
Assert.Equal(2, included.Count);
|
||||
Assert.Contains("[Summary]", included[0].Text);
|
||||
Assert.Contains("Key facts from earlier.", included[0].Text);
|
||||
Assert.Equal("Second question", included[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncPreservesSystemMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "Old question"),
|
||||
new ChatMessage(ChatRole.Assistant, "Old answer"),
|
||||
new ChatMessage(ChatRole.User, "Recent question"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
List<ChatMessage> included = [.. index.GetIncludedMessages()];
|
||||
|
||||
Assert.Equal("You are helpful.", included[0].Text);
|
||||
Assert.Equal(ChatRole.System, included[0].Role);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncInsertsSummaryGroupAtCorrectPositionAsync()
|
||||
{
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Summary text."),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
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"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — summary should be inserted after system, before preserved group
|
||||
MessageGroup summaryGroup = index.Groups.First(g => g.Kind == MessageGroupKind.Summary);
|
||||
Assert.NotNull(summaryGroup);
|
||||
Assert.Contains("[Summary]", summaryGroup.Messages[0].Text);
|
||||
Assert.True(summaryGroup.Messages[0].AdditionalProperties!.ContainsKey(MessageGroup.SummaryPropertyKey));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncHandlesEmptyLlmResponseAsync()
|
||||
{
|
||||
// Arrange — LLM returns whitespace
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(" "),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — should use fallback text
|
||||
List<ChatMessage> included = [.. index.GetIncludedMessages()];
|
||||
Assert.Contains("[Summary unavailable]", included[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncNothingToSummarizeReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — preserve 5 but only 2 non-system groups
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 5);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncUsesCustomPromptAsync()
|
||||
{
|
||||
// Arrange — capture the messages sent to the chat client
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
Mock<IChatClient> mockClient = new();
|
||||
mockClient.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, "Custom summary.")]));
|
||||
|
||||
const string customPrompt = "Summarize in bullet points only.";
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
mockClient.Object,
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1,
|
||||
summarizationPrompt: customPrompt);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — the custom prompt should be the first message sent to the LLM
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.Equal(customPrompt, capturedMessages![0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncSetsExcludeReasonAsync()
|
||||
{
|
||||
// Arrange
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient(),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Old"),
|
||||
new ChatMessage(ChatRole.User, "New"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert
|
||||
MessageGroup excluded = index.Groups.First(g => g.IsExcluded);
|
||||
Assert.NotNull(excluded.ExcludeReason);
|
||||
Assert.Contains("SummarizationCompactionStrategy", excluded.ExcludeReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncTargetStopsMarkingEarlyAsync()
|
||||
{
|
||||
// Arrange — 4 non-system groups, preserve 1, target met after 1 exclusion
|
||||
int exclusionCount = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++exclusionCount >= 1;
|
||||
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Partial summary."),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 1,
|
||||
target: targetAfterOne);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — only 1 group should have been summarized (target met after first exclusion)
|
||||
int excludedCount = index.Groups.Count(g => g.IsExcluded);
|
||||
Assert.Equal(1, excludedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncPreservesMultipleRecentGroupsAsync()
|
||||
{
|
||||
// Arrange — preserve 2
|
||||
SummarizationCompactionStrategy strategy = new(
|
||||
CreateMockChatClient("Summary."),
|
||||
AlwaysTrigger,
|
||||
preserveRecentGroups: 2);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.Assistant, "A2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — 2 oldest excluded, 2 newest preserved + 1 summary inserted
|
||||
List<ChatMessage> included = [.. index.GetIncludedMessages()];
|
||||
Assert.Equal(3, included.Count); // summary + Q2 + A2
|
||||
Assert.Contains("[Summary]", included[0].Text);
|
||||
Assert.Equal("Q2", included[1].Text);
|
||||
Assert.Equal("A2", included[2].Text);
|
||||
}
|
||||
}
|
||||
+42
@@ -191,4 +191,46 @@ public class ToolResultCompactionStrategyTests
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncTargetStopsCollapsingEarlyAsync()
|
||||
{
|
||||
// Arrange — 2 tool groups, target met after first collapse
|
||||
int collapseCount = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++collapseCount >= 1;
|
||||
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 1,
|
||||
target: targetAfterOne);
|
||||
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn1")]),
|
||||
new ChatMessage(ChatRole.Tool, "result1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c2", "fn2")]),
|
||||
new ChatMessage(ChatRole.Tool, "result2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(index);
|
||||
|
||||
// Assert — only first tool group collapsed, second left intact
|
||||
Assert.True(result);
|
||||
|
||||
// Count collapsed tool groups (excluded with ToolCall kind)
|
||||
int collapsedToolGroups = 0;
|
||||
foreach (MessageGroup group in index.Groups)
|
||||
{
|
||||
if (group.IsExcluded && group.Kind == MessageGroupKind.ToolCall)
|
||||
{
|
||||
collapsedToolGroups++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(1, collapsedToolGroups);
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -220,4 +220,60 @@ public class TruncationCompactionStrategyTests
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncCustomTargetStopsEarlyAsync()
|
||||
{
|
||||
// Arrange — always trigger, custom target stops after 1 exclusion
|
||||
int targetChecks = 0;
|
||||
CompactionTrigger targetAfterOne = _ => ++targetChecks >= 1;
|
||||
|
||||
TruncationCompactionStrategy strategy = new(
|
||||
s_alwaysTrigger,
|
||||
preserveRecentGroups: 1,
|
||||
target: targetAfterOne);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert — only 1 group excluded (target met after first)
|
||||
Assert.True(result);
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.False(groups.Groups[1].IsExcluded);
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsyncIncrementalStopsAtTargetAsync()
|
||||
{
|
||||
// Arrange — trigger on groups > 2, target is default (inverse of trigger: groups <= 2)
|
||||
TruncationCompactionStrategy strategy = new(
|
||||
CompactionTriggers.GroupsExceed(2),
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex groups = 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"),
|
||||
]);
|
||||
|
||||
// Act — 5 groups, trigger fires (5 > 2), compacts until groups <= 2
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert — should stop at 2 included groups (not go all the way to 1)
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user