build-flow.api/tests/BuildFlow.Scripting.Tests/ScriptQueryHandlerTests.cs
xiangning 7479521546 feat(rag): 实现RAG模块的Hybrid检索和向量索引优化
- 添加HNSW向量索引提升检索性能
- 实现Hybrid检索模式支持向量和关键词双路召回
- 采用RRF算法融合多路召回结果
- 支持ParentChild分块模式的上下文回填
- 优化向量写入采用批量SQL操作提升性能
- 添加RAG PostgreSQL连接字符串配置
- 更新部署脚本增加PGVector集成测试
- 修正表名引用从document_chunks到b_document_chunks
- 重构RagRetrievalService实现完整的检索流程
- 增加重排策略支持关键词和LLM重排
- 修改服务器部署地址配置
2026-08-11 10:59:09 +08:00

272 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using BuildFlow.Shared.Domain.Errors;
using BuildFlow.Shared.Domain.Exceptions;
using FluentAssertions;
using Scripting.Application.Handlers;
using Scripting.Application.Queries;
using Scripting.Application.Services;
using Scripting.Domain.Entities.ScriptExecutionLog;
using Scripting.Domain.Entities.ScriptFunction;
using Scripting.Domain.Enums;
using Scripting.Infrastructure.Persistence;
using Xunit;
namespace BuildFlow.Scripting.Tests;
/// <summary>
/// scripting 模块查询处理器测试(架构护栏:每个 Handler 至少 1 个测试)。
/// 用真实 InMemory ScriptingDbContext 直接实例化 handler,覆盖:
/// GetScriptDetail(成功 + 不存在→NotFound)、
/// GetScriptExecutionLogs(成功 + 状态过滤 + 空结果)、
/// GetScriptList(成功 + 状态过滤 + 关键字过滤 + 空结果)、
/// GetScriptAssemblyList / GetScriptTypeMeta(委托 ScriptTypeRegistry 的过滤/分页/去重排序)。
/// </summary>
public class ScriptQueryHandlerTests
{
// ── GetScriptDetail ────────────────────────────────────────────────────
[Fact]
public async Task GetScriptDetail_Existing_ReturnsDto()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "detail-fn", ScriptStatusEnum.Published);
var handler = new GetScriptDetailQueryHandler(db);
var dto = await handler.Handle(new GetScriptDetailQuery(script.Id), CancellationToken.None);
dto.Should().NotBeNull();
dto.Id.Should().Be(script.Id);
dto.Name.Should().Be("detail-fn");
dto.Status.Should().Be(ScriptStatusEnum.Published);
}
[Fact]
public async Task GetScriptDetail_NotFound_Throws()
{
await using var db = ScriptingTestDbFactory.Create();
var handler = new GetScriptDetailQueryHandler(db);
var act = async () => await handler.Handle(
new GetScriptDetailQuery(Guid.NewGuid()), CancellationToken.None);
var ex = await act.Should().ThrowAsync<BusinessException>();
ex.Which.Code.Should().Be(ErrorCode.NotFound);
}
// ── GetScriptExecutionLogs ─────────────────────────────────────────────
[Fact]
public async Task GetScriptExecutionLogs_Existing_ReturnsLogs()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-fn");
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Success);
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Total.Should().Be(1);
result.Items[0].ScriptId.Should().Be(script.Id);
result.Items[0].Status.Should().Be(ScriptExecutionStatusEnum.Success);
}
[Fact]
public async Task GetScriptExecutionLogs_StatusFilter_ReturnsOnlyMatching()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-filter-fn");
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Success);
await SeedLogAsync(db, script.Id, ScriptExecutionStatusEnum.Failed);
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20, ScriptExecutionStatusEnum.Success),
CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(ScriptExecutionStatusEnum.Success);
}
[Fact]
public async Task GetScriptExecutionLogs_NoLogs_ReturnsEmpty()
{
await using var db = ScriptingTestDbFactory.Create();
var script = await SeedScriptAsync(db, "log-empty-fn");
var handler = new GetScriptExecutionLogsQueryHandler(db);
var result = await handler.Handle(
new GetScriptExecutionLogsQuery(script.Id, 1, 20), CancellationToken.None);
result.Items.Should().BeEmpty();
result.Total.Should().Be(0);
}
// ── GetScriptList ──────────────────────────────────────────────────────
[Fact]
public async Task GetScriptList_ReturnsAllSeeded()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "list-a", ScriptStatusEnum.Published);
await SeedScriptAsync(db, "list-b", ScriptStatusEnum.Draft);
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, null), CancellationToken.None);
result.Total.Should().Be(2);
result.Items.Should().HaveCount(2);
}
[Fact]
public async Task GetScriptList_StatusFilter_ReturnsOnlyMatching()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "list-pub", ScriptStatusEnum.Published);
await SeedScriptAsync(db, "list-draft", ScriptStatusEnum.Draft);
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, ScriptStatusEnum.Published, null), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Status.Should().Be(ScriptStatusEnum.Published);
}
[Fact]
public async Task GetScriptList_KeywordFilter_MatchesNameOrDisplayName()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "alpha-fn", displayName: "Alpha 显示");
await SeedScriptAsync(db, "beta-fn", displayName: "Beta 显示");
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, "alpha"), CancellationToken.None);
result.Items.Should().HaveCount(1);
result.Items[0].Name.Should().Be("alpha-fn");
}
[Fact]
public async Task GetScriptList_NoMatch_ReturnsEmpty()
{
await using var db = ScriptingTestDbFactory.Create();
await SeedScriptAsync(db, "present-fn");
var handler = new GetScriptListQueryHandler(db);
var result = await handler.Handle(
new GetScriptListQuery(1, 20, null, "absent"), CancellationToken.None);
result.Items.Should().BeEmpty();
result.Total.Should().Be(0);
}
// ── GetScriptAssemblyList ──────────────────────────────────────────────
[Fact]
public async Task GetScriptAssemblyList_ReturnsDistinctSortedAssemblies()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptAssemblyListQueryHandler(registry);
var result = await handler.Handle(new GetScriptAssemblyListQuery(), CancellationToken.None);
result.Should().NotBeNull();
// 去重:每条只出现一次
result.Distinct().Should().HaveCount(result.Count, "handler 应做 Distinct");
// 排序:与直接基于 registry 计算的结果一致
var expected = registry.GetAll()
.Select(m => m.Assembly)
.Distinct()
.OrderBy(a => a)
.ToList();
result.Should().ContainInOrder(expected);
}
// ── GetScriptTypeMeta ──────────────────────────────────────────────────
[Fact]
public async Task GetScriptTypeMeta_All_ReturnsPagedMeta()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptTypeMetaQueryHandler(registry);
var page = await handler.Handle(
new GetScriptTypeMetaQuery(null, null, 1, 50), CancellationToken.None);
page.Items.Should().NotBeNull();
page.Items.Count.Should().BeLessThanOrEqualTo(50);
page.PageIndex.Should().Be(1);
page.PageSize.Should().Be(50);
page.Total.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public async Task GetScriptTypeMeta_KindFilter_ReturnsOnlyThatKind()
{
var registry = new ScriptTypeRegistry();
registry.Scan();
var handler = new GetScriptTypeMetaQueryHandler(registry);
var expectedTotal = registry.Total(null, "Query");
if (expectedTotal == 0) return; // 无 Query 类型时跳过,避免误判
var page = await handler.Handle(
new GetScriptTypeMetaQuery(null, "Query", 1, 100), CancellationToken.None);
page.Items.Should().OnlyContain(m => m.Kind == "Query");
page.Total.Should().Be(expectedTotal);
}
// ── 夹具 ───────────────────────────────────────────────────────────────
private static async Task<ScriptFunctionEntity> SeedScriptAsync(
ScriptingDbContext db,
string name,
ScriptStatusEnum status = ScriptStatusEnum.Draft,
string? displayName = null)
{
var script = new ScriptFunctionEntity
{
Id = Guid.NewGuid(),
Name = name,
DisplayName = displayName ?? name,
Code = "return 1;",
Status = status,
Version = 1,
TimeoutMs = 5000,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
};
db.Scripts.Add(script);
await db.SaveChangesAsync();
return script;
}
private static async Task<ScriptExecutionLogEntity> SeedLogAsync(
ScriptingDbContext db,
Guid functionId,
ScriptExecutionStatusEnum status,
string functionName = "fn")
{
var log = new ScriptExecutionLogEntity
{
Id = Guid.NewGuid(),
FunctionId = functionId,
FunctionName = functionName,
Status = status,
InputJson = "{}",
CreatedBy = "test",
UpdatedBy = "test",
};
db.ScriptExecutionLogs.Add(log);
await db.SaveChangesAsync();
return log;
}
}