using Microsoft.Extensions.Options; using MyAgent.Core; using Xunit; namespace MyAgent.Tests.Core; public sealed class SessionManagerTests { [Fact] public async Task DebounceAsync_TwoRapidMessagesProduceOneStableBatch() { var manager = CreateManager(); var now = DateTimeOffset.Now; var first = manager.DebounceAsync(CreateMessage("1", "第一条", now)); await Task.Delay(10); var second = manager.DebounceAsync(CreateMessage("2", "第二条", now.AddMilliseconds(10))); Assert.Null(await first); var batch = await second; Assert.NotNull(batch); Assert.Equal(2, batch!.Messages.Count); Assert.Contains("第一条", batch.MergedText); Assert.Contains("第二条", batch.MergedText); } [Fact] public async Task DebounceAsync_CompletedBatchIsConsumed() { var manager = CreateManager(); var first = await manager.DebounceAsync(CreateMessage("1", "第一轮", DateTimeOffset.Now)); var second = await manager.DebounceAsync(CreateMessage("2", "第二轮", DateTimeOffset.Now.AddSeconds(1))); Assert.Single(first!.Messages); Assert.Single(second!.Messages); Assert.Equal("第二轮", second.MergedText); } [Fact] public async Task DebounceAsync_CallerCancellationDoesNotLeaveBrokenPendingState() { var manager = CreateManager(); using var cancellation = new CancellationTokenSource(); var cancelled = manager.DebounceAsync(CreateMessage("1", "取消", DateTimeOffset.Now), cancellation.Token); cancellation.Cancel(); await Assert.ThrowsAnyAsync(() => cancelled); var next = await manager.DebounceAsync(CreateMessage("2", "下一条", DateTimeOffset.Now.AddSeconds(1))); Assert.NotNull(next); Assert.Single(next!.Messages); } [Fact] public async Task DebounceAsync_DifferentUsersReplyingToSameMessageStayIsolated() { var manager = CreateManager(); var firstMessage = CreateMessage("1", "用户一", DateTimeOffset.Now, "1", "root"); var secondMessage = CreateMessage("2", "用户二", DateTimeOffset.Now, "2", "root"); var first = manager.DebounceAsync(firstMessage); var second = manager.DebounceAsync(secondMessage); var batches = await Task.WhenAll(first, second); Assert.All(batches, batch => Assert.Single(batch!.Messages)); Assert.NotEqual(batches[0]!.ThreadId, batches[1]!.ThreadId); } private static SessionManager CreateManager() { var options = Options.Create(new ShirobotOptions { Fusion = new FusionOptions { DebounceMs = 40, SameUserMergeWindowSec = 8, MaxMergedMessages = 8 } }); return new SessionManager(options, TimeProvider.System); } private static IncomingMessage CreateMessage(string id, string text, DateTimeOffset at, string userId = "1", string? replyToMessageId = null) => new() { MessageId = id, GroupId = "1", UserId = userId, Text = text, CreatedAt = at, ReplyToMessageId = replyToMessageId }; }