Files
Playa/tests/Volcanic.AgenticCore.Telegram.Tests/TelegramChatChannelTests.cs
EugeneTes 92a0061fdd feat(telegram): ADMIN_USER_ID may /impersonate another user
While on, the admin's updates are routed as the target user's (allow-list,
chat, workspace, session, settings, jobs) and the target chat's output goes
to the admin's chat. The target's own updates are dropped with a notice that
an admin is working with their account. In memory; /impersonate 0 stops.
2026-10-05 07:24:07 +00:00

153 lines
7.1 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Requests;
using Telegram.Bot.Types.Enums;
using Volcanic.AgenticCore.Abstractions.Ui;
using Volcanic.AgenticCore.Telegram.Chats;
using Volcanic.AgenticCore.Telegram.Rendering;
using Volcanic.AgenticCore.Telegram.Tests.Support;
namespace Volcanic.AgenticCore.Telegram.Tests;
public sealed class TelegramChatChannelTests
{
private static async Task<string> WorkspaceFile(ChatHost host, string relative, byte[]? bytes = null)
{
var chat = await host.ChatAsync();
var path = Path.Combine(chat.Session.Cwd, relative);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllBytesAsync(path, bytes ?? [1, 2, 3]);
return path;
}
[Theory]
[InlineData(5L, "5 bytes")]
[InlineData(1536L, "1.5 KB")]
[InlineData(1_258_291L, "1.2 MB")]
public void FormatSize_uses_bytes_kilobytes_or_megabytes(long bytes, string expected) =>
Assert.Equal(expected, TelegramLimits.FormatSize(bytes));
[Fact]
public void Megabytes_has_one_decimal() => Assert.Equal("21.0", TelegramLimits.Megabytes(21L * 1024 * 1024));
[Fact]
public async Task Limits_are_the_bot_api_limits()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
Assert.Equal(new ChatChannelLimits(10L * 1024 * 1024, 50L * 1024 * 1024, 1024), chat.Channel.Limits);
Assert.Same(chat.Channel, chat.Session.Channel);
}
[Fact]
public async Task SendTextAsync_while_idle_delivers_html_split_past_4096()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
var markdown = "**Reminder**\n\n" + string.Join("\n\n", Enumerable.Repeat(new string('a', 1000), 6));
var delivery = await chat.Channel.SendTextAsync(markdown, CancellationToken.None);
Assert.False(chat.Session.IsStreaming);
Assert.True(host.Bot.Sent.Count >= 2);
Assert.All(host.Bot.Sent, s => Assert.Equal(ParseMode.Html, s.ParseMode));
Assert.Contains("<b>Reminder</b>", host.Bot.Sent[0].Text, StringComparison.Ordinal);
Assert.Null(delivery.SentAs);
Assert.Equal(host.Bot.CurrentTexts.Keys.Min().ToString(System.Globalization.CultureInfo.InvariantCulture), delivery.MessageId);
Assert.Empty(chat.Session.Messages); // never persisted
Assert.Empty(host.Faux.Requests);
}
[Fact]
public async Task Auto_png_goes_as_a_photo()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
var path = await WorkspaceFile(host, "chart.png");
var delivery = await chat.Channel.SendFileAsync(new ChatFile(path, ChatFileKind.Auto, "Sales"), CancellationToken.None);
Assert.Equal(ChatFileKind.Photo, delivery.SentAs);
var upload = Assert.Single(host.Bot.Uploads);
Assert.Equal(("sendPhoto", "Sales"), (upload.Method, upload.Caption));
Assert.Equal(upload.MessageId.ToString(System.Globalization.CultureInfo.InvariantCulture), delivery.MessageId);
}
[Fact]
public async Task Auto_non_photo_goes_as_a_document()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
var path = await WorkspaceFile(host, "anim.gif");
var delivery = await chat.Channel.SendFileAsync(new ChatFile(path), CancellationToken.None);
Assert.Equal(ChatFileKind.Document, delivery.SentAs);
Assert.Equal("sendDocument", Assert.Single(host.Bot.Uploads).Method);
}
[Fact]
public async Task Auto_photo_rejected_with_a_400_falls_back_to_a_document()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
var path = await WorkspaceFile(host, "wide.png");
host.Bot.OnSend = r => { if (r is SendPhotoRequest) throw new ApiRequestException("Bad Request: PHOTO_INVALID_DIMENSIONS", 400); };
var delivery = await chat.Channel.SendFileAsync(new ChatFile(path), CancellationToken.None);
Assert.Equal(ChatFileKind.Document, delivery.SentAs);
Assert.Equal("sendDocument", Assert.Single(host.Bot.Uploads).Method);
}
[Fact]
public async Task Explicit_photo_rejected_is_not_resent()
{
await using var host = new ChatHost();
var chat = await host.ChatAsync();
var path = await WorkspaceFile(host, "wide.png");
host.Bot.OnSend = r => { if (r is SendPhotoRequest) throw new ApiRequestException("Bad Request: PHOTO_INVALID_DIMENSIONS", 400); };
var ex = await Assert.ThrowsAsync<ChatDeliveryException>(() => chat.Channel.SendFileAsync(new ChatFile(path, ChatFileKind.Photo), CancellationToken.None));
Assert.Equal("Telegram refused the file: Bad Request: PHOTO_INVALID_DIMENSIONS", ex.Message);
Assert.Empty(host.Bot.Uploads);
}
private static (FakeBotClient Bot, TelegramChatChannel Channel, ChatRenderer Renderer) Direct()
{
var bot = new FakeBotClient();
var sender = new TelegramSender(bot, NullLogger<TelegramSender>.Instance) { FileRetryDelays = [TimeSpan.Zero, TimeSpan.Zero] };
var options = new TelegramOptions { EditInterval = TimeSpan.FromMilliseconds(20), TypingInterval = TimeSpan.FromSeconds(10) };
var renderer = new ChatRenderer(1001, sender, options, NullLogger.Instance) { ReplyRetryDelays = [TimeSpan.Zero, TimeSpan.Zero] };
return (bot, new TelegramChatChannel(sender, renderer), renderer);
}
[Fact]
public async Task SendTextAsync_throws_when_a_chunk_still_fails_after_its_retries_but_sends_the_others()
{
var (bot, channel, renderer) = Direct();
await using var _ = renderer;
var markdown = new string('a', 3000) + "\n\n" + new string('b', 3000); // two chunks
bot.OnSend = r => { if (r is SendMessageRequest m && m.Text.StartsWith('a')) throw FakeBotClient.NetworkDown(); };
var ex = await Assert.ThrowsAsync<ChatDeliveryException>(() => channel.SendTextAsync(markdown, CancellationToken.None));
Assert.Equal("Telegram did not accept the message.", ex.Message);
Assert.Equal(3, bot.Sent.Count(m => m.Text.StartsWith('a'))); // the first try and two retries
Assert.Single(bot.CurrentTexts.Values, t => t.StartsWith('b'));
}
[Fact]
public async Task SendFileAsync_throws_after_three_attempts_when_the_network_stays_down()
{
var (bot, channel, renderer) = Direct();
await using var _ = renderer;
var path = Path.Combine(Path.GetTempPath(), $"volcanic-channel-{Guid.NewGuid():N}.pdf");
await File.WriteAllBytesAsync(path, [1, 2, 3]);
try
{
bot.OnSend = r => { if (r is SendDocumentRequest) throw FakeBotClient.NetworkDown(); };
var ex = await Assert.ThrowsAsync<ChatDeliveryException>(() => channel.SendFileAsync(new ChatFile(path), CancellationToken.None));
Assert.StartsWith("Telegram refused the file: ", ex.Message, StringComparison.Ordinal);
Assert.Equal(3, bot.Requests.OfType<SendDocumentRequest>().Count());
}
finally
{
File.Delete(path);
}
}
}