test(a2a): streaming, input-required, busy contexts, cancel, subscribe, files and hops
This commit is contained in:
151
tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs
Normal file
151
tests/Volcanic.AgenticCore.A2A.Server.Tests/ServerTaskTests.cs
Normal file
@@ -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<List<StreamResponse>> CollectAsync(IAsyncEnumerable<StreamResponse> stream)
|
||||
{
|
||||
var list = new List<StreamResponse>();
|
||||
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<A2AException>(() =>
|
||||
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<A2AException>(() => 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<A2AException>(() =>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user