222 lines
9.1 KiB
C#
222 lines
9.1 KiB
C#
using BuildFlow.Application.Document.Queries;
|
|
using BuildFlow.Application.Document.Handlers;
|
|
using BuildFlow.Application.Embedding.Commands;
|
|
using BuildFlow.Application.Embedding.Handlers;
|
|
using BuildFlow.Application.Chat.Commands;
|
|
using BuildFlow.Application.Chat.Handlers;
|
|
using BuildFlow.Application.Chat.Queries;
|
|
using BuildFlow.Domain.Entities.ChatMessage;
|
|
using BuildFlow.Domain.Entities.Conversation;
|
|
using BuildFlow.Domain.Entities.Document;
|
|
using BuildFlow.Domain.Enums;
|
|
using BuildFlow.Domain.Interfaces;
|
|
using BuildFlow.Infrastructure.Persistence;
|
|
using BuildFlow.Rag.Tests.Stub;
|
|
using BuildFlow.Shared.Domain.Errors;
|
|
using BuildFlow.Shared.Domain.Exceptions;
|
|
using BuildFlow.Shared.Infrastructure;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Rag.Tests;
|
|
|
|
/// <summary>
|
|
/// [SOP-RAG] Embedding Command + DocumentEntity/Chat Query Handler 补测。
|
|
/// 覆盖:EmbedText/EmbedBatch / GetDocuments / GetConversationDetail / GetConversations / DeleteConversation。
|
|
/// </summary>
|
|
public class EmbeddingAndQueryTests
|
|
{
|
|
private static RagDbContext CreateDb([System.Runtime.CompilerServices.CallerMemberName] string name = "")
|
|
{
|
|
var userContext = new TestCurrentUserContext();
|
|
var options = new DbContextOptionsBuilder<RagDbContext>()
|
|
.UseInMemoryDatabase($"rag-misc-{name}-{Guid.NewGuid():N}")
|
|
.AddInterceptors(new AuditInterceptor(userContext))
|
|
.Options;
|
|
return new RagDbContext(options);
|
|
}
|
|
|
|
// ===== Embedding =====
|
|
|
|
[Fact]
|
|
public async Task EmbedText_ReturnsVectorAndDimensions()
|
|
{
|
|
var embedding = new StubEmbeddingService();
|
|
var handler = new EmbedTextCommandHandler(embedding);
|
|
|
|
var result = await handler.Handle(new EmbedTextCommand("测试文本"), CancellationToken.None);
|
|
|
|
result.Vector.Should().HaveCount(StubEmbeddingService.Dimensions);
|
|
result.Dimensions.Should().Be(StubEmbeddingService.Dimensions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmbedBatch_ReturnsVectorsInOrder()
|
|
{
|
|
var embedding = new StubEmbeddingService();
|
|
var handler = new EmbedBatchCommandHandler(embedding);
|
|
|
|
var result = await handler.Handle(
|
|
new EmbedBatchCommand(["文本A", "文本B", "文本C"]), CancellationToken.None);
|
|
|
|
result.Vectors.Should().HaveCount(3);
|
|
result.Vectors[0].Should().HaveCount(StubEmbeddingService.Dimensions);
|
|
result.Dimensions.Should().Be(StubEmbeddingService.Dimensions);
|
|
}
|
|
|
|
// ===== DocumentEntity Queries =====
|
|
|
|
[Fact]
|
|
public async Task GetDocuments_ByKnowledgeBase_ReturnsOnlyMatching()
|
|
{
|
|
var db = CreateDb();
|
|
var kbId = Guid.NewGuid();
|
|
var otherKbId = Guid.NewGuid();
|
|
db.Documents.Add(new DocumentEntity { KnowledgeBaseId = kbId, Title = "doc1", FileName = "a.md", FilePath = "a", ContentType = "text", Status = DocumentStatusEnum.Completed });
|
|
db.Documents.Add(new DocumentEntity { KnowledgeBaseId = kbId, Title = "doc2", FileName = "b.md", FilePath = "b", ContentType = "text", Status = DocumentStatusEnum.Pending });
|
|
db.Documents.Add(new DocumentEntity { KnowledgeBaseId = otherKbId, Title = "other", FileName = "c.md", FilePath = "c", ContentType = "text", Status = DocumentStatusEnum.Completed });
|
|
await db.SaveChangesAsync();
|
|
|
|
var handler = new GetDocumentsQueryHandler(db);
|
|
var result = await handler.Handle(new GetDocumentsQuery(kbId), CancellationToken.None);
|
|
|
|
result.Should().HaveCount(2);
|
|
result.Should().OnlyContain(d => d.KnowledgeBaseId == kbId);
|
|
}
|
|
|
|
// ===== Chat Queries =====
|
|
|
|
[Fact]
|
|
public async Task GetConversations_ReturnsCurrentUserOnly()
|
|
{
|
|
var db = CreateDb();
|
|
var userContext = new TestCurrentUserContext();
|
|
var myId = Guid.Parse(userContext.UserId);
|
|
db.Conversations.Add(new ConversationEntity { Title = "我的", UserId = myId });
|
|
db.Conversations.Add(new ConversationEntity { Title = "他人的", UserId = Guid.NewGuid() });
|
|
await db.SaveChangesAsync();
|
|
|
|
var handler = new GetConversationsQueryHandler(db, userContext);
|
|
var result = await handler.Handle(new GetConversationsQuery(), CancellationToken.None);
|
|
|
|
result.Should().HaveCount(1);
|
|
result[0].Title.Should().Be("我的");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetConversationDetail_Existing_ReturnsWithMessages()
|
|
{
|
|
var db = CreateDb();
|
|
var convId = Guid.NewGuid();
|
|
var userContext = new TestCurrentUserContext();
|
|
var userId = Guid.Parse(userContext.UserId);
|
|
db.Conversations.Add(new ConversationEntity { Id = convId, Title = "详情", UserId = userId });
|
|
db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.User, Content = "你好" });
|
|
db.ChatMessages.Add(new ChatMessageEntity { ConversationId = convId, Role = ChatRoleEnum.Assistant, Content = "回复" });
|
|
await db.SaveChangesAsync();
|
|
|
|
var cache = new StubChatMessageCache();
|
|
var handler = new GetConversationDetailQueryHandler(db, cache, userContext);
|
|
var result = await handler.Handle(new GetConversationDetailQuery(convId), CancellationToken.None);
|
|
|
|
result.Title.Should().Be("详情");
|
|
result.Messages.Should().HaveCount(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteConversation_RemovesFromDbAndCache()
|
|
{
|
|
var db = CreateDb();
|
|
var convId = Guid.NewGuid();
|
|
var userContext = new TestCurrentUserContext();
|
|
var userId = Guid.Parse(userContext.UserId);
|
|
db.Conversations.Add(new ConversationEntity { Id = convId, Title = "待删", UserId = userId });
|
|
await db.SaveChangesAsync();
|
|
|
|
var cache = new StubChatMessageCache();
|
|
await cache.SetMessagesAsync(convId, [new CachedChatMessage(Guid.NewGuid(), "user", "hi", null, DateTime.UtcNow)]);
|
|
|
|
var handler = new DeleteConversationCommandHandler(db, cache, userContext);
|
|
await handler.Handle(new DeleteConversationCommand(convId), CancellationToken.None);
|
|
|
|
(await db.Conversations.AnyAsync(c => c.Id == convId)).Should().BeFalse();
|
|
cache.Count(convId).Should().Be(0, "cache 也应清除");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetConversationDetail_OtherUsersConversation_ThrowsNotFound()
|
|
{
|
|
await using var db = CreateDb();
|
|
var userContext = new TestCurrentUserContext();
|
|
var conversationId = Guid.NewGuid();
|
|
db.Conversations.Add(new ConversationEntity
|
|
{
|
|
Id = conversationId,
|
|
Title = "他人的详情",
|
|
UserId = Guid.NewGuid(),
|
|
});
|
|
await db.SaveChangesAsync();
|
|
var handler = new GetConversationDetailQueryHandler(
|
|
db,
|
|
new StubChatMessageCache(),
|
|
userContext);
|
|
var request = new GetConversationDetailQuery(conversationId);
|
|
|
|
var act = () => handler.Handle(request, CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<NotFoundException>()
|
|
.Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateConversation_OtherUsersConversation_DoesNotChangeTitle()
|
|
{
|
|
await using var db = CreateDb();
|
|
var userContext = new TestCurrentUserContext();
|
|
var conversation = new ConversationEntity
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Title = "原始标题",
|
|
UserId = Guid.NewGuid(),
|
|
};
|
|
db.Conversations.Add(conversation);
|
|
await db.SaveChangesAsync();
|
|
var handler = new UpdateConversationCommandHandler(db, userContext);
|
|
var request = new UpdateConversationCommand(conversation.Id, "越权更新", null);
|
|
|
|
var act = () => handler.Handle(request, CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<NotFoundException>()
|
|
.Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在");
|
|
conversation.Title.Should().Be("原始标题");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteConversation_OtherUsersConversation_LeavesDbAndCacheUntouched()
|
|
{
|
|
await using var db = CreateDb();
|
|
var userContext = new TestCurrentUserContext();
|
|
var conversationId = Guid.NewGuid();
|
|
db.Conversations.Add(new ConversationEntity
|
|
{
|
|
Id = conversationId,
|
|
Title = "不可删除",
|
|
UserId = Guid.NewGuid(),
|
|
});
|
|
await db.SaveChangesAsync();
|
|
var cache = new StubChatMessageCache();
|
|
await cache.SetMessagesAsync(conversationId,
|
|
[new CachedChatMessage(Guid.NewGuid(), "user", "保留", null, DateTime.UtcNow)]);
|
|
var handler = new DeleteConversationCommandHandler(db, cache, userContext);
|
|
var request = new DeleteConversationCommand(conversationId);
|
|
|
|
var act = () => handler.Handle(request, CancellationToken.None);
|
|
|
|
await act.Should().ThrowAsync<NotFoundException>()
|
|
.Where(x => x.Code == ErrorCode.NotFound && x.Message == "会话不存在");
|
|
(await db.Conversations.AnyAsync(x => x.Id == conversationId)).Should().BeTrue();
|
|
cache.Count(conversationId).Should().Be(1);
|
|
}
|
|
}
|