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 BuildFlow.Rag.Tests.Stub; using File.Application.Features.Files.DTOs; using File.Application.Features.Files.Queries; using MediatR; using Microsoft.EntityFrameworkCore; using Moq; 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 System.IO.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 mediator = new Mock(); var handler = new PreviewDocumentChunksQueryHandler( db, new StubDocumentExtractorFactory(), new TextChunker(), mediator.Object); 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()); mediator.Verify(m => m.Send( It.IsAny(), It.IsAny()), Times.Never); } finally { System.IO.File.Delete(filePath); } } [Fact] public async Task HandleRemoteDocumentShouldDownloadThroughFileQueryAndDisposeStream() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"rag-preview-remote-{Guid.NewGuid():N}") .Options; await using var db = new RagDbContext(options); var kbId = Guid.NewGuid(); var docId = Guid.NewGuid(); await db.KnowledgeBases.AddAsync(new KnowledgeBaseEntity { Id = kbId, Name = "远端预览知识库", ChunkSize = 40, ChunkOverlap = 5, ChunkingMode = ChunkingModeEnum.Heading, Status = KnowledgeBaseStatusEnum.Active, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1", }); await db.Documents.AddAsync(new DocumentEntity { Id = docId, KnowledgeBaseId = kbId, Title = "remote.md", FileName = "remote.md", FilePath = "rag-documents/docs/kb/remote.md", FileSize = 32, ContentType = "text/markdown", Status = DocumentStatusEnum.Pending, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1", }); await db.SaveChangesAsync(); var stream = new TrackingMemoryStream("# 第一章\n\n远端文档内容。"u8.ToArray()); var mediator = new Mock(); mediator.Setup(m => m.Send( It.Is(query => query.ObjectKey == "docs/kb/remote.md"), It.IsAny())) .ReturnsAsync(new DownloadFileResult(stream, "text/markdown", "remote.md")); var handler = new PreviewDocumentChunksQueryHandler( db, new StubDocumentExtractorFactory(), new TextChunker(), mediator.Object); var result = await handler.Handle(new PreviewDocumentChunksQuery(docId), CancellationToken.None); Assert.NotEmpty(result); Assert.True(stream.IsDisposed); mediator.Verify(m => m.Send( It.Is(query => query.ObjectKey == "docs/kb/remote.md"), It.IsAny()), Times.Once); } 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 System.IO.File.ReadAllTextAsync(filePath, ct), Title = Path.GetFileName(filePath), }; } } }