using BuildFlow.Application.RagQA.Commands; using BuildFlow.Application.RagQA.Handlers; using BuildFlow.Domain.Entities; using BuildFlow.Domain.Enums; using BuildFlow.Domain.Interfaces; using BuildFlow.Infrastructure.Persistence; using BuildFlow.Rag.Tests.Stub; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace BuildFlow.Rag.Tests; /// /// [SOP-RAG-01] 知识库 RAG 问答全链路端到端测试。 /// 用 Stub IRagRetrievalService 返回预设 sources,绕过 pgvector 真实召回。 /// 验证:RagQueryCommandHandler 取 KB → RetrieveAsync → IAIChatAgent.RunAsync → 返回 answer+sources。 /// 飞书文档: https://ecn1rid3e27z.feishu.cn/wiki/F9S2wBS77i35bkkwJyVccDlfnyg /// public class RagQAEndToEndTests { private static async Task CreateDbAsync(string name) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"rag-qa-{name}-{Guid.NewGuid():N}") .Options; await using var db = new RagDbContext(options); return new RagDbContext(options); } [Fact] public async Task Query_WithSources_ReturnsAnswerAndSources() { // Given: 已创建知识库 await using var db = await CreateDbAsync("with-sources"); var kb = new KnowledgeBase { Name = "测试知识库", RetrievalMode = RetrievalMode.Hybrid, RetrievalTopK = 5, ContextTopK = 3, SimilarityThreshold = 0.3, VectorWeight = 0.7, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1" }; db.KnowledgeBases.Add(kb); var documentId = Guid.NewGuid(); var chunkId = Guid.NewGuid(); db.Documents.Add(new Document { Id = documentId, KnowledgeBaseId = kb.Id, Title = "技术架构", FileName = "arch.md", FilePath = "test/arch.md", FileSize = 100, ContentType = "text/markdown", Status = DocumentStatus.Completed, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1" }); await db.SaveChangesAsync(); // Stub RetrievalService 返回预设 sources var retrieval = new StubRagRetrievalService { Hits = [ new RetrievalHit( documentId, "技术架构", chunkId, "BuildFlow 使用 PostgreSQL + pgvector", 0.87, "BuildFlow 使用 PostgreSQL + pgvector") ] }; var chatAgent = new StubAIChatAgent { ReplyText = "BuildFlow 使用 pgvector 向量数据库" }; var handler = new RagQueryCommandHandler(db, retrieval, chatAgent); // When: 调用 RAG 查询 var result = await handler.Handle( new RAGQueryCommand(kb.Id, "BuildFlow 用了什么向量数据库?"), CancellationToken.None); // Then: 返回 answer + sources result.Answer.Should().NotBeNullOrEmpty("应有 LLM 响应"); result.Answer.Should().Be("BuildFlow 使用 pgvector 向量数据库"); result.Sources.Should().NotBeEmpty("sources 非空"); result.Sources[0].DocumentId.Should().Be(documentId, "DocumentId 匹配上传文档"); result.Sources[0].Similarity.Should().BeGreaterThan(0, "相似度>0"); } [Fact] public async Task Query_NoSources_ReturnsNotFoundMessage() { // Given: 知识库存在但 RetrievalService 返回空 await using var db = await CreateDbAsync("no-sources"); var kb = new KnowledgeBase { Name = "空知识库", CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1" }; db.KnowledgeBases.Add(kb); await db.SaveChangesAsync(); var retrieval = new StubRagRetrievalService { Hits = [] }; var chatAgent = new StubAIChatAgent(); var handler = new RagQueryCommandHandler(db, retrieval, chatAgent); // When: 查询无结果 var result = await handler.Handle( new RAGQueryCommand(kb.Id, "不存在的内容"), CancellationToken.None); // Then: 返回未找到提示,sources 空 result.Sources.Should().BeEmpty("无召回结果"); result.Answer.Should().NotBeNull("应返回提示文本"); } [Theory] [InlineData(RetrievalMode.Vector)] [InlineData(RetrievalMode.Keyword)] [InlineData(RetrievalMode.Hybrid)] public async Task Query_WorksAcrossRetrievalModes(RetrievalMode mode) { // Given: 不同 RetrievalMode 的知识库(Stub 不依赖真实召回算法) await using var db = await CreateDbAsync($"mode-{mode}"); var kb = new KnowledgeBase { Name = $"模式测试-{mode}", RetrievalMode = mode, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1" }; db.KnowledgeBases.Add(kb); await db.SaveChangesAsync(); var documentId = Guid.NewGuid(); var retrieval = new StubRagRetrievalService { Hits = [new RetrievalHit(documentId, "doc", Guid.NewGuid(), "内容", 0.9, "内容")] }; var chatAgent = new StubAIChatAgent { ReplyText = "回答" }; var handler = new RagQueryCommandHandler(db, retrieval, chatAgent); // When var result = await handler.Handle(new RAGQueryCommand(kb.Id, "问题"), CancellationToken.None); // Then: 三种模式都能返回结果(Stub 绕过真实算法差异) result.Sources.Should().HaveCount(1); result.Answer.Should().Be("回答"); } }