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; using BuildFlow.Domain.Enums; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; using BuildFlow.Shared.Domain.Exceptions; using BuildFlow.Shared.Infrastructure; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Xunit; namespace BuildFlow.Rag.Tests; /// /// [SOP-RAG] Embedding Command + Document/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); } // ===== Document Queries ===== [Fact] public async Task GetDocuments_ByKnowledgeBase_ReturnsOnlyMatching() { var db = CreateDb(); var kbId = Guid.NewGuid(); var otherKbId = Guid.NewGuid(); db.Documents.Add(new Document { KnowledgeBaseId = kbId, Title = "doc1", FileName = "a.md", FilePath = "a", ContentType = "text", Status = DocumentStatus.Completed }); db.Documents.Add(new Document { KnowledgeBaseId = kbId, Title = "doc2", FileName = "b.md", FilePath = "b", ContentType = "text", Status = DocumentStatus.Pending }); db.Documents.Add(new Document { KnowledgeBaseId = otherKbId, Title = "other", FileName = "c.md", FilePath = "c", ContentType = "text", Status = DocumentStatus.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 Conversation { Title = "我的", UserId = myId }); db.Conversations.Add(new Conversation { 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 userId = Guid.Parse(new TestCurrentUserContext().UserId); db.Conversations.Add(new Conversation { Id = convId, Title = "详情", UserId = userId }); db.ChatMessages.Add(new ChatMessage { ConversationId = convId, Role = ChatRole.User, Content = "你好" }); db.ChatMessages.Add(new ChatMessage { ConversationId = convId, Role = ChatRole.Assistant, Content = "回复" }); await db.SaveChangesAsync(); var cache = new StubChatMessageCache(); var handler = new GetConversationDetailQueryHandler(db, cache); 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 userId = Guid.Parse(new TestCurrentUserContext().UserId); db.Conversations.Add(new Conversation { 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); 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 也应清除"); } }