mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Add invariant tests and remove unused createdTimes collection
Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/fd187fc2-3ed5-4204-8340-d4b3c391e989 Co-authored-by: lokitoth <6936551+lokitoth@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
dbac4eab56
commit
53693d7aea
@@ -137,7 +137,6 @@ internal sealed class MessageMerger
|
||||
|
||||
UsageDetails? usage = null;
|
||||
AdditionalPropertiesDictionary? additionalProperties = null;
|
||||
HashSet<DateTimeOffset> createdTimes = [];
|
||||
|
||||
foreach (AgentResponse response in responses.Values)
|
||||
{
|
||||
@@ -146,11 +145,6 @@ internal sealed class MessageMerger
|
||||
agentIds.Add(response.AgentId);
|
||||
}
|
||||
|
||||
if (response.CreatedAt.HasValue)
|
||||
{
|
||||
createdTimes.Add(response.CreatedAt.Value);
|
||||
}
|
||||
|
||||
if (response.FinishReason.HasValue)
|
||||
{
|
||||
finishReasons.Add(response.FinishReason.Value);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
@@ -112,4 +113,326 @@ public class MessageMergerTests
|
||||
// Assert - FinishReason from the update should propagate through
|
||||
response.FinishReason.Should().Be(ChatFinishReason.ContentFilter);
|
||||
}
|
||||
|
||||
#region Invariant 2: Output Order Preservation Tests
|
||||
|
||||
[Fact]
|
||||
public void Test_MessageMerger_PreservesInsertionOrder_WhenNoTimestamps()
|
||||
{
|
||||
// Arrange: Multiple updates without CreatedAt, in specific order A, B, C
|
||||
string responseId = Guid.NewGuid().ToString("N");
|
||||
string messageIdA = Guid.NewGuid().ToString("N");
|
||||
string messageIdB = Guid.NewGuid().ToString("N");
|
||||
string messageIdC = Guid.NewGuid().ToString("N");
|
||||
|
||||
MessageMerger merger = new();
|
||||
|
||||
// Add updates without CreatedAt in order A, B, C
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdA,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Message A")],
|
||||
// No CreatedAt
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdB,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Message B")],
|
||||
// No CreatedAt
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdC,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Message C")],
|
||||
// No CreatedAt
|
||||
});
|
||||
|
||||
// Act
|
||||
AgentResponse response = merger.ComputeMerged(responseId);
|
||||
|
||||
// Assert: Output order should be A, B, C (insertion order)
|
||||
response.Messages.Should().HaveCount(3);
|
||||
response.Messages[0].Text.Should().Be("Message A");
|
||||
response.Messages[1].Text.Should().Be("Message B");
|
||||
response.Messages[2].Text.Should().Be("Message C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_MessageMerger_PreservesInsertionOrder_WhenMixedTimestamps()
|
||||
{
|
||||
// Arrange: Updates where some have CreatedAt and some don't
|
||||
string responseId = Guid.NewGuid().ToString("N");
|
||||
string messageIdA = Guid.NewGuid().ToString("N");
|
||||
string messageIdB = Guid.NewGuid().ToString("N");
|
||||
string messageIdC = Guid.NewGuid().ToString("N");
|
||||
|
||||
DateTimeOffset time1 = DateTimeOffset.UtcNow.AddMinutes(-2);
|
||||
DateTimeOffset time3 = DateTimeOffset.UtcNow;
|
||||
|
||||
MessageMerger merger = new();
|
||||
|
||||
// A has timestamp (time1), B has no timestamp, C has timestamp (time3)
|
||||
// Insertion order: A, B, C
|
||||
// B should maintain its relative position among untimestamped messages
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdA,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time1,
|
||||
Contents = [new TextContent("Message A")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdB,
|
||||
Role = ChatRole.Assistant,
|
||||
// No CreatedAt - should use insertion order as tiebreaker
|
||||
Contents = [new TextContent("Message B")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdC,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time3,
|
||||
Contents = [new TextContent("Message C")],
|
||||
});
|
||||
|
||||
// Act
|
||||
AgentResponse response = merger.ComputeMerged(responseId);
|
||||
|
||||
// Assert: Untimestamped messages should maintain relative order via insertion index fallback
|
||||
response.Messages.Should().HaveCount(3);
|
||||
|
||||
// A (time1) should come first, B (no timestamp, uses index 1) should be in middle,
|
||||
// C (time3) should come last since it has the latest timestamp
|
||||
response.Messages[0].Text.Should().Be("Message A");
|
||||
response.Messages[1].Text.Should().Be("Message B");
|
||||
response.Messages[2].Text.Should().Be("Message C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_MessageMerger_StableSort_WithThreeOrMoreMixedTimestampMessages()
|
||||
{
|
||||
// Arrange: 3+ messages with mixed null/non-null CreatedAt values
|
||||
// This tests the non-transitive comparison edge case
|
||||
string responseId = Guid.NewGuid().ToString("N");
|
||||
string messageIdA = Guid.NewGuid().ToString("N");
|
||||
string messageIdB = Guid.NewGuid().ToString("N");
|
||||
string messageIdC = Guid.NewGuid().ToString("N");
|
||||
|
||||
DateTimeOffset time10 = DateTimeOffset.UtcNow.AddSeconds(10);
|
||||
DateTimeOffset time5 = DateTimeOffset.UtcNow.AddSeconds(5);
|
||||
|
||||
MessageMerger merger = new();
|
||||
|
||||
// A: CreatedAt = time10, idx=0
|
||||
// B: CreatedAt = null, idx=1
|
||||
// C: CreatedAt = time5, idx=2
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdA,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time10,
|
||||
Contents = [new TextContent("Message A (T=10)")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdB,
|
||||
Role = ChatRole.Assistant,
|
||||
// No CreatedAt
|
||||
Contents = [new TextContent("Message B (no timestamp)")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdC,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time5,
|
||||
Contents = [new TextContent("Message C (T=5)")],
|
||||
});
|
||||
|
||||
// Act - Run multiple times to verify determinism
|
||||
AgentResponse response1 = merger.ComputeMerged(responseId);
|
||||
|
||||
// Create a fresh merger with same data to verify determinism
|
||||
MessageMerger merger2 = new();
|
||||
merger2.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdA,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time10,
|
||||
Contents = [new TextContent("Message A (T=10)")],
|
||||
});
|
||||
merger2.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdB,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Message B (no timestamp)")],
|
||||
});
|
||||
merger2.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseId,
|
||||
MessageId = messageIdC,
|
||||
Role = ChatRole.Assistant,
|
||||
CreatedAt = time5,
|
||||
Contents = [new TextContent("Message C (T=5)")],
|
||||
});
|
||||
AgentResponse response2 = merger2.ComputeMerged(responseId);
|
||||
|
||||
// Assert: Result is deterministic and consistent across runs
|
||||
response1.Messages.Should().HaveCount(3);
|
||||
response2.Messages.Should().HaveCount(3);
|
||||
|
||||
// Both runs should produce identical ordering
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
response1.Messages[i].Text.Should().Be(response2.Messages[i].Text);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Invariant 3: Agent Message Grouping Tests
|
||||
|
||||
[Fact]
|
||||
public void Test_MessageMerger_GroupsMessagesByResponseId_InMultiAgentScenario()
|
||||
{
|
||||
// Arrange: Interleaved updates from Agent1 (R1) and Agent2 (R2)
|
||||
string responseIdR1 = Guid.NewGuid().ToString("N");
|
||||
string responseIdR2 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA1M1 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA1M2 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA2M1 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA2M2 = Guid.NewGuid().ToString("N");
|
||||
|
||||
MessageMerger merger = new();
|
||||
|
||||
// Interleaved arrival: A1-msg1, A2-msg1, A1-msg2, A2-msg2
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR1,
|
||||
MessageId = messageIdA1M1,
|
||||
AgentId = TestAgentId1,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent1 Message 1")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR2,
|
||||
MessageId = messageIdA2M1,
|
||||
AgentId = TestAgentId2,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent2 Message 1")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR1,
|
||||
MessageId = messageIdA1M2,
|
||||
AgentId = TestAgentId1,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent1 Message 2")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR2,
|
||||
MessageId = messageIdA2M2,
|
||||
AgentId = TestAgentId2,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent2 Message 2")],
|
||||
});
|
||||
|
||||
// Act
|
||||
AgentResponse response = merger.ComputeMerged(responseIdR1);
|
||||
|
||||
// Assert: Messages should be grouped by ResponseId (which groups by agent)
|
||||
// Output should be either [A1-msg1, A1-msg2, A2-msg1, A2-msg2] or [A2-msg1, A2-msg2, A1-msg1, A1-msg2]
|
||||
// The key invariant: Agent1's messages are contiguous, Agent2's messages are contiguous
|
||||
response.Messages.Should().HaveCount(4);
|
||||
|
||||
// Verify grouping - collect message texts and verify they're grouped by agent
|
||||
var messageTexts = response.Messages.Select(m => m.Text).ToList();
|
||||
|
||||
// Find first Agent1 message index and first Agent2 message index
|
||||
int firstA1Index = messageTexts.FindIndex(t => t.StartsWith("Agent1", StringComparison.Ordinal));
|
||||
int firstA2Index = messageTexts.FindIndex(t => t.StartsWith("Agent2", StringComparison.Ordinal));
|
||||
|
||||
// All Agent1 messages should be contiguous (either at start or after all Agent2 messages)
|
||||
var a1Messages = messageTexts.Where(t => t.StartsWith("Agent1", StringComparison.Ordinal)).ToList();
|
||||
var a2Messages = messageTexts.Where(t => t.StartsWith("Agent2", StringComparison.Ordinal)).ToList();
|
||||
|
||||
a1Messages.Should().HaveCount(2);
|
||||
a2Messages.Should().HaveCount(2);
|
||||
|
||||
// Verify no interleaving: if A1 comes first, A2 should come after all A1 messages
|
||||
if (firstA1Index < firstA2Index)
|
||||
{
|
||||
// A1 messages at indices 0, 1 and A2 messages at indices 2, 3
|
||||
messageTexts[0].Should().StartWith("Agent1");
|
||||
messageTexts[1].Should().StartWith("Agent1");
|
||||
messageTexts[2].Should().StartWith("Agent2");
|
||||
messageTexts[3].Should().StartWith("Agent2");
|
||||
}
|
||||
else
|
||||
{
|
||||
// A2 messages at indices 0, 1 and A1 messages at indices 2, 3
|
||||
messageTexts[0].Should().StartWith("Agent2");
|
||||
messageTexts[1].Should().StartWith("Agent2");
|
||||
messageTexts[2].Should().StartWith("Agent1");
|
||||
messageTexts[3].Should().StartWith("Agent1");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_MessageMerger_MaintainsAgentGrouping_WithDifferentResponseIds()
|
||||
{
|
||||
// Arrange: Agent1 uses ResponseId=R1, Agent2 uses ResponseId=R2
|
||||
string responseIdR1 = Guid.NewGuid().ToString("N");
|
||||
string responseIdR2 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA1 = Guid.NewGuid().ToString("N");
|
||||
string messageIdA2 = Guid.NewGuid().ToString("N");
|
||||
|
||||
MessageMerger merger = new();
|
||||
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR1,
|
||||
MessageId = messageIdA1,
|
||||
AgentId = TestAgentId1,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent1 Response")],
|
||||
});
|
||||
merger.AddUpdate(new AgentResponseUpdate
|
||||
{
|
||||
ResponseId = responseIdR2,
|
||||
MessageId = messageIdA2,
|
||||
AgentId = TestAgentId2,
|
||||
Role = ChatRole.Assistant,
|
||||
Contents = [new TextContent("Agent2 Response")],
|
||||
});
|
||||
|
||||
// Act
|
||||
AgentResponse response = merger.ComputeMerged(responseIdR1);
|
||||
|
||||
// Assert: Messages from each agent are contiguous (not interleaved)
|
||||
response.Messages.Should().HaveCount(2);
|
||||
|
||||
// Both messages should be present
|
||||
var messageTexts = response.Messages.Select(m => m.Text).ToList();
|
||||
messageTexts.Should().Contain("Agent1 Response");
|
||||
messageTexts.Should().Contain("Agent2 Response");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user