Hour-long remote tasks were never delivered: a subscribe watch held one SSE stream for up to a day, the remote sent nothing while its agent worked without text, and once the quiet connection died on the way (no FIN/RST) the watch waited on it with no error and no log line. - A subscription lasts at most 5 minutes (SubscriptionSlice), then GetTask and a new subscription: a pause, not a failure. A slice ended mid-read counts as ours even when it surfaces as an I/O error. - The shared handler connects with TCP keepalive (60 s, every 15 s, 4 probes) and HTTP/2 pings. - Log lines when a watch starts or is restored, when a slice ends on a running task, when a result is delivered or kept; the Telegram host's console log carries a UTC timestamp. - action=status on a settled task ends its watch, so the result isn't delivered a second time. - A "context busy" refusal of send says nothing was sent, names the watched task in that context and that its result will arrive by itself; refusals never touch a watch.
395 lines
21 KiB
C#
395 lines
21 KiB
C#
using Volcanic.AgenticCore.Extensions.A2A;
|
|
using Volcanic.AgenticCore.Extensions.Tests.A2A.Support;
|
|
using Volcanic.AgenticCore.Testing;
|
|
|
|
namespace Volcanic.AgenticCore.Extensions.Tests.A2A;
|
|
|
|
public class A2AToolTests
|
|
{
|
|
private static string Field(string text, string label) =>
|
|
System.Text.RegularExpressions.Regex.Match(text, label + @" ([A-Za-z0-9_-]+)").Groups[1].Value;
|
|
|
|
private static async Task WaitUntilAsync(Func<bool> condition, int timeoutMs = 15_000)
|
|
{
|
|
var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
|
|
while (!condition())
|
|
{
|
|
if (DateTime.UtcNow > deadline) throw new TimeoutException("condition not met");
|
|
await Task.Delay(20);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Agents_ListsCardsAndSkills()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
File.WriteAllText(Path.Combine(remote.Workspace, ".volcanic", "a2a", "card.json"),
|
|
"""{ "name": "Remote travel", "description": "Plans trips", "skills": [ { "id": "trips", "name": "Trips", "description": "Plans trips", "tags": ["travel"], "examples": ["Find a flight"] } ] }""");
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "agents" });
|
|
Assert.False(isError);
|
|
Assert.Contains("remote", text);
|
|
Assert.Contains("Plans trips", text);
|
|
Assert.Contains("trips", text);
|
|
Assert.Contains("Find a flight", text);
|
|
Assert.DoesNotContain(RemoteAgentServer.Key, text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Agents_UnreachableAgent_Listed()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, CardTimeoutSeconds = 2 });
|
|
host.WriteAgents("""[ { "name": "ghost", "url": "http://nowhere.invalid/missing/card.json", "key": "ghost-key-0123456789" } ]""");
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "agents" });
|
|
Assert.False(isError);
|
|
Assert.Contains("ghost", text);
|
|
Assert.Contains("unreachable", text);
|
|
Assert.DoesNotContain("ghost-key-0123456789", text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_CompletesWithinWait_ReturnsAnswerAndIds()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Paris is lovely"));
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "Where should I go?" });
|
|
Assert.False(isError);
|
|
Assert.Contains("completed", text);
|
|
Assert.Contains("Paris is lovely", text);
|
|
Assert.NotEmpty(Field(text, "task"));
|
|
Assert.NotEmpty(Field(text, "context"));
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_ContinuesContext()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("first"), FauxTurn.Say("second"));
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (first, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "one" });
|
|
var (second, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "two", contextId = Field(first, "context") });
|
|
Assert.Contains("second", second);
|
|
Assert.Equal(Field(first, "context"), Field(second, "context"));
|
|
Assert.Contains("first", string.Join("\n", remote.Faux.Requests.Last().Messages.Select(m => m.Text)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_InputRequired_ReturnsQuestion_AndAnswerContinues()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Tool("request_input", new { question = "Which date?" }), FauxTurn.Say("Booked Friday"));
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (question, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "Book a table" });
|
|
Assert.Contains("input-required", question);
|
|
Assert.Contains("Which date?", question);
|
|
var (answer, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "Friday", taskId = Field(question, "task") });
|
|
Assert.Contains("completed", answer);
|
|
Assert.Contains("Booked Friday", answer);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_SlowTask_StartsWatch_AndResultArrivesAsFollowUp()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Slow answer").WithDelay(TimeSpan.FromMilliseconds(1500)));
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, WaitSeconds = 0 });
|
|
var session = await host.CreateSessionAsync();
|
|
// The follow-up run gets the faux default answer.
|
|
var (text, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "take your time" });
|
|
Assert.True(text.Contains("working") || text.Contains("submitted"), text);
|
|
Assert.Contains("delivered", text);
|
|
Assert.Single(host.Registry.List(A2AClientHost.Target));
|
|
var delivered = await host.WaitForUserMessageAsync(session, "[A2A result from \"remote\"");
|
|
Assert.Contains("Slow answer", delivered);
|
|
Assert.Contains(host.Faux.Requests, r => r.Messages.Any(m => m.Text.Contains("Slow answer")));
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
await WaitUntilAsync(() => PendingFile.Read(host.Workspace).Count == 0); // removed right after the hand-over
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SessionStart_FlushesUndeliveredResult_AsFollowUp()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
PendingFile.Upsert(host.Workspace, new WatchRecord("w1", A2AClientHost.Target, "remote", "t1", "c1", null, DateTimeOffset.UtcNow,
|
|
WatchSources.Poll, "[A2A result from \"remote\", task t1 (completed)]\nKept for later"));
|
|
host.Faux.Enqueue(FauxTurn.Say("got it"));
|
|
var session = await host.CreateSessionAsync(); // returns before the flush's run (it runs off the creation path)
|
|
Assert.DoesNotContain(session.Messages, m => m.Text.Contains("Kept for later")); // not inside creation
|
|
// Read from the transcript, not the host's event subscription, so an early run can't be missed.
|
|
await WaitUntilAsync(() => session.Messages.Any(m => m is Abstractions.Messages.UserMessage && m.Text.Contains("Kept for later")));
|
|
var delivered = session.Messages.First(m => m.Text.Contains("Kept for later")).Text;
|
|
Assert.StartsWith("[A2A result from \"remote\"", delivered);
|
|
await WaitUntilAsync(() => PendingFile.Read(host.Workspace).Count == 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_FilesArgument_SentAsParts_AndFileArtifactSaved()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
File.WriteAllText(Path.Combine(remote.Workspace, "reply.txt"), "remote file");
|
|
remote.Faux.Enqueue(FauxTurn.Tool("send", new { path = "reply.txt" }), FauxTurn.Say("sent it"));
|
|
await using var host = new A2AClientHost(remote);
|
|
File.WriteAllText(Path.Combine(host.Workspace, "notes.txt"), "my notes");
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "here", files = new[] { "notes.txt" } });
|
|
Assert.False(isError, text);
|
|
Assert.Contains(remote.Faux.Requests.First().Messages, m => m.Text.Contains("Files: inbox/a2a-client-"));
|
|
var taskId = Field(text, "task");
|
|
Assert.Contains($"inbox/a2a/{taskId}/reply.txt (11 bytes", text);
|
|
Assert.Equal("remote file", File.ReadAllText(Path.Combine(host.Workspace, "inbox", "a2a", taskId, "reply.txt")));
|
|
var inbound = Assert.Single(Directory.GetFiles(Path.Combine(remote.Workspace, "inbox"), "a2a-client-*"));
|
|
Assert.Equal("my notes", File.ReadAllText(inbound));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_FileOutsideWorkspace_Error()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "x", files = new[] { "../../etc/passwd" } });
|
|
Assert.True(isError);
|
|
Assert.Contains("workspace", text);
|
|
Assert.Empty(remote.Faux.Requests);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public async Task Send_SymlinkOutsideWorkspace_Error(bool directoryLink)
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
var outside = Path.Combine(Path.GetTempPath(), "a2a-outside", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(outside);
|
|
try
|
|
{
|
|
File.WriteAllText(Path.Combine(outside, "secret.txt"), "secret");
|
|
string path;
|
|
if (directoryLink)
|
|
{
|
|
Directory.CreateSymbolicLink(Path.Combine(host.Workspace, "linked"), outside);
|
|
path = "linked/secret.txt";
|
|
}
|
|
else
|
|
{
|
|
File.CreateSymbolicLink(Path.Combine(host.Workspace, "secret.txt"), Path.Combine(outside, "secret.txt"));
|
|
path = "secret.txt";
|
|
}
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "x", files = new[] { path } });
|
|
Assert.True(isError);
|
|
Assert.Contains("outside the workspace", text);
|
|
Assert.Empty(remote.Faux.Requests);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(outside, true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_UnknownAgent_Error()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "nobody", message = "hi" });
|
|
Assert.True(isError);
|
|
Assert.Contains("nobody", text);
|
|
Assert.Contains("remote", text); // lists the configured ones
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_401_ReadableError_AndResultNeverContainsKey()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
host.WriteAgents($$"""[ { "name": "remote", "url": "{{RemoteAgentServer.BaseUrl}}", "key": "wrong-key-abcdefghij" } ]""");
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "hi" });
|
|
Assert.True(isError);
|
|
Assert.Contains("rejected", text);
|
|
Assert.DoesNotContain("wrong-key-abcdefghij", text);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Status_ReturnsTask_AndCancel_CancelsRemoteAndDropsWatch()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("slow").WithDelay(TimeSpan.FromSeconds(10)));
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, WaitSeconds = 0 });
|
|
var session = await host.CreateSessionAsync();
|
|
var (started, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "go" });
|
|
var taskId = Field(started, "task");
|
|
Assert.Single(host.Registry.List(A2AClientHost.Target));
|
|
var (status, _) = await host.CallToolAsync(session, new { action = "status", agent = "remote", taskId });
|
|
Assert.True(status.Contains("working") || status.Contains("submitted"), status);
|
|
var (cancelled, isError) = await host.CallToolAsync(session, new { action = "cancel", agent = "remote", taskId });
|
|
Assert.False(isError, cancelled);
|
|
Assert.Contains("canceled", cancelled);
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
Assert.Empty(PendingFile.Read(host.Workspace));
|
|
Assert.DoesNotContain(host.UserTexts(), t => t.Contains("[A2A result", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Status_SettledTask_EndsItsWatch_SoTheResultIsNotDeliveredTwice()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Answer by status").WithDelay(TimeSpan.FromMilliseconds(300)));
|
|
RemoteRewriter? rewriter = null;
|
|
// The watch's stream stays silent (a dead connection): only status can see the task finish.
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, WaitSeconds = 0 },
|
|
inner => rewriter = new RemoteRewriter(inner) { SilentSubscribes = true });
|
|
var session = await host.CreateSessionAsync();
|
|
var (started, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "go" });
|
|
var taskId = Field(started, "task");
|
|
Assert.Single(host.Registry.List(A2AClientHost.Target));
|
|
string status;
|
|
do
|
|
{
|
|
await Task.Delay(100);
|
|
(status, _) = await host.CallToolAsync(session, new { action = "status", agent = "remote", taskId });
|
|
} while (!status.Contains("Answer by status"));
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
Assert.Empty(PendingFile.Read(host.Workspace));
|
|
Assert.DoesNotContain(host.UserTexts(), t => t.Contains("[A2A result", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_BusyContext_SaysWhichTaskRunsAndThatItIsStillWatched()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("first").WithDelay(TimeSpan.FromSeconds(10)));
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, WaitSeconds = 0 });
|
|
var session = await host.CreateSessionAsync();
|
|
var (started, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "long job" });
|
|
var taskId = Field(started, "task");
|
|
var contextId = Field(started, "context");
|
|
var (refused, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "and another", contextId });
|
|
Assert.True(isError, refused);
|
|
Assert.Contains($"still working on task {taskId} in context {contextId}", refused);
|
|
Assert.Contains("still watched", refused);
|
|
Assert.Contains("without contextId", refused);
|
|
var watch = Assert.Single(host.Registry.List(A2AClientHost.Target)); // the refusal leaves the running task's watch alone
|
|
Assert.Equal(taskId, watch.TaskId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_AbortedWhileWaiting_TaskWatched_AndResultArrives()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Late answer").WithDelay(TimeSpan.FromMilliseconds(1500)));
|
|
RemoteRewriter? rewriter = null;
|
|
await using var host = new A2AClientHost(remote, handler: inner => rewriter = new RemoteRewriter(inner));
|
|
var session = await host.CreateSessionAsync();
|
|
host.Faux.Enqueue(FauxTurn.Tool("a2a", new { action = "send", agent = "remote", message = "take your time" }), FauxTurn.Say("ok"));
|
|
var run = session.PromptAsync("use the remote agent");
|
|
await WaitUntilAsync(() => rewriter!.Subscribes > 0); // the task exists and the tool waits on its stream
|
|
await session.AbortAsync();
|
|
try { await run; } catch (OperationCanceledException) { }
|
|
Assert.Single(host.Registry.List(A2AClientHost.Target));
|
|
var delivered = await host.WaitForUserMessageAsync(session, "[A2A result from \"remote\"");
|
|
Assert.Contains("Late answer", delivered);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_NonStreamingAgent_PollsWithinWait()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Polled answer").WithDelay(TimeSpan.FromMilliseconds(1200)));
|
|
RemoteRewriter? rewriter = null;
|
|
await using var host = new A2AClientHost(remote, handler: inner => rewriter = new RemoteRewriter(inner) { NoStreaming = true });
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "go" });
|
|
Assert.False(isError, text);
|
|
Assert.Contains("completed", text);
|
|
Assert.Contains("Polled answer", text);
|
|
Assert.Equal(0, rewriter!.Subscribes);
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_MessageReply_ReturnedAsText()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote, handler: inner => new RemoteRewriter(inner) { MessageReply = "Just a message" });
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "hi" });
|
|
Assert.False(isError, text);
|
|
Assert.Contains("replied (context ctx-message)", text);
|
|
Assert.Contains("Just a message", text);
|
|
Assert.Empty(host.Registry.List(A2AClientHost.Target));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_FileOverMaxBytes_Error()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, MaxFileBytes = 10 });
|
|
File.WriteAllText(Path.Combine(host.Workspace, "big.txt"), new string('x', 11));
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "x", files = new[] { "big.txt" } });
|
|
Assert.True(isError);
|
|
Assert.Contains("larger than 10 bytes", text);
|
|
Assert.Empty(remote.Faux.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Send_FilesTogetherOverMaxBytes_Error()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, MaxFileBytes = 10 });
|
|
File.WriteAllText(Path.Combine(host.Workspace, "a.txt"), new string('a', 6));
|
|
File.WriteAllText(Path.Combine(host.Workspace, "b.txt"), new string('b', 6));
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "x", files = new[] { "a.txt", "b.txt" } });
|
|
Assert.True(isError);
|
|
Assert.Contains("together are larger than 10 bytes", text);
|
|
Assert.Empty(remote.Faux.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Cancel_RemoteDown_KeepsWatching_AndResultStillArrives()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
remote.Faux.Enqueue(FauxTurn.Say("Survived").WithDelay(TimeSpan.FromMilliseconds(1500)));
|
|
RemoteRewriter? rewriter = null;
|
|
await using var host = new A2AClientHost(remote, new A2AClientSettings { Enabled = true, WaitSeconds = 0 }, inner => rewriter = new RemoteRewriter(inner));
|
|
var session = await host.CreateSessionAsync();
|
|
var (started, _) = await host.CallToolAsync(session, new { action = "send", agent = "remote", message = "go" });
|
|
var watch = Assert.Single(host.Registry.List(A2AClientHost.Target));
|
|
rewriter!.Down = true;
|
|
var (cancelled, isError) = await host.CallToolAsync(session, new { action = "cancel", agent = "remote", taskId = Field(started, "task") });
|
|
Assert.True(isError);
|
|
Assert.Contains("still watched", cancelled);
|
|
Assert.Equal(watch.WatchId, Assert.Single(host.Registry.List(A2AClientHost.Target)).WatchId);
|
|
Assert.Contains(PendingFile.Read(host.Workspace), r => r.WatchId == watch.WatchId);
|
|
rewriter.Down = false;
|
|
var delivered = await host.WaitForUserMessageAsync(session, "[A2A result from \"remote\"");
|
|
Assert.Contains("Survived", delivered);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoAgentsConfigured_ExplainsFile()
|
|
{
|
|
await using var remote = await RemoteAgentServer.StartAsync();
|
|
await using var host = new A2AClientHost(remote);
|
|
File.Delete(AgentsFile.PathFor(host.Workspace));
|
|
var session = await host.CreateSessionAsync();
|
|
var (text, isError) = await host.CallToolAsync(session, new { action = "agents" });
|
|
Assert.False(isError);
|
|
Assert.Contains(".volcanic/a2a/agents.json", text);
|
|
}
|
|
}
|