forked from admin/VolcanicAgenticCore
157 lines
7.6 KiB
C#
157 lines
7.6 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Telegram.Bot.Types;
|
|
using Telegram.Bot.Types.Enums;
|
|
using Volcanic.AgenticCore.Telegram.Groups;
|
|
using Volcanic.AgenticCore.Telegram.Triggers;
|
|
|
|
namespace Volcanic.AgenticCore.Telegram.Tests;
|
|
|
|
/// <summary>The group registry, the trigger store and the snooze queue: persistence and bookkeeping.</summary>
|
|
public sealed class TriggerStoreTests : IDisposable
|
|
{
|
|
private const long Owner = 1001;
|
|
private const long OtherOwner = 2002;
|
|
private const long Group = -1001234567890;
|
|
private readonly string _root = Path.Combine(Path.GetTempPath(), "volcanic-triggers", Guid.NewGuid().ToString("N"));
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_root, true); } catch (IOException) { }
|
|
}
|
|
|
|
private TriggerStore Store(int max = 20) => new(Path.Combine(_root, "workspaces"), max, NullLogger<TriggerStore>.Instance);
|
|
|
|
private static NewTrigger Deploys(long group = Group) => new("deploys", group, "Dev Team", "Is this about deploys?", "Mentions deploys", "Small talk", 0.7);
|
|
|
|
internal static GroupMessage Message(string text, int id = 1, string sender = "Alice (@alice)", long group = Group) =>
|
|
new(group, "Dev Team", null, true, id, new DateTimeOffset(2026, 10, 5, 12, 2, 0, TimeSpan.Zero), sender, text);
|
|
|
|
[Fact]
|
|
public async Task Triggers_get_sequential_ids_per_owner_and_survive_a_reload()
|
|
{
|
|
var store = Store();
|
|
Assert.Equal("t1", (await store.AddAsync(Owner, Deploys(), CancellationToken.None)).Id);
|
|
Assert.Equal("t2", (await store.AddAsync(Owner, Deploys(), CancellationToken.None)).Id);
|
|
Assert.Equal("t1", (await store.AddAsync(OtherOwner, Deploys(), CancellationToken.None)).Id);
|
|
await store.RemoveAsync(Owner, "t1", CancellationToken.None);
|
|
Assert.Equal("t3", (await store.AddAsync(Owner, Deploys(), CancellationToken.None)).Id);
|
|
|
|
var reloaded = Store();
|
|
Assert.Equal(["t2", "t3"], reloaded.List(Owner).Select(t => t.Id));
|
|
Assert.Equal(3, reloaded.Watching(Group).Count);
|
|
Assert.True(File.Exists(TriggerStore.PathIn(reloaded.WorkspaceOf(Owner))));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_per_chat_limit_is_enforced()
|
|
{
|
|
var store = Store(max: 1);
|
|
await store.AddAsync(Owner, Deploys(), CancellationToken.None);
|
|
await Assert.ThrowsAsync<TriggerLimitException>(() => store.AddAsync(Owner, Deploys(), CancellationToken.None));
|
|
await store.AddAsync(OtherOwner, Deploys(), CancellationToken.None); // per chat, not global
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Updates_keep_id_and_creation_time_and_snoozes_are_tracked()
|
|
{
|
|
var store = Store();
|
|
var t = await store.AddAsync(Owner, Deploys(), CancellationToken.None);
|
|
var until = DateTimeOffset.UtcNow.AddHours(1);
|
|
|
|
var updated = await store.UpdateAsync(Owner, "T1", x => x with { Id = "hijack", Threshold = 0.9, SnoozedUntil = until }, CancellationToken.None);
|
|
|
|
Assert.Equal(("t1", t.CreatedAt, 0.9), (updated!.Id, updated.CreatedAt, updated.Threshold));
|
|
Assert.Equal(until, store.NextSnoozeEnd());
|
|
Assert.Empty(store.SnoozeDue(until.AddSeconds(-1)));
|
|
Assert.Single(store.SnoozeDue(until));
|
|
Assert.Null(await store.UpdateAsync(Owner, "t9", x => x, CancellationToken.None));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Moving_a_group_moves_every_owners_triggers()
|
|
{
|
|
var store = Store();
|
|
await store.AddAsync(Owner, Deploys(-5), CancellationToken.None);
|
|
await store.AddAsync(OtherOwner, Deploys(-5), CancellationToken.None);
|
|
|
|
await store.MoveGroupAsync(-5, Group, CancellationToken.None);
|
|
|
|
Assert.Empty(store.Watching(-5));
|
|
Assert.Equal(2, Store().Watching(Group).Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_corrupt_file_is_set_aside()
|
|
{
|
|
var store = Store();
|
|
var path = TriggerStore.PathIn(store.WorkspaceOf(Owner));
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
|
await File.WriteAllTextAsync(path, "{ not json");
|
|
|
|
Assert.Empty(Store().List(Owner));
|
|
Assert.Single(Directory.GetFiles(Path.GetDirectoryName(path)!, "triggers.json.corrupt-*"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_snooze_queue_caps_drops_the_oldest_and_takes_all_at_once()
|
|
{
|
|
var queue = new SnoozeQueue(maxPerTrigger: 2);
|
|
var ws = Path.Combine(_root, "ws");
|
|
Assert.Equal((1, false), await queue.AppendAsync(ws, "t1", Message("one", 1), CancellationToken.None));
|
|
Assert.Equal((2, false), await queue.AppendAsync(ws, "t1", Message("two", 2), CancellationToken.None));
|
|
Assert.Equal((2, true), await queue.AppendAsync(ws, "t1", Message("three", 3), CancellationToken.None));
|
|
Assert.Equal(2, queue.Count(ws, "t1"));
|
|
|
|
var taken = await queue.TakeAllAsync(ws, "t1", CancellationToken.None);
|
|
|
|
Assert.Equal(["two", "three"], taken.Select(m => m.Text));
|
|
Assert.Equal(Message("two", 2), taken[0]);
|
|
Assert.Equal(0, queue.Count(ws, "t1"));
|
|
Assert.Empty(await queue.TakeAllAsync(ws, "t1", CancellationToken.None));
|
|
await queue.DeleteAsync(Path.Combine(_root, "missing"), "t1", CancellationToken.None);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Groups_are_vouched_refreshed_paused_migrated_and_persisted()
|
|
{
|
|
var path = GroupRegistry.DefaultPath(Path.Combine(_root, "agent"));
|
|
var registry = new GroupRegistry(path, NullLogger<GroupRegistry>.Instance);
|
|
var chat = new Chat { Id = -5, Type = ChatType.Group, Title = "Dev" };
|
|
|
|
Assert.True(await registry.VouchAsync(chat, Owner));
|
|
Assert.False(await registry.VouchAsync(chat, Owner));
|
|
Assert.True(await registry.RefreshAsync(new Chat { Id = -5, Type = ChatType.Group, Title = "Dev Team" }));
|
|
Assert.False(await registry.RefreshAsync(new Chat { Id = -9, Type = ChatType.Group, Title = "Unknown" }));
|
|
Assert.True(await registry.SetInactiveAsync(-5));
|
|
Assert.False(registry.Find(-5)!.Active);
|
|
Assert.True(await registry.VouchAsync(new Chat { Id = -5, Type = ChatType.Group, Title = "Dev Team" }, Owner));
|
|
Assert.True(registry.Find(-5)!.Active);
|
|
Assert.True(await registry.MigrateAsync(-5, Group));
|
|
|
|
var reloaded = new GroupRegistry(path, NullLogger<GroupRegistry>.Instance);
|
|
var group = Assert.Single(reloaded.All());
|
|
Assert.Equal((Group, "Dev Team", true, Owner, true), (group.Id, group.Title, group.IsSupergroup, group.AddedBy, group.Active));
|
|
}
|
|
|
|
[Fact]
|
|
public void Message_links_and_senders()
|
|
{
|
|
Assert.Equal("https://t.me/c/1234567890/7", (Message("x") with { MessageId = 7 }).Link);
|
|
Assert.Equal("https://t.me/devteam/7", (Message("x") with { MessageId = 7, GroupUsername = "devteam" }).Link);
|
|
Assert.Null((Message("x") with { IsSupergroup = false, GroupId = -5 }).Link);
|
|
|
|
var message = new Message
|
|
{
|
|
Id = 3,
|
|
Chat = new Chat { Id = Group, Type = ChatType.Supergroup, Title = "Dev Team" },
|
|
From = new User { Id = 7, FirstName = "Alice", LastName = "Smith", Username = "alice" },
|
|
Caption = "deploy log attached",
|
|
Date = new DateTime(2026, 10, 5, 12, 0, 0, DateTimeKind.Utc),
|
|
ReplyToMessage = new Message { Id = 2, Chat = new Chat { Id = Group }, From = new User { Id = 8, FirstName = "Bob" }, Text = "status?" },
|
|
};
|
|
var built = GroupMessage.From(message);
|
|
Assert.Equal(("Alice Smith (@alice)", "deploy log attached", "Bob", "status?"), (built.Sender, built.Text, built.ReplyTo!.Sender, built.ReplyTo.Text));
|
|
Assert.Equal("Dev Team", GroupMessage.SenderOf(new Message { Chat = new Chat { Id = Group }, SenderChat = new Chat { Id = Group, Title = "Dev Team" } }));
|
|
}
|
|
}
|