- 为平台测试添加EntityFrameworkCore和Xunit引用 - 实现PlatformTestDb创建带审计字段自动填充的测试数据库 - 添加DictionaryConfiguration支持配置值获取测试 - 实现完整的密码强度验证规则 - 添加RAG问答全链路端到端测试 - 创建多个Stub服务用于解耦外部依赖测试 - 实现测试专用的当前用户上下文
165 lines
5.8 KiB
C#
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("回答");
|
|
}
|
|
}
|