forked from admin/VolcanicAgenticCore
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.
153 lines
7.1 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|