fix(telegram): clean exit on A2A bind failure, log listen URLs, /a2a polish
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Volcanic.AgenticCore.A2A.Server;
|
||||
using Volcanic.AgenticCore.A2A.Server.Agent;
|
||||
using Volcanic.AgenticCore.A2A.Server.Callers;
|
||||
using Volcanic.AgenticCore.A2A.Server.Hosting;
|
||||
using Volcanic.AgenticCore.A2A.Server.Tasks;
|
||||
using Volcanic.AgenticCore.Telegram.Chats;
|
||||
using Volcanic.AgenticCore.Telegram.A2A;
|
||||
using Volcanic.AgenticCore.Telegram.Tests.Support;
|
||||
|
||||
@@ -72,4 +79,76 @@ public class A2AHostTests
|
||||
await host.SettleAsync();
|
||||
Assert.Contains("A2A is off", host.LastReply());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A2aCommand_ShortKeyFullyMasked_TaskAge()
|
||||
{
|
||||
var time = new FakeTimeProvider(new DateTimeOffset(2026, 10, 7, 12, 0, 0, TimeSpan.Zero));
|
||||
await using var host = new ChatHost(time: time, a2a: new A2AHostOptions { Enabled = true });
|
||||
WriteCallers(host.WorkspaceOf(), """[ { "name": "travel-bot", "key": "short-1234" } ]""");
|
||||
Directory.CreateDirectory(A2ATaskFiles.Dir(host.WorkspaceOf()));
|
||||
A2ATaskFiles.Write(A2ATaskFiles.PathFor(host.WorkspaceOf(), "task0001abc"), new A2ATaskFile
|
||||
{
|
||||
Caller = "travel-bot",
|
||||
Task = new global::A2A.AgentTask
|
||||
{
|
||||
Id = "task0001abc", ContextId = "c1",
|
||||
Status = new global::A2A.TaskStatus { State = global::A2A.TaskState.Completed, Timestamp = time.GetUtcNow() - TimeSpan.FromMinutes(5) },
|
||||
},
|
||||
});
|
||||
await host.SendAsync("/a2a");
|
||||
time.Advance(TimeSpan.FromSeconds(1)); // past the input window
|
||||
await host.SettleAsync();
|
||||
var reply = host.LastReply();
|
||||
Assert.Contains("travel-bot (isolated) key …\n", reply + "\n");
|
||||
Assert.DoesNotContain("1234", reply);
|
||||
Assert.Contains("travel-bot: Completed, 5m ago (task0001)", reply);
|
||||
}
|
||||
|
||||
/// <summary>The bot's A2A registrations on top of the chat side: no construction cycle, and the shared options.</summary>
|
||||
private static ChatHost EnabledHost() => new((h, s) =>
|
||||
{
|
||||
s.AddLogging();
|
||||
s.AddSingleton(_ => h.Chats);
|
||||
s.AddSingleton(_ => h.Settings);
|
||||
TelegramA2ASetup.Register(s, h.Options, new A2AHostOptions { Enabled = true, MaxHops = 5 });
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task A2aEnabled_ServicesResolve()
|
||||
{
|
||||
await using var host = EnabledHost();
|
||||
Assert.NotNull(host.Services.GetRequiredService<A2ARequestRouter>());
|
||||
Assert.NotNull(host.Services.GetRequiredService<A2AServerCache>());
|
||||
Assert.NotNull(host.Services.GetRequiredService<A2ASessionPool>());
|
||||
Assert.IsType<TelegramOwnerNotifier>(host.Services.GetRequiredService<IA2AOwnerNotifier>());
|
||||
Assert.IsType<WorkspaceCallerDirectory>(host.Services.GetRequiredService<IA2ACallerDirectory>());
|
||||
var options = host.Services.GetRequiredService<IOptions<A2AHostOptions>>().Value;
|
||||
Assert.True(options.Enabled);
|
||||
Assert.Equal(5, options.MaxHops);
|
||||
Assert.Equal(host.Options.Model, options.Model);
|
||||
Assert.NotNull(options.TimeZoneFor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A2aSession_GetsBotModelAndOwnersZone()
|
||||
{
|
||||
await using var host = EnabledHost();
|
||||
var tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
|
||||
await host.Zones.SetAsync(Updates.AllowedUser, tokyo, CancellationToken.None);
|
||||
var pool = host.Services.GetRequiredService<A2ASessionPool>();
|
||||
var caller = new A2ACaller("travel-bot", "isolated", host.WorkspaceOf(), $"telegram-{Updates.AllowedUser}", Updates.AllowedUser.ToString());
|
||||
using var lease = await pool.AcquireAsync(caller, "ctx1", "task1", CancellationToken.None);
|
||||
Assert.Equal(host.Options.Model, lease.Session.Model?.Key);
|
||||
Assert.Equal(tokyo.Id, lease.Session.TimeZone.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BindFailure_Classified()
|
||||
{
|
||||
Assert.True(TelegramA2ASetup.IsBindFailure(new IOException("Failed to bind", new Microsoft.AspNetCore.Connections.AddressInUseException("in use"))));
|
||||
Assert.True(TelegramA2ASetup.IsBindFailure(new System.Net.Sockets.SocketException((int)System.Net.Sockets.SocketError.AccessDenied)));
|
||||
Assert.False(TelegramA2ASetup.IsBindFailure(new IOException("disk full")));
|
||||
Assert.False(TelegramA2ASetup.IsBindFailure(new InvalidOperationException()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ public sealed class ChatHost : IAsyncDisposable
|
||||
public FakeBotClient Bot { get; } = new();
|
||||
public FauxProvider Faux { get; } = new();
|
||||
public TelegramOptions Options { get; }
|
||||
|
||||
/// <summary>The services built from <c>configure</c> and the core (sessions, plus whatever the test added).</summary>
|
||||
public IServiceProvider Services => _services;
|
||||
public ChatRegistry Chats { get; }
|
||||
public UpdateRouter Router { get; }
|
||||
private readonly ServiceProvider _services;
|
||||
@@ -116,7 +119,8 @@ public sealed class ChatHost : IAsyncDisposable
|
||||
if (Scheduler is not null) Delivery = new TelegramJobDelivery(Chats, Settings, Options, clock, NullLogger<TelegramJobDelivery>.Instance);
|
||||
var media = new InboundMedia(sender, Options, time ?? TimeProvider.System, NullLogger<InboundMedia>.Instance, speech);
|
||||
Composer = new InboundComposer(sender, media, NullLogger<InboundComposer>.Instance);
|
||||
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers, a2a), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
|
||||
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers,
|
||||
a2a is null ? null : Microsoft.Extensions.Options.Options.Create(a2a), clock), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
|
||||
}
|
||||
|
||||
private readonly TriggerDispatcher? _dispatcher;
|
||||
|
||||
Reference in New Issue
Block a user