forked from admin/VolcanicAgenticCore
Content step 2 (InboundMedia.AddFilesAsync, InboxFiles): files land in <cwd>/inbox/<date>-<safe name>[-N], image documents within Telegram:InlineImageMaxBytes are attached inline, oversize files and failed downloads get a notice and no run. Documents, videos, audio and voice are accepted; the unsupported reply is now 'I can read text, photos, files and voice messages.' Spec: Updates helpers are named DocumentMessage/VideoMessage/VoiceMessage/AudioMessage; the getFile 'file is too big' notice reads 'over 20 MB'; the too-large inline note is a parenthesised suffix after the size; size formats spelled out.
170 lines
7.5 KiB
C#
170 lines
7.5 KiB
C#
using Microsoft.Extensions.Time.Testing;
|
|
using Telegram.Bot.Exceptions;
|
|
using Telegram.Bot.Requests;
|
|
using Volcanic.AgenticCore.Abstractions.Messages;
|
|
using Volcanic.AgenticCore.Telegram.Tests.Support;
|
|
using Volcanic.AgenticCore.Testing;
|
|
|
|
namespace Volcanic.AgenticCore.Telegram.Tests;
|
|
|
|
/// <summary>Documents and videos end to end: saved to the chat's inbox and prompted, or refused with a notice.</summary>
|
|
public sealed class AttachmentChatTests
|
|
{
|
|
private static readonly DateTimeOffset Today = new(2026, 9, 26, 10, 0, 0, TimeSpan.Zero);
|
|
private const long MB = 1024 * 1024;
|
|
|
|
private static UserMessage Prompt(ChatHost host) => (UserMessage)host.Faux.Requests.Single().Messages.Last();
|
|
private static string Inbox(ChatHost host) => Path.Combine(host.WorkspaceOf(), "inbox");
|
|
private static string[] InboxFiles(ChatHost host) => Directory.Exists(Inbox(host)) ? Directory.GetFiles(Inbox(host)) : [];
|
|
|
|
private static async Task RouteAndFlushAsync(ChatHost host, FakeTimeProvider time, params global::Telegram.Bot.Types.Update[] updates)
|
|
{
|
|
foreach (var update in updates) await host.RouteAsync(update);
|
|
await MessageContextChatTests.FlushAsync(host, time, updates.Length);
|
|
await host.SettleAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_document_with_a_caption_is_saved_to_the_inbox_and_prompted()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Bot.Files["doc"] = "hello world"u8.ToArray();
|
|
host.Faux.Enqueue(FauxTurn.Say("Summary."));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("report.pdf", "application/pdf", 11, caption: "Please summarise"));
|
|
|
|
Assert.Equal("hello world"u8.ToArray(), await File.ReadAllBytesAsync(Path.Combine(Inbox(host), "2026-09-26-report.pdf")));
|
|
Assert.EndsWith("\nPlease summarise\n\n[File received: inbox/2026-09-26-report.pdf (application/pdf, 11 bytes)]", Prompt(host).Text, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_second_upload_of_the_same_name_gets_a_suffix()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Bot.Files["doc"] = [1];
|
|
host.Faux.Enqueue(FauxTurn.Say("One."), FauxTurn.Say("Two."));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("report.pdf", "application/pdf", 1));
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("report.pdf", "application/pdf", 1));
|
|
|
|
Assert.Contains("[File received: inbox/2026-09-26-report-1.pdf (application/pdf, 1 bytes)]", host.Faux.Requests[1].Messages.Last().Text, StringComparison.Ordinal);
|
|
Assert.Equal(2, InboxFiles(host).Length);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_hostile_file_name_stays_in_the_inbox()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Bot.Files["doc"] = [1];
|
|
host.Faux.Enqueue(FauxTurn.Say("ok"));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("../../evil.sh", "text/x-sh", 1));
|
|
|
|
Assert.Equal(Path.Combine(Inbox(host), "2026-09-26-evil.sh"), Assert.Single(InboxFiles(host)));
|
|
Assert.False(File.Exists(Path.Combine(host.WorkspaceOf(), "..", "evil.sh")));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_file_over_20_MB_gets_the_size_notice_and_no_run()
|
|
{
|
|
await using var host = new ChatHost();
|
|
await host.RouteAsync(Updates.DocumentMessage("big.zip", "application/zip", 21 * MB, caption: "look"));
|
|
await host.SettleAsync();
|
|
|
|
Assert.Contains("That file is 21.0 MB; Telegram lets bots download at most 20 MB. Share a link or a smaller file.", host.Texts);
|
|
Assert.Empty(host.Bot.Requests.OfType<GetFileRequest>());
|
|
Assert.Empty(InboxFiles(host));
|
|
Assert.Empty(host.Faux.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_getFile_too_big_error_gets_the_size_notice()
|
|
{
|
|
await using var host = new ChatHost();
|
|
host.Bot.OnSend = r => { if (r is GetFileRequest) throw new ApiRequestException("Bad Request: file is too big", 400); };
|
|
await host.RouteAsync(Updates.DocumentMessage("big.zip", "application/zip", size: null));
|
|
await host.SettleAsync();
|
|
|
|
Assert.Contains("That file is over 20 MB; Telegram lets bots download at most 20 MB. Share a link or a smaller file.", host.Texts);
|
|
Assert.Empty(InboxFiles(host));
|
|
Assert.Empty(host.Faux.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_failed_download_deletes_the_partial_file_and_gets_a_notice()
|
|
{
|
|
await using var host = new ChatHost();
|
|
host.Bot.Files["doc"] = [1, 2, 3];
|
|
host.Bot.FailDownloads = true;
|
|
await host.RouteAsync(Updates.DocumentMessage("report.pdf", "application/pdf", 3, caption: "read this"));
|
|
await host.SettleAsync();
|
|
|
|
Assert.Contains("I could not download that file.", host.Texts);
|
|
Assert.Empty(InboxFiles(host));
|
|
Assert.Empty(host.Faux.Requests);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_image_document_within_the_limit_is_attached_inline()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Bot.Files["doc"] = [9, 8, 7];
|
|
host.Faux.Enqueue(FauxTurn.Say("A diagram."));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("diagram.png", "image/png", 3));
|
|
|
|
var image = Assert.Single(Prompt(host).Content.OfType<ImageContent>());
|
|
Assert.Equal(("image/png", Convert.ToBase64String([9, 8, 7])), (image.MimeType, image.Data));
|
|
Assert.EndsWith("[File received: inbox/2026-09-26-diagram.png (image/png, 3 bytes)]", Prompt(host).Text, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_image_document_over_the_limit_is_path_only()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Options.InlineImageMaxBytes = 2;
|
|
host.Bot.Files["doc"] = [9, 8, 7];
|
|
host.Faux.Enqueue(FauxTurn.Say("ok"));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.DocumentMessage("diagram.png", "image/png", 3));
|
|
|
|
Assert.Empty(Prompt(host).Content.OfType<ImageContent>());
|
|
Assert.EndsWith("[File received: inbox/2026-09-26-diagram.png (image/png, 3 bytes) (too large to show inline; downscale it with bash if you need to see it)]",
|
|
Prompt(host).Text, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_document_captioned_new_is_not_a_command()
|
|
{
|
|
await using var host = new ChatHost();
|
|
host.Bot.Files["doc"] = [1];
|
|
host.Faux.Enqueue(FauxTurn.Say("ok"));
|
|
var chat = await host.ChatAsync();
|
|
var sessionId = chat.Session.SessionId;
|
|
|
|
await host.RouteAsync(Updates.DocumentMessage("notes.txt", "text/plain", 1, caption: "/new"));
|
|
await host.SettleAsync();
|
|
|
|
Assert.Equal(sessionId, chat.Session.SessionId);
|
|
Assert.Contains("/new\n\n[File received: inbox/", host.Faux.Requests.Single().Messages.Last().Text, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_video_without_a_name_is_saved_as_video_mp4()
|
|
{
|
|
var time = new FakeTimeProvider(Today);
|
|
await using var host = new ChatHost(time: time);
|
|
host.Bot.Files["video"] = [1];
|
|
host.Faux.Enqueue(FauxTurn.Say("ok"));
|
|
|
|
await RouteAndFlushAsync(host, time, Updates.VideoMessage(null, 1));
|
|
|
|
Assert.EndsWith("[File received: inbox/2026-09-26-video.mp4 (video/mp4, 1 bytes)]", Prompt(host).Text, StringComparison.Ordinal);
|
|
}
|
|
}
|