mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
ca02146ee4
* Changes
* Fix ChatClientAgent streaming responses missing MessageId
Generate fallback MessageId in ChatClientAgent.RunCoreStreamingAsync when
the underlying LLM provider does not set ChatResponseUpdate.MessageId.
Without a MessageId the AGUI converter's null==null check silently drops
all text content, causing CopilotKit Zod validation errors.
Changes:
- ChatClientAgent: generate msg_{Guid} fallback via ??= in streaming loop
- AgentResponseExtensions: sync wrapper MessageId back to RawRepresentation
in AsChatResponseUpdate() so downstream consumers see the value
- Add unit tests for both fixes and AGUI streaming MessageId scenarios
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address PR #4615 review comments
- Fix MessageId seeding: use first-seen provider MessageId (or generate
fallback) and apply consistently to all chunks in the stream, preventing
message splitting when providers set MessageId only on the first chunk
- Add test for mixed MessageId scenario (first chunk only)
- Fix skipped TextStreaming test: assert Empty (not NotEmpty) to match
actual null==null behavior
- Fix skipped ToolCalls test: assert empty ParentMessageId to match
actual empty-string passthrough behavior
* Handle empty MessageId in AsChatResponseUpdate sync
Treat empty/whitespace MessageId the same as null when syncing from
the AgentResponseUpdate wrapper back to RawRepresentation. Providers
that return empty string MessageId (e.g. tool call responses) now get
the wrapper value recovered correctly.
Add test for empty string MessageId recovery scenario.
* Move MessageId fallback generation to AGUI layer
Move fallback MessageId generation from ChatClientAgent to
AsAGUIEventStreamAsync, addressing the architectural concern that
MessageId is nullable in the AIAgent abstraction and the requirement
for non-null values is specific to the AGUI protocol.
The AGUI layer now generates a fallback MessageId for null or
empty/whitespace values, covering all agent types (not just
ChatClientAgent) including external implementations.
Changes:
- Revert MessageId generation from ChatClientAgent.RunCoreStreamingAsync
- Add fallback MessageId generation in AsAGUIEventStreamAsync for
null/empty MessageId values (handles both null and whitespace)
- Unskip and update AGUI tests to verify fallback generation
- Update ChatClientAgent tests to reflect passthrough behavior
* Revert AsChatResponseUpdate MessageId sync-back
Remove the MessageId sync-back logic from AsChatResponseUpdate() as it
is no longer needed. With fallback generation moved to the AGUI layer,
the abstraction layer should not mutate the RawRepresentation object.
Revert to the original passthrough behavior for AsChatResponseUpdate()
and update tests accordingly.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
509 lines
19 KiB
C#
509 lines
19 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
|
|
|
public class AgentResponseUpdateExtensionsTests
|
|
{
|
|
public static IEnumerable<object[]> ToAgentResponseCoalescesVariousSequenceAndGapLengthsMemberData()
|
|
{
|
|
foreach (bool useAsync in new[] { false, true })
|
|
{
|
|
for (int numSequences = 1; numSequences <= 3; numSequences++)
|
|
{
|
|
for (int sequenceLength = 1; sequenceLength <= 3; sequenceLength++)
|
|
{
|
|
for (int gapLength = 1; gapLength <= 3; gapLength++)
|
|
{
|
|
foreach (bool gapBeginningEnd in new[] { false, true })
|
|
{
|
|
yield return new object[] { useAsync, numSequences, sequenceLength, gapLength, false };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ToAgentResponseWithInvalidArgsThrows() =>
|
|
Assert.Throws<ArgumentNullException>("updates", () => ((List<AgentResponseUpdate>)null!).ToAgentResponse());
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task ToAgentResponseSuccessfullyCreatesResponseAsync(bool useAsync)
|
|
{
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
new(ChatRole.Assistant, "Hello") { ResponseId = "someResponse", MessageId = "12345", CreatedAt = new DateTimeOffset(2024, 2, 3, 4, 5, 6, TimeSpan.Zero), AgentId = "agentId" },
|
|
new(new("human"), ", ") { AuthorName = "Someone", AdditionalProperties = new() { ["a"] = "b" } },
|
|
new(null, "world!") { CreatedAt = new DateTimeOffset(2025, 2, 3, 4, 5, 6, TimeSpan.Zero), AdditionalProperties = new() { ["c"] = "d" } },
|
|
|
|
new() { Contents = [new UsageContent(new() { InputTokenCount = 1, OutputTokenCount = 2 })] },
|
|
new() { Contents = [new UsageContent(new() { InputTokenCount = 4, OutputTokenCount = 5 })] },
|
|
];
|
|
|
|
AgentResponse response = useAsync ?
|
|
updates.ToAgentResponse() :
|
|
await YieldAsync(updates).ToAgentResponseAsync();
|
|
Assert.NotNull(response);
|
|
|
|
Assert.Equal("agentId", response.AgentId);
|
|
|
|
Assert.NotNull(response.Usage);
|
|
Assert.Equal(5, response.Usage.InputTokenCount);
|
|
Assert.Equal(7, response.Usage.OutputTokenCount);
|
|
|
|
Assert.Equal("someResponse", response.ResponseId);
|
|
Assert.Equal(new DateTimeOffset(2024, 2, 3, 4, 5, 6, TimeSpan.Zero), response.CreatedAt);
|
|
|
|
Assert.Equal(2, response.Messages.Count);
|
|
|
|
ChatMessage message = response.Messages[0];
|
|
Assert.Equal("12345", message.MessageId);
|
|
Assert.Equal(ChatRole.Assistant, message.Role);
|
|
Assert.Null(message.AuthorName);
|
|
Assert.Null(message.AdditionalProperties);
|
|
Assert.Single(message.Contents);
|
|
Assert.Equal("Hello", Assert.IsType<TextContent>(message.Contents[0]).Text);
|
|
|
|
message = response.Messages[1];
|
|
Assert.Null(message.MessageId);
|
|
Assert.Equal(new("human"), message.Role);
|
|
Assert.Equal("Someone", message.AuthorName);
|
|
Assert.Single(message.Contents);
|
|
Assert.Equal(", world!", Assert.IsType<TextContent>(message.Contents[0]).Text);
|
|
|
|
Assert.NotNull(response.AdditionalProperties);
|
|
Assert.Equal(2, response.AdditionalProperties.Count);
|
|
Assert.Equal("b", response.AdditionalProperties["a"]);
|
|
Assert.Equal("d", response.AdditionalProperties["c"]);
|
|
|
|
Assert.Equal("Hello" + Environment.NewLine + ", world!", response.Text);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ToAgentResponseCoalescesVariousSequenceAndGapLengthsMemberData))]
|
|
public async Task ToAgentResponseCoalescesVariousSequenceAndGapLengthsAsync(bool useAsync, int numSequences, int sequenceLength, int gapLength, bool gapBeginningEnd)
|
|
{
|
|
List<AgentResponseUpdate> updates = [];
|
|
|
|
List<string> expected = [];
|
|
|
|
if (gapBeginningEnd)
|
|
{
|
|
AddGap();
|
|
}
|
|
|
|
for (int sequenceNum = 0; sequenceNum < numSequences; sequenceNum++)
|
|
{
|
|
StringBuilder sb = new();
|
|
for (int i = 0; i < sequenceLength; i++)
|
|
{
|
|
string text = $"{(char)('A' + sequenceNum)}{i}";
|
|
updates.Add(new(null, text));
|
|
sb.Append(text);
|
|
}
|
|
|
|
expected.Add(sb.ToString());
|
|
|
|
if (sequenceNum < numSequences - 1)
|
|
{
|
|
AddGap();
|
|
}
|
|
}
|
|
|
|
if (gapBeginningEnd)
|
|
{
|
|
AddGap();
|
|
}
|
|
|
|
void AddGap()
|
|
{
|
|
for (int i = 0; i < gapLength; i++)
|
|
{
|
|
updates.Add(new() { Contents = [new DataContent("data:image/png;base64,aGVsbG8=")] });
|
|
}
|
|
}
|
|
|
|
AgentResponse response = useAsync ? await YieldAsync(updates).ToAgentResponseAsync() : updates.ToAgentResponse();
|
|
Assert.NotNull(response);
|
|
|
|
ChatMessage message = response.Messages.Single();
|
|
Assert.NotNull(message);
|
|
|
|
Assert.Equal(expected.Count + (gapLength * (numSequences - 1 + (gapBeginningEnd ? 2 : 0))), message.Contents.Count);
|
|
|
|
TextContent[] contents = message.Contents.OfType<TextContent>().ToArray();
|
|
Assert.Equal(expected.Count, contents.Length);
|
|
for (int i = 0; i < expected.Count; i++)
|
|
{
|
|
Assert.Equal(expected[i], contents[i].Text);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task ToAgentResponseCoalescesTextContentAndTextReasoningContentSeparatelyAsync(bool useAsync)
|
|
{
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
new(null, "A"),
|
|
new(null, "B"),
|
|
new(null, "C"),
|
|
new() { Contents = [new TextReasoningContent("D")] },
|
|
new() { Contents = [new TextReasoningContent("E")] },
|
|
new() { Contents = [new TextReasoningContent("F")] },
|
|
new(null, "G"),
|
|
new(null, "H"),
|
|
new() { Contents = [new TextReasoningContent("I")] },
|
|
new() { Contents = [new TextReasoningContent("J")] },
|
|
new(null, "K"),
|
|
new() { Contents = [new TextReasoningContent("L")] },
|
|
new(null, "M"),
|
|
new(null, "N"),
|
|
new() { Contents = [new TextReasoningContent("O")] },
|
|
new() { Contents = [new TextReasoningContent("P")] },
|
|
];
|
|
|
|
AgentResponse response = useAsync ? await YieldAsync(updates).ToAgentResponseAsync() : updates.ToAgentResponse();
|
|
ChatMessage message = Assert.Single(response.Messages);
|
|
Assert.Equal(8, message.Contents.Count);
|
|
Assert.Equal("ABC", Assert.IsType<TextContent>(message.Contents[0]).Text);
|
|
Assert.Equal("DEF", Assert.IsType<TextReasoningContent>(message.Contents[1]).Text);
|
|
Assert.Equal("GH", Assert.IsType<TextContent>(message.Contents[2]).Text);
|
|
Assert.Equal("IJ", Assert.IsType<TextReasoningContent>(message.Contents[3]).Text);
|
|
Assert.Equal("K", Assert.IsType<TextContent>(message.Contents[4]).Text);
|
|
Assert.Equal("L", Assert.IsType<TextReasoningContent>(message.Contents[5]).Text);
|
|
Assert.Equal("MN", Assert.IsType<TextContent>(message.Contents[6]).Text);
|
|
Assert.Equal("OP", Assert.IsType<TextReasoningContent>(message.Contents[7]).Text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ToAgentResponseUsesContentExtractedFromContentsAsync()
|
|
{
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
new(null, "Hello, "),
|
|
new(null, "world!"),
|
|
new() { Contents = [new UsageContent(new() { TotalTokenCount = 42 })] },
|
|
];
|
|
|
|
AgentResponse response = await YieldAsync(updates).ToAgentResponseAsync();
|
|
|
|
Assert.NotNull(response);
|
|
|
|
Assert.NotNull(response.Usage);
|
|
Assert.Equal(42, response.Usage.TotalTokenCount);
|
|
|
|
Assert.Equal("Hello, world!", Assert.IsType<TextContent>(Assert.Single(Assert.Single(response.Messages).Contents)).Text);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task ToAgentResponse_AlternativeTimestampsAsync(bool useAsync)
|
|
{
|
|
DateTimeOffset early = new(2024, 1, 1, 10, 0, 0, TimeSpan.Zero);
|
|
DateTimeOffset middle = new(2024, 1, 1, 11, 0, 0, TimeSpan.Zero);
|
|
DateTimeOffset late = new(2024, 1, 1, 12, 0, 0, TimeSpan.Zero);
|
|
DateTimeOffset unixEpoch = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
|
|
// Start with an early timestamp
|
|
new(ChatRole.Tool, "a") { MessageId = "4", CreatedAt = early },
|
|
|
|
// Unix epoch (as "null") should not overwrite
|
|
new(null, "b") { CreatedAt = unixEpoch },
|
|
|
|
// Newer timestamp should not overwrite (first timestamp wins)
|
|
new(null, "c") { CreatedAt = middle },
|
|
|
|
// Older timestamp should not overwrite
|
|
new(null, "d") { CreatedAt = early },
|
|
|
|
// Even newer timestamp should not overwrite (first timestamp wins)
|
|
new(null, "e") { CreatedAt = late },
|
|
|
|
// Unix epoch should not overwrite again
|
|
new(null, "f") { CreatedAt = unixEpoch },
|
|
|
|
// null should not overwrite
|
|
new(null, "g") { CreatedAt = null },
|
|
];
|
|
|
|
AgentResponse response = useAsync ?
|
|
updates.ToAgentResponse() :
|
|
await YieldAsync(updates).ToAgentResponseAsync();
|
|
Assert.Single(response.Messages);
|
|
|
|
Assert.Equal("abcdefg", response.Messages[0].Text);
|
|
Assert.Equal(ChatRole.Tool, response.Messages[0].Role);
|
|
Assert.Equal(early, response.Messages[0].CreatedAt);
|
|
Assert.Equal(early, response.CreatedAt);
|
|
}
|
|
|
|
public static IEnumerable<object?[]> ToAgentResponse_TimestampFolding_MemberData()
|
|
{
|
|
// Base test cases - first non-null valid timestamp wins
|
|
var testCases = new (string? timestamp1, string? timestamp2, string? expectedTimestamp)[]
|
|
{
|
|
(null, null, null),
|
|
("2024-01-01T10:00:00Z", null, "2024-01-01T10:00:00Z"),
|
|
(null, "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"),
|
|
("2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z"), // First timestamp wins
|
|
("2024-01-01T11:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T11:00:00Z"), // First timestamp wins
|
|
("2024-01-01T10:00:00Z", "1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z"),
|
|
("1970-01-01T00:00:00Z", "2024-01-01T10:00:00Z", "2024-01-01T10:00:00Z"),
|
|
};
|
|
|
|
// Yield each test case twice, once for useAsync = false and once for useAsync = true
|
|
foreach (var (timestamp1, timestamp2, expectedTimestamp) in testCases)
|
|
{
|
|
yield return new object?[] { false, timestamp1, timestamp2, expectedTimestamp };
|
|
yield return new object?[] { true, timestamp1, timestamp2, expectedTimestamp };
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ToAgentResponse_TimestampFolding_MemberData))]
|
|
public async Task ToAgentResponse_TimestampFoldingAsync(bool useAsync, string? timestamp1, string? timestamp2, string? expectedTimestamp)
|
|
{
|
|
DateTimeOffset? first = timestamp1 is not null ? DateTimeOffset.Parse(timestamp1) : null;
|
|
DateTimeOffset? second = timestamp2 is not null ? DateTimeOffset.Parse(timestamp2) : null;
|
|
DateTimeOffset? expected = expectedTimestamp is not null ? DateTimeOffset.Parse(expectedTimestamp) : null;
|
|
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
new(ChatRole.Assistant, "a") { CreatedAt = first },
|
|
new(null, "b") { CreatedAt = second },
|
|
];
|
|
|
|
AgentResponse response = useAsync ?
|
|
updates.ToAgentResponse() :
|
|
await YieldAsync(updates).ToAgentResponseAsync();
|
|
|
|
Assert.Single(response.Messages);
|
|
Assert.Equal("ab", response.Messages[0].Text);
|
|
Assert.Equal(expected, response.Messages[0].CreatedAt);
|
|
Assert.Equal(expected, response.CreatedAt);
|
|
}
|
|
|
|
#region AsChatResponse Tests
|
|
|
|
[Fact]
|
|
public void AsChatResponse_WithNullArgument_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange & Act & Assert
|
|
Assert.Throws<ArgumentNullException>("response", () => ((AgentResponse)null!).AsChatResponse());
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponse_WithRawRepresentationAsChatResponse_ReturnsSameInstance()
|
|
{
|
|
// Arrange
|
|
ChatResponse originalChatResponse = new()
|
|
{
|
|
ResponseId = "original-response",
|
|
Messages = [new ChatMessage(ChatRole.Assistant, "Hello")]
|
|
};
|
|
AgentResponse agentResponse = new(originalChatResponse);
|
|
|
|
// Act
|
|
ChatResponse result = agentResponse.AsChatResponse();
|
|
|
|
// Assert
|
|
Assert.Same(originalChatResponse, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponse_WithoutRawRepresentation_CreatesNewChatResponse()
|
|
{
|
|
// Arrange
|
|
AgentResponse agentResponse = new(new ChatMessage(ChatRole.Assistant, "Test message"))
|
|
{
|
|
ResponseId = "test-response-id",
|
|
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
|
|
FinishReason = ChatFinishReason.ContentFilter,
|
|
Usage = new UsageDetails { TotalTokenCount = 50 },
|
|
AdditionalProperties = new() { ["key"] = "value" },
|
|
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
|
};
|
|
|
|
// Act
|
|
ChatResponse result = agentResponse.AsChatResponse();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("test-response-id", result.ResponseId);
|
|
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
|
|
Assert.Equal(ChatFinishReason.ContentFilter, result.FinishReason);
|
|
Assert.Same(agentResponse.Messages, result.Messages);
|
|
Assert.Same(agentResponse, result.RawRepresentation);
|
|
Assert.Same(agentResponse.Usage, result.Usage);
|
|
Assert.Same(agentResponse.AdditionalProperties, result.AdditionalProperties);
|
|
Assert.Equal(agentResponse.ContinuationToken, result.ContinuationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AsChatResponseUpdate Tests
|
|
|
|
[Fact]
|
|
public void AsChatResponseUpdate_WithNullArgument_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange & Act & Assert
|
|
Assert.Throws<ArgumentNullException>("responseUpdate", () => ((AgentResponseUpdate)null!).AsChatResponseUpdate());
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponseUpdate_WithRawRepresentationAsChatResponseUpdate_ReturnsSameInstance()
|
|
{
|
|
// Arrange
|
|
ChatResponseUpdate originalChatResponseUpdate = new()
|
|
{
|
|
ResponseId = "original-update",
|
|
Contents = [new TextContent("Hello")]
|
|
};
|
|
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
|
|
|
|
// Act
|
|
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
|
|
|
|
// Assert
|
|
Assert.Same(originalChatResponseUpdate, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponseUpdate_WithRawRepresentationNullMessageId_ReturnsRawDirectly()
|
|
{
|
|
// Arrange - RawRepresentation has null MessageId
|
|
ChatResponseUpdate originalChatResponseUpdate = new()
|
|
{
|
|
ResponseId = "original-update",
|
|
Contents = [new TextContent("Hello")]
|
|
};
|
|
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
|
|
|
|
// Act
|
|
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
|
|
|
|
// Assert - Returns the raw representation directly without mutation
|
|
Assert.Same(originalChatResponseUpdate, result);
|
|
Assert.Null(result.MessageId);
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponseUpdate_WithRawRepresentationExistingMessageId_PreservesOriginal()
|
|
{
|
|
// Arrange - RawRepresentation already has MessageId set by provider
|
|
ChatResponseUpdate originalChatResponseUpdate = new()
|
|
{
|
|
ResponseId = "original-update",
|
|
MessageId = "provider-message-id",
|
|
Contents = [new TextContent("Hello")]
|
|
};
|
|
AgentResponseUpdate agentResponseUpdate = new(originalChatResponseUpdate);
|
|
|
|
// Act
|
|
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
|
|
|
|
// Assert - Provider's original MessageId should be preserved
|
|
Assert.Same(originalChatResponseUpdate, result);
|
|
Assert.Equal("provider-message-id", result.MessageId);
|
|
}
|
|
|
|
[Fact]
|
|
public void AsChatResponseUpdate_WithoutRawRepresentation_CreatesNewChatResponseUpdate()
|
|
{
|
|
// Arrange
|
|
AgentResponseUpdate agentResponseUpdate = new(ChatRole.Assistant, "Test")
|
|
{
|
|
AuthorName = "TestAuthor",
|
|
ResponseId = "update-id",
|
|
MessageId = "message-id",
|
|
CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero),
|
|
FinishReason = ChatFinishReason.ToolCalls,
|
|
AdditionalProperties = new() { ["key"] = "value" },
|
|
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
|
|
};
|
|
|
|
// Act
|
|
ChatResponseUpdate result = agentResponseUpdate.AsChatResponseUpdate();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("TestAuthor", result.AuthorName);
|
|
Assert.Equal("update-id", result.ResponseId);
|
|
Assert.Equal("message-id", result.MessageId);
|
|
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero), result.CreatedAt);
|
|
Assert.Equal(ChatFinishReason.ToolCalls, result.FinishReason);
|
|
Assert.Equal(ChatRole.Assistant, result.Role);
|
|
Assert.Same(agentResponseUpdate.Contents, result.Contents);
|
|
Assert.Same(agentResponseUpdate, result.RawRepresentation);
|
|
Assert.Same(agentResponseUpdate.AdditionalProperties, result.AdditionalProperties);
|
|
Assert.Equal(agentResponseUpdate.ContinuationToken, result.ContinuationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region AsChatResponseUpdatesAsync Tests
|
|
|
|
[Fact]
|
|
public async Task AsChatResponseUpdatesAsync_WithNullArgument_ThrowsArgumentNullExceptionAsync()
|
|
{
|
|
// Arrange & Act & Assert
|
|
await Assert.ThrowsAsync<ArgumentNullException>("responseUpdates", async () =>
|
|
{
|
|
await foreach (ChatResponseUpdate _ in ((IAsyncEnumerable<AgentResponseUpdate>)null!).AsChatResponseUpdatesAsync())
|
|
{
|
|
// Do nothing
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AsChatResponseUpdatesAsync_ConvertsUpdatesAsync()
|
|
{
|
|
// Arrange
|
|
AgentResponseUpdate[] updates =
|
|
[
|
|
new(ChatRole.Assistant, "First"),
|
|
new(ChatRole.Assistant, "Second"),
|
|
];
|
|
|
|
// Act
|
|
List<ChatResponseUpdate> results = [];
|
|
await foreach (ChatResponseUpdate update in YieldAsync(updates).AsChatResponseUpdatesAsync())
|
|
{
|
|
results.Add(update);
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(2, results.Count);
|
|
Assert.Equal("First", Assert.IsType<TextContent>(results[0].Contents[0]).Text);
|
|
Assert.Equal("Second", Assert.IsType<TextContent>(results[1].Contents[0]).Text);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(IEnumerable<AgentResponseUpdate> updates)
|
|
{
|
|
foreach (AgentResponseUpdate update in updates)
|
|
{
|
|
await Task.Yield();
|
|
yield return update;
|
|
}
|
|
}
|
|
}
|