build-flow.api/tests/BuildFlow.Rag.Tests/RagQAEndToEndTests.cs
xiangning f550074fcc test(platform): 添加平台模块测试基础设施和RAG端到端测试
- 为平台测试添加EntityFrameworkCore和Xunit引用
- 实现PlatformTestDb创建带审计字段自动填充的测试数据库
- 添加DictionaryConfiguration支持配置值获取测试
- 实现完整的密码强度验证规则
- 添加RAG问答全链路端到端测试
- 创建多个Stub服务用于解耦外部依赖测试
- 实现测试专用的当前用户上下文
2026-07-07 17:31:50 +08:00

165 lines
5.8 KiB
C#

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;
/// <summary>
/// [SOP-RAG-01] 知识库 RAG 问答全链路端到端测试。
/// 用 Stub IRagRetrievalService 返回预设 sources,绕过 pgvector 真实召回。
/// 验证:RagQueryCommandHandler 取 KB → RetrieveAsync → IAIChatAgent.RunAsync → 返回 answer+sources。
/// 飞书文档: https://ecn1rid3e27z.feishu.cn/wiki/F9S2wBS77i35bkkwJyVccDlfnyg
/// </summary>
public class RagQAEndToEndTests
{
private static async Task<RagDbContext> CreateDbAsync(string name)
{
var options = new DbContextOptionsBuilder<RagDbContext>()
.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("回答");
}
}