Files
VolcanicAgenticCore/tests/Volcanic.AgenticCore.Extensions.Tests/A2A/A2AResultsTests.cs
EugeneTes a92623b89b fix(a2a-client): save every file the remote agent returns, per task, with size and type
File parts were only taken from artifacts as inline bytes up to 10 MB; parts in the
status message or the history were dropped silently, URL parts were never downloaded,
and a plain message reply lost its files. Files now go to inbox/a2a/<taskId>/ from
artifacts, status message and recent agent messages (deduplicated), URLs are streamed
to disk (agent key only on the agent's origin, else https without it), the cap is the
new MaxReceivedFileBytes (200 MB), and results list path, bytes and media type or why
a file was not saved. Re-saving a task overwrites, so action=status refetches files.
2026-10-12 16:16:14 +00:00

74 lines
3.2 KiB
C#

using System.Text;
using A2A;
using Volcanic.AgenticCore.Extensions.A2A;
using A2AMessage = global::A2A.Message;
using A2ATaskStatus = global::A2A.TaskStatus;
namespace Volcanic.AgenticCore.Extensions.Tests.A2A;
public sealed class A2AResultsTests : IDisposable
{
private readonly string _ws = Path.Combine(Path.GetTempPath(), "a2a-results", Guid.NewGuid().ToString("N"));
private static AgentTask Task_(TaskState state, string? statusText = null, params Artifact[] artifacts) => new()
{
Id = "task-123456789", ContextId = "ctx-1",
Status = new A2ATaskStatus
{
State = state,
Message = statusText is null ? null : new A2AMessage { Role = Role.Agent, MessageId = "m", Parts = [Part.FromText(statusText)] },
},
Artifacts = [.. artifacts],
};
[Fact]
public void FinalText_PrefersResponseArtifact()
{
var task = Task_(TaskState.Completed, null,
new Artifact { ArtifactId = "other", Parts = [Part.FromText("side")] },
new Artifact { ArtifactId = "response", Parts = [Part.FromText("Booked "), Part.FromText("Friday")] });
Assert.Equal("Booked Friday", A2AResults.FinalText(task));
}
[Fact]
public void FinalText_FallsBackToTextArtifactsThenStatus()
{
Assert.Equal("side", A2AResults.FinalText(Task_(TaskState.Completed, null, new Artifact { ArtifactId = "x", Parts = [Part.FromText("side")] })));
Assert.Equal("why", A2AResults.FinalText(Task_(TaskState.Failed, "why")));
}
[Fact]
public void Describe_InputRequired_HasQuestionAndHint()
{
var text = A2AResults.Describe("travel-bot", Task_(TaskState.InputRequired, "Which date?"), []);
Assert.Contains("input-required", text);
Assert.Contains("Which date?", text);
Assert.Contains("action=send", text);
Assert.Contains("task-123456789", text);
Assert.Contains("ctx-1", text);
}
[Fact]
public void DeliveryText_HasHeader()
{
var text = A2AResults.DeliveryText("travel-bot", Task_(TaskState.Completed, null,
new Artifact { ArtifactId = "response", Parts = [Part.FromText("Done")] }),
[new ReceivedFile("x.pdf", 4, "application/pdf", "inbox/a2a/task-123456789/x.pdf"),
new ReceivedFile("big.tar.gz", 300, null, null, "over 100 bytes")]);
Assert.StartsWith("[A2A result from \"travel-bot\", task task-123456789 (completed)]", text);
Assert.Contains("Done", text);
Assert.Contains("\nFiles:\n- inbox/a2a/task-123456789/x.pdf (4 bytes, application/pdf)\n- big.tar.gz (300 bytes): not saved, over 100 bytes", text);
}
[Fact]
public void Describe_InputRequired_QuestionComesBeforeStaleResponseArtifact()
{
var task = Task_(TaskState.InputRequired, "Which date?",
new Artifact { ArtifactId = "response", Parts = [Part.FromText("old answer")] });
var text = A2AResults.Describe("travel-bot", task, []);
Assert.True(text.IndexOf("Which date?", StringComparison.Ordinal) < text.IndexOf("old answer", StringComparison.Ordinal));
}
public void Dispose() { try { Directory.Delete(_ws, true); } catch (IOException) { } }
}