.NET: Add support for background responses to A2A agent (#2381)

* add support for baackground responses to a2a agent

* fix line endings

* address pr review comments

* address pr review comments

* update sample to net10.0

* Update dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/README.md

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentTaskExtensions.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Update dotnet/samples/GettingStarted/A2A/A2AAgent_PollingForTaskCompletion/Program.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* address pr review feedback

* add clarification regarding background responses

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2025-11-24 19:41:06 +00:00
committed by GitHub
co-authored by Roger Barreto
parent bcbf1b33e8
commit a610a4769c
17 changed files with 1360 additions and 57 deletions
@@ -367,6 +367,7 @@ public sealed class A2AAgentTests : IDisposable
// Act & Assert
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages))
{
// Just iterate through to trigger the logic
}
}
@@ -396,15 +397,422 @@ public sealed class A2AAgentTests : IDisposable
Assert.Equal("https://example.com/file.pdf", ((FilePart)message.Parts[1]).File.Uri?.ToString());
}
[Fact]
public async Task RunAsync_WithContinuationTokenAndMessages_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => this._agent.RunAsync(inputMessages, null, options));
}
[Fact]
public async Task RunAsync_WithContinuationToken_CallsGetTaskAsyncAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-123",
ContextId = "context-123"
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act
await this._agent.RunAsync([], options: options);
// Assert
Assert.Equal("tasks/get", this._handler.CapturedJsonRpcRequest?.Method);
Assert.Equal("task-123", this._handler.CapturedTaskIdParams?.Id);
}
[Fact]
public async Task RunAsync_WithTaskInThreadAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentMessage
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
thread.TaskId = "task-123";
var inputMessage = new ChatMessage(ChatRole.User, "Please make the background transparent");
// Act
await this._agent.RunAsync(inputMessage, thread);
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
}
[Fact]
public async Task RunAsync_WithAgentTask_UpdatesThreadTaskIdAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
// Act
await this._agent.RunAsync("Start a task", thread);
// Assert
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("task-456", a2aThread.TaskId);
}
[Fact]
public async Task RunAsync_WithAgentTaskResponse_ReturnsTaskResponseCorrectlyAsync()
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-789",
ContextId = "context-456",
Status = new() { State = TaskState.Submitted },
Metadata = new Dictionary<string, JsonElement>
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "count", JsonSerializer.SerializeToElement(42) }
}
};
var thread = this._agent.GetNewThread();
// Act
var result = await this._agent.RunAsync("Start a long-running task", thread);
// Assert - verify task is converted correctly
Assert.NotNull(result);
Assert.Equal(this._agent.Id, result.AgentId);
Assert.Equal("task-789", result.ResponseId);
Assert.NotNull(result.RawRepresentation);
Assert.IsType<AgentTask>(result.RawRepresentation);
Assert.Equal("task-789", ((AgentTask)result.RawRepresentation).Id);
// Assert - verify continuation token is set for submitted task
Assert.NotNull(result.ContinuationToken);
Assert.IsType<A2AContinuationToken>(result.ContinuationToken);
Assert.Equal("task-789", ((A2AContinuationToken)result.ContinuationToken).TaskId);
// Assert - verify thread is updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("context-456", a2aThread.ContextId);
Assert.Equal("task-789", a2aThread.TaskId);
// Assert - verify metadata is preserved
Assert.NotNull(result.AdditionalProperties);
Assert.NotNull(result.AdditionalProperties["key1"]);
Assert.Equal("value1", ((JsonElement)result.AdditionalProperties["key1"]!).GetString());
Assert.NotNull(result.AdditionalProperties["count"]);
Assert.Equal(42, ((JsonElement)result.AdditionalProperties["count"]!).GetInt32());
}
[Theory]
[InlineData(TaskState.Submitted)]
[InlineData(TaskState.Working)]
[InlineData(TaskState.Completed)]
[InlineData(TaskState.Failed)]
[InlineData(TaskState.Canceled)]
public async Task RunAsync_WithVariousTaskStates_ReturnsCorrectTokenAsync(TaskState taskState)
{
// Arrange
this._handler.ResponseToReturn = new AgentTask
{
Id = "task-123",
ContextId = "context-123",
Status = new() { State = taskState }
};
// Act
var result = await this._agent.RunAsync("Test message");
// Assert
if (taskState == TaskState.Submitted || taskState == TaskState.Working)
{
Assert.NotNull(result.ContinuationToken);
}
else
{
Assert.Null(result.ContinuationToken);
}
}
[Fact]
public async Task RunStreamingAsync_WithContinuationTokenAndMessages_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
var inputMessages = new List<ChatMessage>
{
new(ChatRole.User, "Test message")
};
var options = new AgentRunOptions { ContinuationToken = new A2AContinuationToken("task-123") };
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var _ in this._agent.RunStreamingAsync(inputMessages, null, options))
{
// Just iterate through to trigger the exception
}
});
}
[Fact]
public async Task RunStreamingAsync_WithTaskInThreadAndMessage_AddTaskAsReferencesToMessageAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentMessage
{
MessageId = "response-123",
Role = MessageRole.Agent,
Parts = [new TextPart { Text = "Response to task" }]
};
var thread = (A2AAgentThread)this._agent.GetNewThread();
thread.TaskId = "task-123";
// Act
await foreach (var _ in this._agent.RunStreamingAsync("Please make the background transparent", thread))
{
// Just iterate through to trigger the logic
}
// Assert
var message = this._handler.CapturedMessageSendParams?.Message;
Assert.Null(message?.TaskId);
Assert.NotNull(message?.ReferenceTaskIds);
Assert.Contains("task-123", message.ReferenceTaskIds);
}
[Fact]
public async Task RunStreamingAsync_WithAgentTask_UpdatesThreadTaskIdAsync()
{
// Arrange
this._handler.StreamingResponseToReturn = new AgentTask
{
Id = "task-456",
ContextId = "context-789",
Status = new() { State = TaskState.Submitted }
};
var thread = this._agent.GetNewThread();
// Act
await foreach (var _ in this._agent.RunStreamingAsync("Start a task", thread))
{
// Just iterate through to trigger the logic
}
// Assert
var a2aThread = (A2AAgentThread)thread;
Assert.Equal("task-456", a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithAgentMessage_YieldsResponseUpdateAsync()
{
// Arrange
const string MessageId = "msg-123";
const string ContextId = "ctx-456";
const string MessageText = "Hello from agent!";
this._handler.StreamingResponseToReturn = new AgentMessage
{
MessageId = MessageId,
Role = MessageRole.Agent,
ContextId = ContextId,
Parts =
[
new TextPart { Text = MessageText }
]
};
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Test message"))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(MessageId, update0.MessageId);
Assert.Equal(MessageId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.Equal(MessageText, update0.Text);
Assert.IsType<AgentMessage>(update0.RawRepresentation);
Assert.Equal(MessageId, ((AgentMessage)update0.RawRepresentation!).MessageId);
}
[Fact]
public async Task RunStreamingAsync_WithAgentTask_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-789";
const string ContextId = "ctx-012";
this._handler.StreamingResponseToReturn = new AgentTask
{
Id = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Submitted },
Artifacts = [
new()
{
ArtifactId = "art-123",
Parts = [new TextPart { Text = "Task artifact content" }]
}
]
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Start long-running task", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded from artifact
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<AgentTask>(update0.RawRepresentation);
Assert.Equal(TaskId, ((AgentTask)update0.RawRepresentation!).Id);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithTaskStatusUpdateEvent_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-status-123";
const string ContextId = "ctx-status-456";
this._handler.StreamingResponseToReturn = new TaskStatusUpdateEvent
{
TaskId = TaskId,
ContextId = ContextId,
Status = new() { State = TaskState.Working }
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Check task status", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<TaskStatusUpdateEvent>(update0.RawRepresentation);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
[Fact]
public async Task RunStreamingAsync_WithTaskArtifactUpdateEvent_YieldsResponseUpdateAsync()
{
// Arrange
const string TaskId = "task-artifact-123";
const string ContextId = "ctx-artifact-456";
const string ArtifactContent = "Task artifact data";
this._handler.StreamingResponseToReturn = new TaskArtifactUpdateEvent
{
TaskId = TaskId,
ContextId = ContextId,
Artifact = new()
{
ArtifactId = "artifact-789",
Parts = [new TextPart { Text = ArtifactContent }]
}
};
var thread = this._agent.GetNewThread();
// Act
var updates = new List<AgentRunResponseUpdate>();
await foreach (var update in this._agent.RunStreamingAsync("Process artifact", thread))
{
updates.Add(update);
}
// Assert - one update should be yielded
Assert.Single(updates);
var update0 = updates[0];
Assert.Equal(ChatRole.Assistant, update0.Role);
Assert.Equal(TaskId, update0.ResponseId);
Assert.Equal(this._agent.Id, update0.AgentId);
Assert.IsType<TaskArtifactUpdateEvent>(update0.RawRepresentation);
// Assert - artifact content should be in the update
Assert.NotEmpty(update0.Contents);
Assert.Equal(ArtifactContent, update0.Text);
// Assert - thread should be updated with context and task IDs
var a2aThread = (A2AAgentThread)thread;
Assert.Equal(ContextId, a2aThread.ContextId);
Assert.Equal(TaskId, a2aThread.TaskId);
}
public void Dispose()
{
this._handler.Dispose();
this._httpClient.Dispose();
}
internal sealed class A2AClientHttpMessageHandlerStub : HttpMessageHandler
{
public JsonRpcRequest? CapturedJsonRpcRequest { get; set; }
public MessageSendParams? CapturedMessageSendParams { get; set; }
public TaskIdParams? CapturedTaskIdParams { get; set; }
public A2AEvent? ResponseToReturn { get; set; }
public A2AEvent? StreamingResponseToReturn { get; set; }
@@ -416,9 +824,19 @@ public sealed class A2AAgentTests : IDisposable
var content = await request.Content!.ReadAsStringAsync();
#pragma warning restore CA2016
var jsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content)!;
this.CapturedJsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content);
this.CapturedMessageSendParams = jsonRpcRequest.Params?.Deserialize<MessageSendParams>();
try
{
this.CapturedMessageSendParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<MessageSendParams>();
}
catch { /* Ignore deserialization errors for non-MessageSendParams requests */ }
try
{
this.CapturedTaskIdParams = this.CapturedJsonRpcRequest?.Params?.Deserialize<TaskIdParams>();
}
catch { /* Ignore deserialization errors for non-TaskIdParams requests */ }
// Return the pre-configured non-streaming response
if (this.ResponseToReturn is not null)
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AAgentThread"/> class.
/// </summary>
public sealed class A2AAgentThreadTests
{
[Fact]
public void Constructor_RoundTrip_SerializationPreservesState()
{
// Arrange
const string ContextId = "context-rt-001";
const string TaskId = "task-rt-002";
A2AAgentThread originalThread = new() { ContextId = ContextId, TaskId = TaskId };
// Act
JsonElement serialized = originalThread.Serialize();
A2AAgentThread deserializedThread = new(serialized);
// Assert
Assert.Equal(originalThread.ContextId, deserializedThread.ContextId);
Assert.Equal(originalThread.TaskId, deserializedThread.TaskId);
}
}
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AContinuationToken"/> class.
/// </summary>
public sealed class A2AContinuationTokenTests
{
[Fact]
public void Constructor_WithValidTaskId_InitializesTaskIdProperty()
{
// Arrange
const string TaskId = "task-123";
// Act
var token = new A2AContinuationToken(TaskId);
// Assert
Assert.Equal(TaskId, token.TaskId);
}
[Fact]
public void ToBytes_WithValidToken_SerializesToJsonBytes()
{
// Arrange
const string TaskId = "task-456";
var token = new A2AContinuationToken(TaskId);
// Act
var bytes = token.ToBytes();
// Assert
Assert.NotEqual(0, bytes.Length);
var jsonString = System.Text.Encoding.UTF8.GetString(bytes.ToArray());
using var jsonDoc = JsonDocument.Parse(jsonString);
var root = jsonDoc.RootElement;
Assert.True(root.TryGetProperty("taskId", out var taskIdElement));
Assert.Equal(TaskId, taskIdElement.GetString());
}
[Fact]
public void FromToken_WithA2AContinuationToken_ReturnsSameInstance()
{
// Arrange
const string TaskId = "task-direct";
var originalToken = new A2AContinuationToken(TaskId);
// Act
var resultToken = A2AContinuationToken.FromToken(originalToken);
// Assert
Assert.Same(originalToken, resultToken);
Assert.Equal(TaskId, resultToken.TaskId);
}
[Fact]
public void FromToken_WithSerializedToken_DeserializesCorrectly()
{
// Arrange
const string TaskId = "task-deserialized";
var originalToken = new A2AContinuationToken(TaskId);
var serialized = originalToken.ToBytes();
// Create a mock token wrapper to pass to FromToken
var mockToken = new MockResponseContinuationToken(serialized);
// Act
var resultToken = A2AContinuationToken.FromToken(mockToken);
// Assert
Assert.Equal(TaskId, resultToken.TaskId);
Assert.IsType<A2AContinuationToken>(resultToken);
}
[Fact]
public void FromToken_RoundTrip_PreservesTaskId()
{
// Arrange
const string TaskId = "task-roundtrip-123";
var originalToken = new A2AContinuationToken(TaskId);
var serialized = originalToken.ToBytes();
var mockToken = new MockResponseContinuationToken(serialized);
// Act
var deserializedToken = A2AContinuationToken.FromToken(mockToken);
var reserialized = deserializedToken.ToBytes();
var mockToken2 = new MockResponseContinuationToken(reserialized);
var deserializedAgain = A2AContinuationToken.FromToken(mockToken2);
// Assert
Assert.Equal(TaskId, deserializedAgain.TaskId);
}
[Fact]
public void FromToken_WithEmptyData_ThrowsArgumentException()
{
// Arrange
var emptyToken = new MockResponseContinuationToken(ReadOnlyMemory<byte>.Empty);
// Act & Assert
Assert.Throws<ArgumentException>(() => A2AContinuationToken.FromToken(emptyToken));
}
[Fact]
public void FromToken_WithMissingTaskIdProperty_ThrowsException()
{
// Arrange
var jsonWithoutTaskId = System.Text.Encoding.UTF8.GetBytes("{ \"someOtherProperty\": \"value\" }").AsMemory();
var mockToken = new MockResponseContinuationToken(jsonWithoutTaskId);
// Act & Assert
Assert.Throws<JsonException>(() => A2AContinuationToken.FromToken(mockToken));
}
[Fact]
public void FromToken_WithValidTaskId_ParsesTaskIdCorrectly()
{
// Arrange
const string TaskId = "task-multi-prop";
var json = System.Text.Encoding.UTF8.GetBytes($"{{ \"taskId\": \"{TaskId}\" }}").AsMemory();
var mockToken = new MockResponseContinuationToken(json);
// Act
var resultToken = A2AContinuationToken.FromToken(mockToken);
// Assert
Assert.Equal(TaskId, resultToken.TaskId);
}
/// <summary>
/// Mock implementation of ResponseContinuationToken for testing.
/// </summary>
private sealed class MockResponseContinuationToken : ResponseContinuationToken
{
private readonly ReadOnlyMemory<byte> _data;
public MockResponseContinuationToken(ReadOnlyMemory<byte> data)
{
this._data = data;
}
public override ReadOnlyMemory<byte> ToBytes()
{
return this._data;
}
}
}
@@ -0,0 +1,169 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AAgentTaskExtensions"/> class.
/// </summary>
public sealed class A2AAgentTaskExtensionsTests
{
[Fact]
public void ToChatMessages_WithNullAgentTask_ThrowsArgumentNullException()
{
// Arrange
AgentTask agentTask = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agentTask.ToChatMessages());
}
[Fact]
public void ToAIContents_WithNullAgentTask_ThrowsArgumentNullException()
{
// Arrange
AgentTask agentTask = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => agentTask.ToAIContents());
}
[Fact]
public void ToChatMessages_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessages_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToAIContents_WithEmptyArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.Null(result);
}
[Fact]
public void ToAIContents_WithNullArtifactsAndNoUserInputRequests_ReturnsNull()
{
// Arrange
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = null,
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessages_WithValidArtifact_ReturnsChatMessages()
{
// Arrange
var artifact = new Artifact
{
Parts = [new TextPart { Text = "response" }],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<ChatMessage>? result = agentTask.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.All(result, msg => Assert.Equal(ChatRole.Assistant, msg.Role));
Assert.Equal("response", result[0].Contents[0].ToString());
}
[Fact]
public void ToAIContents_WithMultipleArtifacts_FlattenAllContents()
{
// Arrange
var artifact1 = new Artifact
{
Parts = [new TextPart { Text = "content1" }],
};
var artifact2 = new Artifact
{
Parts =
[
new TextPart { Text = "content2" },
new TextPart { Text = "content3" }
],
};
var agentTask = new AgentTask
{
Id = "task1",
Artifacts = [artifact1, artifact2],
Status = new AgentTaskStatus { State = TaskState.Completed },
};
// Act
IList<AIContent>? result = agentTask.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Equal(3, result.Count);
Assert.Equal("content1", result[0].ToString());
Assert.Equal("content2", result[1].ToString());
Assert.Equal("content3", result[2].ToString());
}
}
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.A2A.UnitTests;
/// <summary>
/// Unit tests for the <see cref="A2AArtifactExtensions"/> class.
/// </summary>
public sealed class A2AArtifactExtensionsTests
{
[Fact]
public void ToChatMessage_WithMultiplePartsMetadataAndRawRepresentation_ReturnsCorrectChatMessage()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-comprehensive",
Name = "comprehensive-artifact",
Parts =
[
new TextPart { Text = "First part" },
new TextPart { Text = "Second part" },
new TextPart { Text = "Third part" }
],
Metadata = new Dictionary<string, JsonElement>
{
{ "key1", JsonSerializer.SerializeToElement("value1") },
{ "key2", JsonSerializer.SerializeToElement(42) }
}
};
// Act
var result = artifact.ToChatMessage();
// Assert - Verify multiple parts
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
Assert.Equal(3, result.Contents.Count);
Assert.All(result.Contents, content => Assert.IsType<TextContent>(content));
Assert.Equal("First part", ((TextContent)result.Contents[0]).Text);
Assert.Equal("Second part", ((TextContent)result.Contents[1]).Text);
Assert.Equal("Third part", ((TextContent)result.Contents[2]).Text);
// Assert - Verify metadata conversion to AdditionalProperties
Assert.NotNull(result.AdditionalProperties);
Assert.Equal(2, result.AdditionalProperties.Count);
Assert.True(result.AdditionalProperties.ContainsKey("key1"));
Assert.True(result.AdditionalProperties.ContainsKey("key2"));
// Assert - Verify RawRepresentation is set to artifact
Assert.NotNull(result.RawRepresentation);
Assert.Same(artifact, result.RawRepresentation);
}
[Fact]
public void ToAIContents_WithMultipleParts_ReturnsCorrectList()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-ai-multi",
Name = "test",
Parts = new List<Part>
{
new TextPart { Text = "Part 1" },
new TextPart { Text = "Part 2" },
new TextPart { Text = "Part 3" }
},
Metadata = null
};
// Act
var result = artifact.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Count);
Assert.All(result, content => Assert.IsType<TextContent>(content));
Assert.Equal("Part 1", ((TextContent)result[0]).Text);
Assert.Equal("Part 2", ((TextContent)result[1]).Text);
Assert.Equal("Part 3", ((TextContent)result[2]).Text);
}
[Fact]
public void ToAIContents_WithEmptyParts_ReturnsEmptyList()
{
// Arrange
var artifact = new Artifact
{
ArtifactId = "artifact-empty",
Name = "test",
Parts = new List<Part>(),
Metadata = null
};
// Act
var result = artifact.ToAIContents();
// Assert
Assert.NotNull(result);
Assert.Empty(result);
}
}