using BuildFlow.Application.Document.Handlers;
using BuildFlow.Application.Document.Queries;
using BuildFlow.Domain.Entities.Document;
using BuildFlow.Domain.Entities.KnowledgeBase;
using BuildFlow.Domain.Enums;
using BuildFlow.Domain.Interfaces;
using BuildFlow.Infrastructure.AI;
using BuildFlow.Infrastructure.Persistence;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace BuildFlow.Rag.Tests;
///
/// 文档切片预览测试。预览只提取并分块,不生成 embedding,也不写入正式分块表。
///
public class PreviewDocumentChunksQueryHandlerTests
{
[Fact]
public async Task HandleShouldReturnDryRunChunksWithoutPersistingDocumentChunks()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase($"rag-preview-{Guid.NewGuid():N}")
.Options;
await using var db = new RagDbContext(options);
var kbId = Guid.NewGuid();
var docId = Guid.NewGuid();
var filePath = Path.Combine(Path.GetTempPath(), $"preview-{Guid.NewGuid():N}.md");
await File.WriteAllTextAsync(filePath, "# 第一章\n\nBuildFlow 文档切片预览。\n\n# 第二章\n\n上传后先看切片,再手动处理向量。");
try
{
await db.KnowledgeBases.AddAsync(new KnowledgeBaseEntity
{
Id = kbId,
Name = "预览知识库",
ChunkSize = 40,
ChunkOverlap = 5,
ChunkingMode = ChunkingModeEnum.Heading,
EmbeddingModel = "text-embedding-embeddinggemma-300m",
Status = KnowledgeBaseStatusEnum.Active,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
await db.Documents.AddAsync(new DocumentEntity
{
Id = docId,
KnowledgeBaseId = kbId,
Title = "preview.md",
FileName = "preview.md",
FilePath = filePath,
FileSize = 64,
ContentType = "text/markdown",
Status = DocumentStatusEnum.Pending,
CreatedBy = "test",
UpdatedBy = "test",
OperatorIP = "127.0.0.1",
});
await db.SaveChangesAsync();
var handler = new PreviewDocumentChunksQueryHandler(
db,
new StubDocumentExtractorFactory(),
new TextChunker(),
new StubFileStorageClient(),
new HttpContextAccessor());
var result = await handler.Handle(new PreviewDocumentChunksQuery(docId), CancellationToken.None);
Assert.Equal(2, result.Count);
Assert.All(result, chunk => Assert.False(chunk.HasEmbedding));
Assert.Contains("第一章", result[0].Content);
Assert.Contains("第二章", result[1].Content);
Assert.Equal(0, await db.DocumentChunks.CountAsync());
}
finally
{
File.Delete(filePath);
}
}
private sealed class StubDocumentExtractorFactory : IDocumentExtractorFactory
{
public IDocumentExtractor GetExtractor(string fileNameOrExtension) => new StubDocumentExtractor();
}
private sealed class StubDocumentExtractor : IDocumentExtractor
{
public IReadOnlyCollection SupportedExtensions { get; init; } = ["md"];
public async Task ExtractAsync(string filePath, CancellationToken ct = default)
{
return new ExtractedDocument
{
Content = await File.ReadAllTextAsync(filePath, ct),
Title = Path.GetFileName(filePath),
};
}
}
private sealed class StubFileStorageClient : IFileStorageClient
{
public Task UploadAsync(
string bucket,
string objectKey,
Stream content,
string contentType,
string? authToken,
CancellationToken ct = default) => Task.CompletedTask;
public Task DownloadAsync(string bucket, string objectKey, string? authToken, CancellationToken ct = default)
=> throw new InvalidOperationException("本用例不应下载文件服务对象");
}
}