using BuildFlow.Application.Document.Queries; using BuildFlow.Application.Document.Handlers; using BuildFlow.Application.Embedding.Commands; using BuildFlow.Application.Embedding.Handlers; using BuildFlow.Application.Chat.Commands; using BuildFlow.Application.Chat.Handlers; using BuildFlow.Application.Chat.Queries; using BuildFlow.Domain.Entities.ChatMessage; using BuildFlow.Domain.Entities.Conversation; using BuildFlow.Domain.Entities.Document; using BuildFlow.Domain.Enums; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Infrastructure; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Xunit; namespace BuildFlow.Rag.Tests; /// /// [SOP-RAG] Embedding Command + DocumentEntity/Chat Query Handler 补测。 /// 覆盖:EmbedText/EmbedBatch / GetDocuments / GetConversationDetail / GetConversations / DeleteConversation。 /// public class EmbeddingAndQueryTests { private static RagDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "") { var userContext = new TestCurrentUserContext(); var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"rag-misc-{name}-{Guid.NewGuid():N}") .AddInterceptors(new AuditInterceptor(userContext)) .Options; return new RagDbContext(options); } // ===== Embedding ===== [Fact] public async Task EmbedText_ReturnsVectorAndDimensions() { var embedding = new StubEmbeddingService(); var handler = new EmbedTextCommandHandler(embedding); var result = await handler.Handle(new EmbedTextCommand("测试文本"), CancellationToken.None); result.Vector.Should().HaveCount(StubEmbeddingService.Dimensions); result.Dimensions.Should().Be(StubEmbeddingService.Dimensions); } [Fact] public async Task EmbedBatch_ReturnsVectorsInOrder() { var embedding = new StubEmbeddingService(); var handler = new EmbedBatchCommandHandler(embedding); var result = await handler.Handle( new EmbedBatchCommand(["文本A", "文本B", "文本C"]), CancellationToken.None); result.Vectors.Should().HaveCount(3); result.Vectors[0].Should().HaveCount(StubEmbeddingService.Dimensions); result.Dimensions.Should().Be(StubEmbeddingService.Dimensions); } // ===== DocumentEntity Queries ===== [Fact] public async Task GetDocuments_ByKnowledgeBase_ReturnsOnlyMatching() { var db = CreateDb(); var kbId = Guid.NewGuid(); var otherKbId = Guid.NewGuid(); db.Documents.Add(new DocumentEntity { KnowledgeBaseId = kbId, Title = "doc1", FileName = "a.md", FilePath = "a", ContentType = "text", Status = DocumentStatusEnum.Completed }); db.Documents.Add(new DocumentEntity { KnowledgeBaseId = kbId, Title = "doc2", FileName = "b.md", FilePath = "b", ContentType = "text", Status = DocumentStatusEnum.Pending }); db.Documents.Add(new DocumentEntity { KnowledgeBaseId = otherKbId, Title = "other", FileName = "c.md", FilePath = "c", ContentType = "text", Status = DocumentStatusEnum.Completed }); await db.SaveChangesAsync(); var handler = new GetDocumentsQueryHandler(db); var result = await handler.Handle(new GetDocumentsQuery(kbId), CancellationToken.None); result.Should().HaveCount(2); result.Should().OnlyContain(d => d.KnowledgeBaseId == kbId); } // ===== Chat Queries ===== [Fact] public async Task GetConversations_ReturnsCurrentUserOnly() { var db = CreateDb(); var userContext = new TestCurrentUserContext(); var myId = Guid.Parse(userContext.UserId); db.Conversations.Add(new ConversationEntity { Title = "我的", UserId = myId }); db.Conversations.Add(new ConversationEntity { Title = "他人的", UserId = Guid.NewGuid() }); await db.SaveChangesAsync(); var handler = new GetConversationsQueryHandler(db, userContext); var result = await handler.Handle(new GetConversationsQuery(), CancellationToken.None); result.Should().HaveCount(1); result[0].Title.Should().Be("我的"); } [Fact] public async Task GetConversationDetail_Existing_ReturnsWithMessages() { var db = CreateDb(); var convId = Guid.NewGuid(); var userContext = new TestCurrentUserContext(); var userId = Guid.Parse(userContext.UserId); db.Conversations.Add(new ConversationEntity { Id = convId, Title = "详情", UserId = userId }); db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.User, Content = "你好" }); db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.Assistant, Content = "回复" }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); var handler = new GetConversationDetailQueryHandler(db, cache, userContext); var result = await handler.Handle(new GetConversationDetailQuery(convId), CancellationToken.None); result.Title.Should().Be("详情"); result.Messages.Should().HaveCount(2); } [Fact] public async Task DeleteConversation_RemovesFromDbAndCache() { var db = CreateDb(); var convId = Guid.NewGuid(); var userContext = new TestCurrentUserContext(); var userId = Guid.Parse(userContext.UserId); db.Conversations.Add(new ConversationEntity { Id = convId, Title = "待删", UserId = userId }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); await cache.SetMessagesAsync(convId, [new CachedChatMessage(Guid.NewGuid(), "user", "hi", null, DateTime.UtcNow)]); var handler = new DeleteConversationCommandHandler(db, cache, userContext); await handler.Handle(new DeleteConversationCommand(convId), CancellationToken.None); (await db.Conversations.AnyAsync(c => c.Id == convId)).Should().BeFalse(); cache.Count(convId).Should().Be(0, "cache 也应清除"); } [Fact] public async Task GetConversationDetail_OtherUsersConversation_ThrowsNotFound() { await using var db = CreateDb(); var userContext = new TestCurrentUserContext(); var conversationId = Guid.NewGuid(); db.Conversations.Add(new ConversationEntity { Id = conversationId, Title = "他人的详情", UserId = Guid.NewGuid(), }); await db.SaveChangesAsync(); var handler = new GetConversationDetailQueryHandler( db, new StubChatMessageCache(), userContext); var request = new GetConversationDetailQuery(conversationId); var act = () => handler.Handle(request, CancellationToken.None); await act.Should().ThrowAsync() .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); } [Fact] public async Task UpdateConversation_OtherUsersConversation_DoesNotChangeTitle() { await using var db = CreateDb(); var userContext = new TestCurrentUserContext(); var conversation = new ConversationEntity { Id = Guid.NewGuid(), Title = "原始标题", UserId = Guid.NewGuid(), }; db.Conversations.Add(conversation); await db.SaveChangesAsync(); var handler = new UpdateConversationCommandHandler(db, userContext); var request = new UpdateConversationCommand(conversation.Id, "越权更新", null); var act = () => handler.Handle(request, CancellationToken.None); await act.Should().ThrowAsync() .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); conversation.Title.Should().Be("原始标题"); } [Fact] public async Task DeleteConversation_OtherUsersConversation_LeavesDbAndCacheUntouched() { await using var db = CreateDb(); var userContext = new TestCurrentUserContext(); var conversationId = Guid.NewGuid(); db.Conversations.Add(new ConversationEntity { Id = conversationId, Title = "不可删除", UserId = Guid.NewGuid(), }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); await cache.SetMessagesAsync(conversationId, [new CachedChatMessage(Guid.NewGuid(), "user", "保留", null, DateTime.UtcNow)]); var handler = new DeleteConversationCommandHandler(db, cache, userContext); var request = new DeleteConversationCommand(conversationId); var act = () => handler.Handle(request, CancellationToken.None); await act.Should().ThrowAsync() .Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在"); (await db.Conversations.AnyAsync(x => x.Id == conversationId)).Should().BeTrue(); cache.Count(conversationId).Should().Be(1); } }