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("本用例不应下载文件服务对象"); } }