- 添加HNSW向量索引提升检索性能 - 实现Hybrid检索模式支持向量和关键词双路召回 - 采用RRF算法融合多路召回结果 - 支持ParentChild分块模式的上下文回填 - 优化向量写入采用批量SQL操作提升性能 - 添加RAG PostgreSQL连接字符串配置 - 更新部署脚本增加PGVector集成测试 - 修正表名引用从document_chunks到b_document_chunks - 重构RagRetrievalService实现完整的检索流程 - 增加重排策略支持关键词和LLM重排 - 修改服务器部署地址配置
272 lines
10 KiB
C#
272 lines
10 KiB
C#
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;
|
||
}
|
||
}
|