using BuildFlow.Application.Chat.Commands; using BuildFlow.Application.Chat.Handlers; using BuildFlow.Domain.Entities; using BuildFlow.Domain.Enums; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; using BuildFlow.Shared.Domain.Common; using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Infrastructure; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace BuildFlow.Rag.Tests; /// /// [SOP-RAG-02] 多轮对话端到端测试。 /// 不依赖 pgvector。验证:CreateConversation → SendMessage(两轮)→ 历史拼装 → cache 一致性。 /// 飞书文档: https://ecn1rid3e27z.feishu.cn/wiki/CBrcwbDH6ialzFkF1cgcnvKenHM /// public class ChatEndToEndTests { private static RagDbContext CreateDb(string name) { // 挂生产 AuditInterceptor,自动填充 CreatedBy/UpdatedBy/OperatorIP 等审计字段 // (InMemory 默认无拦截器,导致必填审计字段为空抛 DbUpdateException) var userContext = new TestCurrentUserContext(); var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"rag-chat-{name}-{Guid.NewGuid():N}") .AddInterceptors(new AuditInterceptor(userContext)) .Options; return new RagDbContext(options); } private static void SeedConversation(RagDbContext db, Guid conversationId, Guid userId) { db.Conversations.Add(new Conversation { Id = conversationId, Title = "测试会话", UserId = userId }); db.SaveChanges(); } [Fact] public async Task CreateConversation_ReturnsConversationDto() { // Given await using var db = CreateDb("create"); var userContext = new TestCurrentUserContext(); var handler = new CreateConversationCommandHandler(db, userContext); // When var result = await handler.Handle( new CreateConversationCommand("我的对话", null), CancellationToken.None); // Then result.Title.Should().Be("我的对话"); result.UserId.Should().Be(Guid.Parse(userContext.UserId)); result.Id.Should().NotBeEmpty(); db.Conversations.Should().ContainSingle(); } [Fact] public async Task SendMessage_TwoRounds_HistoryAssembledAndCacheConsistent() { // Given: 已创建会话 + 两轮对话 await using var db = CreateDb("multi-turn"); var userId = Guid.Parse(new TestCurrentUserContext().UserId); var conversationId = Guid.NewGuid(); SeedConversation(db, conversationId, userId); var cache = new StubChatMessageCache(); var chatAgent = new StubAIChatAgent { ReplyText = "你好!我是 AI 助手。" }; var handler = new SendMessageCommandHandler(db, chatAgent, cache); // When: 第一轮 var round1 = await handler.Handle( new SendMessageCommand(conversationId, "你好"), CancellationToken.None); // Then: 第一轮返回 messageId + 内容 round1.MessageId.Should().NotBeEmpty(); round1.Content.Should().Be("你好!我是 AI 助手。"); // DB: 1 user + 1 assistant = 2 条消息 db.ChatMessages.Should().HaveCount(2); db.ChatMessages.Count(m => m.Role == ChatRole.User).Should().Be(1); db.ChatMessages.Count(m => m.Role == ChatRole.Assistant).Should().Be(1); // Cache: 同步 2 条 cache.Count(conversationId).Should().Be(2, "cache 与 DB 一致"); // When: 第二轮(验证历史拼装 - Stub chatAgent 不依赖历史但 Handler 应查历史) var round2 = await handler.Handle( new SendMessageCommand(conversationId, "我刚才说了什么?"), CancellationToken.None); // Then: 第二轮同样返回 round2.MessageId.Should().NotBeEmpty(); round2.Content.Should().NotBeNullOrEmpty(); // DB: 累计 4 条(2 user + 2 assistant) db.ChatMessages.Should().HaveCount(4); cache.Count(conversationId).Should().Be(4, "两轮后 cache 4 条"); } [Fact] public async Task SendMessage_ConversationNotFound_ThrowsNotFoundException() { // Given: 会话不存在 await using var db = CreateDb("not-found"); var cache = new StubChatMessageCache(); var chatAgent = new StubAIChatAgent(); var handler = new SendMessageCommandHandler(db, chatAgent, cache); // When/Then: 发消息到不存在的会话 var act = async () => await handler.Handle( new SendMessageCommand(Guid.NewGuid(), "内容"), CancellationToken.None); await act.Should().ThrowAsync(); } [Fact] public async Task CreateConversation_UnauthorizedUser_Throws() { // Given: UserId = "system"(未授权) await using var db = CreateDb("unauth"); var userContext = new TestCurrentUserContext { UserId = "system" }; var handler = new CreateConversationCommandHandler(db, userContext); // When/Then var act = async () => await handler.Handle( new CreateConversationCommand("x", null), CancellationToken.None); await act.Should().ThrowAsync("未授权用户不能创建会话"); } }