mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Stable
This commit is contained in:
-282
@@ -1,282 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="PipelineCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public class PipelineCompactionStrategyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CompactAsync_ExecutesAllStrategiesInOrder()
|
||||
{
|
||||
// Arrange
|
||||
List<string> executionOrder = [];
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => executionOrder.Add("first"))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback(() => executionOrder.Add("second"))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(["first", "second"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ReturnsFalse_WhenNoStrategyCompacts()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ReturnsTrue_WhenAnyStrategyCompacts()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ContinuesAfterFirstCompaction_WhenEarlyStopDisabled()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies were called
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_StopsEarly_WhenTargetReached()
|
||||
{
|
||||
// Arrange — first strategy reduces to target
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
// Exclude the first group to bring count down
|
||||
groups.Groups[0].IsExcluded = true;
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
TargetIncludedGroupCount = 2,
|
||||
};
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — strategy2 should not have been called
|
||||
Assert.True(result);
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_DoesNotStopEarly_WhenTargetNotReached()
|
||||
{
|
||||
// Arrange — first strategy does NOT bring count to target
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
TargetIncludedGroupCount = 1,
|
||||
};
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
new ChatMessage(ChatRole.User, "Third"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies were called since target was never reached
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_EarlyStopIgnored_WhenNoTargetSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ICompactionStrategy> strategy1 = new();
|
||||
strategy1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> strategy2 = new();
|
||||
strategy2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object])
|
||||
{
|
||||
EarlyStop = true,
|
||||
// TargetIncludedGroupCount is null
|
||||
};
|
||||
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies called because no target to check against
|
||||
strategy1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
strategy2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ComposesStrategies_EndToEnd()
|
||||
{
|
||||
// Arrange — pipeline: first exclude oldest 2 non-system groups, then exclude 2 more
|
||||
Mock<ICompactionStrategy> phase1 = new();
|
||||
phase1.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
excluded++;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
|
||||
Mock<ICompactionStrategy> phase2 = new();
|
||||
phase2.Setup(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<MessageIndex, CancellationToken>((groups, _) =>
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in groups.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
excluded++;
|
||||
}
|
||||
}
|
||||
})
|
||||
.ReturnsAsync(true);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new([phase1.Object, phase2.Object]);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
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
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — system is preserved, phase1 excluded Q1+A1, phase2 excluded Q2+A2 → System + Q3
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
Assert.Equal(2, included.Count);
|
||||
Assert.Equal("You are helpful.", included[0].Text);
|
||||
Assert.Equal("Q3", included[1].Text);
|
||||
|
||||
phase1.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
phase2.Verify(s => s.CompactAsync(It.IsAny<MessageIndex>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_EmptyPipeline_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
PipelineCompactionStrategy pipeline = new(new List<ICompactionStrategy>());
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for <see cref="CompactionTrigger"/> and <see cref="CompactionTriggers"/>.
|
||||
/// </summary>
|
||||
public class CompactionTriggersTests
|
||||
{
|
||||
[Fact]
|
||||
public void TokensExceed_ReturnsTrueWhenAboveThreshold()
|
||||
{
|
||||
// Arrange — use a long message to guarantee tokens > 0
|
||||
CompactionTrigger trigger = CompactionTriggers.TokensExceed(0);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello world")]);
|
||||
|
||||
// Act & Assert
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TokensExceed_ReturnsFalseWhenBelowThreshold()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.TokensExceed(999_999);
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hi")]);
|
||||
|
||||
Assert.False(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessagesExceed_ReturnsExpectedResult()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.MessagesExceed(2);
|
||||
MessageIndex small = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "A"),
|
||||
new ChatMessage(ChatRole.User, "B"),
|
||||
]);
|
||||
MessageIndex large = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "A"),
|
||||
new ChatMessage(ChatRole.User, "B"),
|
||||
new ChatMessage(ChatRole.User, "C"),
|
||||
]);
|
||||
|
||||
Assert.False(trigger(small));
|
||||
Assert.True(trigger(large));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurnsExceed_ReturnsExpectedResult()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.TurnsExceed(1);
|
||||
MessageIndex oneTurn = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
]);
|
||||
MessageIndex twoTurns = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
Assert.False(trigger(oneTurn));
|
||||
Assert.True(trigger(twoTurns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroupsExceed_ReturnsExpectedResult()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.GroupsExceed(2);
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "A"),
|
||||
new ChatMessage(ChatRole.Assistant, "B"),
|
||||
new ChatMessage(ChatRole.User, "C"),
|
||||
]);
|
||||
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasToolCalls_ReturnsTrueWhenToolCallGroupExists()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.HasToolCalls();
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
|
||||
new ChatMessage(ChatRole.Tool, "result"),
|
||||
]);
|
||||
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasToolCalls_ReturnsFalseWhenNoToolCallGroup()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.HasToolCalls();
|
||||
MessageIndex index = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
Assert.False(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_RequiresAllConditions()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.All(
|
||||
CompactionTriggers.TokensExceed(0),
|
||||
CompactionTriggers.MessagesExceed(5));
|
||||
|
||||
MessageIndex small = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
|
||||
// Tokens > 0 is true, but messages > 5 is false
|
||||
Assert.False(trigger(small));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_RequiresAtLeastOneCondition()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.Any(
|
||||
CompactionTriggers.TokensExceed(999_999),
|
||||
CompactionTriggers.MessagesExceed(0));
|
||||
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
|
||||
// Tokens not exceeded, but messages > 0 is true
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_EmptyTriggers_ReturnsTrue()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.All();
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
Assert.True(trigger(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Any_EmptyTriggers_ReturnsFalse()
|
||||
{
|
||||
CompactionTrigger trigger = CompactionTriggers.Any();
|
||||
MessageIndex index = MessageIndex.Create([new ChatMessage(ChatRole.User, "A")]);
|
||||
Assert.False(trigger(index));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
//using Microsoft.Extensions.AI;
|
||||
//using Moq;
|
||||
|
||||
//namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
//namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
///// <summary>
|
||||
///// Contains tests for the compaction integration with <see cref="InMemoryChatHistoryProvider"/>.
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
|
||||
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="MessageIndex"/> class.
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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="PipelineCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public class PipelineCompactionStrategyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CompactAsync_ExecutesAllStrategiesInOrderAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<string> executionOrder = [];
|
||||
TestCompactionStrategy strategy1 = new(
|
||||
_ =>
|
||||
{
|
||||
executionOrder.Add("first");
|
||||
return false;
|
||||
});
|
||||
|
||||
TestCompactionStrategy strategy2 = new(
|
||||
_ =>
|
||||
{
|
||||
executionOrder.Add("second");
|
||||
return false;
|
||||
});
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1, strategy2);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(["first", "second"], executionOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ReturnsFalse_WhenNoStrategyCompactsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestCompactionStrategy strategy1 = new(_ => false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ReturnsTrue_WhenAnyStrategyCompactsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestCompactionStrategy strategy1 = new(_ => false);
|
||||
TestCompactionStrategy strategy2 = new(_ => true);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1, strategy2);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ContinuesAfterFirstCompaction_WhenEarlyStopDisabledAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestCompactionStrategy strategy1 = new(_ => true);
|
||||
TestCompactionStrategy strategy2 = new(_ => false);
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(strategy1, strategy2);
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — both strategies were called
|
||||
Assert.Equal(1, strategy1.ApplyCallCount);
|
||||
Assert.Equal(1, strategy2.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ComposesStrategies_EndToEndAsync()
|
||||
{
|
||||
// Arrange — pipeline: first exclude oldest 2 non-system groups, then exclude 2 more
|
||||
static void ExcludeOldest2(MessageIndex index)
|
||||
{
|
||||
int excluded = 0;
|
||||
foreach (MessageGroup group in index.Groups)
|
||||
{
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
excluded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestCompactionStrategy phase1 = new(
|
||||
index =>
|
||||
{
|
||||
ExcludeOldest2(index);
|
||||
return true;
|
||||
});
|
||||
|
||||
TestCompactionStrategy phase2 = new(
|
||||
index =>
|
||||
{
|
||||
ExcludeOldest2(index);
|
||||
return true;
|
||||
});
|
||||
|
||||
PipelineCompactionStrategy pipeline = new(phase1, phase2);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
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
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert — system is preserved, phase1 excluded Q1+A1, phase2 excluded Q2+A2 → System + Q3
|
||||
Assert.True(result);
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
Assert.Equal(2, included.Count);
|
||||
Assert.Equal("You are helpful.", included[0].Text);
|
||||
Assert.Equal("Q3", included[1].Text);
|
||||
|
||||
Assert.Equal(1, phase1.ApplyCallCount);
|
||||
Assert.Equal(1, phase2.ApplyCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_EmptyPipeline_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
PipelineCompactionStrategy pipeline = new(new List<CompactionStrategy>());
|
||||
MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
|
||||
|
||||
// Act
|
||||
bool result = await pipeline.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple test implementation of <see cref="CompactionStrategy"/> that delegates to a synchronous callback.
|
||||
/// </summary>
|
||||
private sealed class TestCompactionStrategy : CompactionStrategy
|
||||
{
|
||||
private readonly Func<MessageIndex, bool> _applyFunc;
|
||||
|
||||
public TestCompactionStrategy(Func<MessageIndex, bool> applyFunc)
|
||||
: base(CompactionTriggers.Always)
|
||||
{
|
||||
this._applyFunc = applyFunc;
|
||||
}
|
||||
|
||||
public int ApplyCallCount { get; private set; }
|
||||
|
||||
protected override Task<bool> ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken)
|
||||
{
|
||||
this.ApplyCallCount++;
|
||||
return Task.FromResult(this._applyFunc(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// 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.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for the <see cref="SlidingWindowCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public class SlidingWindowCompactionStrategyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CompactAsync_BelowMaxTurns_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 3);
|
||||
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
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ExceedsMaxTurns_ExcludesOldestTurnsAsync()
|
||||
{
|
||||
// Arrange — keep 2 turns, conversation has 3
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 2);
|
||||
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"),
|
||||
new ChatMessage(ChatRole.Assistant, "A3"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
// Turn 1 (Q1 + A1) should be excluded
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
// Turn 2 and 3 should remain
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
Assert.False(groups.Groups[4].IsExcluded);
|
||||
Assert.False(groups.Groups[5].IsExcluded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreservesSystemMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.False(groups.Groups[0].IsExcluded); // System preserved
|
||||
Assert.True(groups.Groups[1].IsExcluded); // Turn 1 excluded
|
||||
Assert.True(groups.Groups[2].IsExcluded); // Turn 1 response excluded
|
||||
Assert.False(groups.Groups[3].IsExcluded); // Turn 2 kept
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreservesToolCallGroupsInKeptTurnsAsync()
|
||||
{
|
||||
// Arrange
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, "A1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "search")]),
|
||||
new ChatMessage(ChatRole.Tool, "Results"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
// Turn 1 excluded
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
// Turn 2 kept (user + tool call group)
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_CustomTrigger_OverridesDefaultAsync()
|
||||
{
|
||||
// Arrange — custom trigger: only compact when tokens exceed threshold
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 99);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
new ChatMessage(ChatRole.User, "Q3"),
|
||||
]);
|
||||
|
||||
// Act — tokens are tiny, trigger not met
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_IncludedMessages_ContainOnlyKeptTurnsAsync()
|
||||
{
|
||||
// Arrange
|
||||
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 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"),
|
||||
new ChatMessage(ChatRole.Assistant, "A2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
Assert.Equal(3, included.Count);
|
||||
Assert.Equal("System", included[0].Text);
|
||||
Assert.Equal("Q2", included[1].Text);
|
||||
Assert.Equal("A2", included[2].Text);
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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="ToolResultCompactionStrategy"/> class.
|
||||
/// </summary>
|
||||
public class ToolResultCompactionStrategyTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CompactAsync_TriggerNotMet_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger requires > 1000 tokens
|
||||
ToolResultCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000));
|
||||
|
||||
ChatMessage toolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
|
||||
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "What's the weather?"),
|
||||
toolCall,
|
||||
toolResult,
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_CollapsesOldToolGroupsAsync()
|
||||
{
|
||||
// Arrange — always trigger
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]),
|
||||
new ChatMessage(ChatRole.Tool, "Sunny and 72°F"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
// Q1 + collapsed tool summary + Q2
|
||||
Assert.Equal(3, included.Count);
|
||||
Assert.Equal("Q1", included[0].Text);
|
||||
Assert.Contains("[Tool calls: get_weather]", included[1].Text);
|
||||
Assert.Equal("Q2", included[2].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreservesRecentToolGroupsAsync()
|
||||
{
|
||||
// Arrange — protect 2 recent non-system groups (the tool group + Q2)
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 3);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "search")]),
|
||||
new ChatMessage(ChatRole.Tool, "Results"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert — all groups are in the protected window, nothing to collapse
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreservesSystemMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call1", "fn")]),
|
||||
new ChatMessage(ChatRole.Tool, "result"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
Assert.Equal("You are helpful.", included[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ExtractsMultipleToolNamesAsync()
|
||||
{
|
||||
// Arrange — assistant calls two tools
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
ChatMessage multiToolCall = new(ChatRole.Assistant,
|
||||
[
|
||||
new FunctionCallContent("c1", "get_weather"),
|
||||
new FunctionCallContent("c2", "search_docs"),
|
||||
]);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
multiToolCall,
|
||||
new ChatMessage(ChatRole.Tool, "Sunny"),
|
||||
new ChatMessage(ChatRole.Tool, "Found 3 docs"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
List<ChatMessage> included = [.. groups.GetIncludedMessages()];
|
||||
string collapsed = included[1].Text!;
|
||||
Assert.Contains("get_weather", collapsed);
|
||||
Assert.Contains("search_docs", collapsed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_NoToolGroups_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger fires but no tool groups to collapse
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
trigger: _ => true,
|
||||
preserveRecentGroups: 0);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_CompoundTrigger_RequiresTokensAndToolCallsAsync()
|
||||
{
|
||||
// Arrange — compound: tokens > 0 AND has tool calls
|
||||
ToolResultCompactionStrategy strategy = new(
|
||||
CompactionTriggers.All(
|
||||
CompactionTriggers.TokensExceed(0),
|
||||
CompactionTriggers.HasToolCalls()),
|
||||
preserveRecentGroups: 1);
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Q1"),
|
||||
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
|
||||
new ChatMessage(ChatRole.Tool, "result"),
|
||||
new ChatMessage(ChatRole.User, "Q2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
}
|
||||
+114
-82
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -11,78 +12,91 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
/// </summary>
|
||||
public class TruncationCompactionStrategyTests
|
||||
{
|
||||
private static readonly CompactionTrigger s_alwaysTrigger = _ => true;
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_BelowLimit_ReturnsFalseAsync()
|
||||
public async Task CompactAsync_AlwaysTrigger_CompactsToPreserveRecentAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 5);
|
||||
// Arrange — always-trigger means always compact
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_AtLimit_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 2);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_ExceedsLimit_ExcludesOldestGroupsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 2);
|
||||
ChatMessage msg1 = new(ChatRole.User, "First");
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response 1");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
ChatMessage msg4 = new(ChatRole.Assistant, "Response 2");
|
||||
|
||||
MessageIndex groups = MessageIndex.Create([msg1, msg2, msg3, msg4]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, groups.Groups.Count(g => !g.IsExcluded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_TriggerNotMet_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — trigger requires > 1000 tokens, conversation is tiny
|
||||
TruncationCompactionStrategy strategy = new(
|
||||
preserveRecentGroups: 1,
|
||||
trigger: CompactionTriggers.TokensExceed(1000));
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_TriggerMet_ExcludesOldestGroupsAsync()
|
||||
{
|
||||
// Arrange — trigger on groups > 2
|
||||
TruncationCompactionStrategy strategy = new(
|
||||
preserveRecentGroups: 1,
|
||||
trigger: CompactionTriggers.GroupsExceed(2));
|
||||
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 2"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, groups.IncludedGroupCount);
|
||||
// Oldest 3 excluded, newest 1 kept
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
Assert.True(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreservesSystemMessages_WhenEnabledAsync()
|
||||
public async Task CompactAsync_PreservesSystemMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 2, preserveSystemMessages: true);
|
||||
ChatMessage systemMsg = new(ChatRole.System, "You are helpful.");
|
||||
ChatMessage msg1 = new(ChatRole.User, "First");
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response 1");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
|
||||
MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]);
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, "You are helpful."),
|
||||
new ChatMessage(ChatRole.User, "First"),
|
||||
new ChatMessage(ChatRole.Assistant, "Response 1"),
|
||||
new ChatMessage(ChatRole.User, "Second"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
@@ -92,34 +106,10 @@ public class TruncationCompactionStrategyTests
|
||||
// System message should be preserved
|
||||
Assert.False(groups.Groups[0].IsExcluded);
|
||||
Assert.Equal(MessageGroupKind.System, groups.Groups[0].Kind);
|
||||
// Oldest non-system groups should be excluded
|
||||
// Oldest non-system groups excluded
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
Assert.True(groups.Groups[2].IsExcluded);
|
||||
// Most recent should remain
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_DoesNotPreserveSystemMessages_WhenDisabledAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 2, preserveSystemMessages: false);
|
||||
ChatMessage systemMsg = new(ChatRole.System, "You are helpful.");
|
||||
ChatMessage msg1 = new(ChatRole.User, "First");
|
||||
ChatMessage msg2 = new(ChatRole.Assistant, "Response");
|
||||
ChatMessage msg3 = new(ChatRole.User, "Second");
|
||||
|
||||
MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
// System message should be excluded (oldest)
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
// Most recent kept
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
@@ -127,9 +117,9 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsync_PreservesToolCallGroupAtomicityAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 1);
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 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!");
|
||||
|
||||
@@ -151,7 +141,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsync_SetsExcludeReasonAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 1);
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Old"),
|
||||
@@ -170,7 +160,7 @@ public class TruncationCompactionStrategyTests
|
||||
public async Task CompactAsync_SkipsAlreadyExcludedGroupsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TruncationCompactionStrategy strategy = new(maxGroups: 1);
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Already excluded"),
|
||||
@@ -188,4 +178,46 @@ public class TruncationCompactionStrategyTests
|
||||
Assert.True(groups.Groups[1].IsExcluded); // newly excluded
|
||||
Assert.False(groups.Groups[2].IsExcluded); // kept
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompactAsync_PreserveRecentGroups_KeepsMultipleAsync()
|
||||
{
|
||||
// Arrange — keep 2 most recent
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 2);
|
||||
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
|
||||
Assert.True(result);
|
||||
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 CompactAsync_NothingToRemove_ReturnsFalseAsync()
|
||||
{
|
||||
// Arrange — preserve 5 but only 2 groups
|
||||
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 5);
|
||||
MessageIndex groups = MessageIndex.Create(
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Hello"),
|
||||
new ChatMessage(ChatRole.Assistant, "Hi!"),
|
||||
]);
|
||||
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user