From 7b044c543730320b480d5379a47243db43c1a5df Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Wed, 7 Oct 2026 13:25:33 +0000 Subject: [PATCH] test(a2a): streaming, input-required, busy contexts, cancel, subscribe, files and hops --- .../ServerTaskTests.cs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs diff --git a/tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs b/tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs new file mode 100644 index 0000000..223ee13 --- /dev/null +++ b/tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs @@ -0,0 +1,151 @@ +using System.Text; +using A2A; +using Volcanic.AgenticCore.A2A.Server.Tests.Support; +using Volcanic.AgenticCore.Testing; + +namespace Volcanic.AgenticCore.A2A.Server.Tests; + +public class ServerTaskTests +{ + private static async Task> CollectAsync(IAsyncEnumerable stream) + { + var list = new List(); + await foreach (var e in stream) list.Add(e); + return list; + } + + [Fact] + public async Task Stream_TextDeltas_AreArtifactAppends() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Say("A fairly long answer that the faux provider streams in several chunks.")); + var events = await CollectAsync(host.Client().SendStreamingMessageAsync(Requests.Text("hi"), CancellationToken.None)); + var appends = events.Where(e => e.ArtifactUpdate is { Append: true, Artifact.ArtifactId: "response" }).ToList(); + Assert.True(appends.Count > 1); + Assert.Equal(TaskState.Completed, events.Last(e => e.StatusUpdate is not null).StatusUpdate!.Status.State); + } + + [Fact] + public async Task Stream_ToolStart_IsWorkingStatus() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Tool("notify_owner", new { text = "hello owner" }), FauxTurn.Say("done")); + var events = await CollectAsync(host.Client().SendStreamingMessageAsync(Requests.Text("hi"), CancellationToken.None)); + Assert.Contains(events, e => e.StatusUpdate is { Status.State: TaskState.Working } s && s.Status.Message?.Parts[0].Text == "using notify_owner"); + } + + [Fact] + public async Task NotifyOwner_CallsNotifier() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Tool("notify_owner", new { text = "hello owner" }), FauxTurn.Say("done")); + await host.Client().SendMessageAsync(Requests.Text("hi"), CancellationToken.None); + Assert.Equal(("alpha", "hello owner"), Assert.Single(host.Notifier.Sent)); + } + + [Fact] + public async Task NotifyOwner_AbsentWithoutNotifier() + { + await using var host = await A2ATestServer.StartAsync(withNotifier: false); + host.Faux.Enqueue(FauxTurn.Say("ok")); + await host.Client().SendMessageAsync(Requests.Text("hi"), CancellationToken.None); + Assert.DoesNotContain(host.Faux.Requests.Last().Tools, t => t.Name == "notify_owner"); + Assert.Contains(host.Faux.Requests.Last().Tools, t => t.Name == "request_input"); + } + + [Fact] + public async Task Stream_SendTool_ProducesArtifact() + { + await using var host = await A2ATestServer.StartAsync(); + File.WriteAllText(Path.Combine(host.Workspace, "report.txt"), "the report"); + host.Faux.Enqueue(FauxTurn.Tool("send", new { path = "report.txt" }), FauxTurn.Say("sent")); + var task = (await host.Client().SendMessageAsync(Requests.Text("send me the report"), CancellationToken.None)).Task!; + var file = Assert.Single(task.Artifacts!, a => a.Name == "report.txt"); + Assert.Equal("the report", Encoding.UTF8.GetString(file.Parts[0].Raw!)); + } + + [Fact] + public async Task RequestInput_SetsInputRequired_ContinuationResumesTask() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Tool("request_input", new { question = "Which date?" }), FauxTurn.Say("Booked for Friday")); + var client = host.Client(); + var first = (await client.SendMessageAsync(Requests.Text("book a table"), CancellationToken.None)).Task!; + Assert.Equal(TaskState.InputRequired, first.Status.State); + Assert.Equal("Which date?", first.Status.Message!.Parts[0].Text); + var second = (await client.SendMessageAsync(Requests.Text("Friday", contextId: first.ContextId, taskId: first.Id), CancellationToken.None)).Task!; + Assert.Equal(first.Id, second.Id); + Assert.Equal(TaskState.Completed, second.Status.State); + Assert.Equal("Booked for Friday", Requests.ResponseText(second)); + } + + [Fact] + public async Task Send_ContextBusy_Rejected() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Say("first"), FauxTurn.Say("slow").WithDelay(TimeSpan.FromSeconds(5))); + var client = host.Client(); + var first = (await client.SendMessageAsync(Requests.Text("one"), CancellationToken.None)).Task!; + var slow = (await client.SendMessageAsync(Requests.Text("two", contextId: first.ContextId, returnImmediately: true), CancellationToken.None)).Task!; + await Requests.WaitForStateAsync(client, slow.Id, s => s == TaskState.Working); + var ex = await Assert.ThrowsAsync(() => + client.SendMessageAsync(Requests.Text("three", contextId: first.ContextId), CancellationToken.None)); + Assert.Equal(A2AErrorCode.UnsupportedOperation, ex.ErrorCode); + await client.CancelTaskAsync(new CancelTaskRequest { Id = slow.Id }, CancellationToken.None); + } + + [Fact] + public async Task Cancel_AbortsRun() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Say("slow").WithDelay(TimeSpan.FromSeconds(10))); + var client = host.Client(); + var task = (await client.SendMessageAsync(Requests.Text("go", returnImmediately: true), CancellationToken.None)).Task!; + await Requests.WaitForStateAsync(client, task.Id, s => s == TaskState.Working); + await client.CancelTaskAsync(new CancelTaskRequest { Id = task.Id }, CancellationToken.None); + var final = await Requests.WaitForStateAsync(client, task.Id, s => s.IsTerminal()); + Assert.Equal(TaskState.Canceled, final.Status.State); + } + + [Fact] + public async Task Subscribe_LiveTask_GetsEvents() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Say("eventually").WithDelay(TimeSpan.FromMilliseconds(500))); + var client = host.Client(); + var task = (await client.SendMessageAsync(Requests.Text("go", returnImmediately: true), CancellationToken.None)).Task!; + var events = await CollectAsync(client.SubscribeToTaskAsync(new SubscribeToTaskRequest { Id = task.Id }, CancellationToken.None)); + Assert.Contains(events, e => e.StatusUpdate?.Status.State == TaskState.Completed || e.Task?.Status.State == TaskState.Completed); + } + + [Fact] + public async Task Subscribe_TerminalTask_Unsupported() + { + await using var host = await A2ATestServer.StartAsync(); + host.Faux.Enqueue(FauxTurn.Say("done")); + var client = host.Client(); + var task = (await client.SendMessageAsync(Requests.Text("go"), CancellationToken.None)).Task!; + var ex = await Assert.ThrowsAsync(() => CollectAsync(client.SubscribeToTaskAsync(new SubscribeToTaskRequest { Id = task.Id }, CancellationToken.None))); + Assert.Equal(A2AErrorCode.UnsupportedOperation, ex.ErrorCode); + } + + [Fact] + public async Task Files_OverCap_InvalidParams() + { + await using var host = await A2ATestServer.StartAsync(o => o.MaxFileBytes = 4); + var big = Part.FromRaw(Encoding.UTF8.GetBytes("too large"), "text/plain", "a.txt"); + var ex = await Assert.ThrowsAsync(() => + host.Client().SendMessageAsync(Requests.Text("file", extra: big), CancellationToken.None)); + Assert.Equal(A2AErrorCode.InvalidParams, ex.ErrorCode); + } + + [Fact] + public async Task Hops_OverLimit_TaskFailed() + { + await using var host = await A2ATestServer.StartAsync(); + var task = (await host.Client().SendMessageAsync(Requests.Text("hi", hops: 4), CancellationToken.None)).Task!; + Assert.Equal(TaskState.Failed, task.Status.State); + Assert.Contains("hop limit", task.Status.Message!.Parts[0].Text); + Assert.Empty(host.Faux.Requests); + } +}