using System.Text.Json;
using BuildFlow.Application.RagQA.DTOs;
using BuildFlow.Application.RagQA.Queries;
using BuildFlow.Host.Endpoints.Rag.RAG;
using FastEndpoints;
using FluentAssertions;
using MediatR;
using NSubstitute;
using Xunit;
namespace BuildFlow.Host.Tests;
///
/// RAG 流式 Endpoint 测试,固定既有 Server-Sent Events 协议。
///
public class RagStreamEndpointTests
{
[Fact]
public async Task EndpointEncodesSourcesContentAndDoneUsingExistingSseProtocol()
{
var documentId = Guid.NewGuid();
var sources = new List
{
new(documentId, "架构说明", "来源片段", 0.9),
};
var mediator = Substitute.For();
mediator.CreateStream(Arg.Any(), Arg.Any())
.Returns(CreateEvents(sources));
await using var body = new MemoryStream();
var endpoint = Factory.Create(
context => context.Response.Body = body,
mediator);
var request = new RAGQueryRequest(Guid.NewGuid(), "问题");
await endpoint.HandleAsync(request, CancellationToken.None);
_ = mediator.Received(1).CreateStream(
Arg.Is(query =>
query.KnowledgeBaseId == request.KnowledgeBaseId
&& query.Question == request.Question),
Arg.Any());
body.Position = 0;
var response = await new StreamReader(body).ReadToEndAsync();
var sourcesJson = JsonSerializer.Serialize(new { sources });
var contentJson = JsonSerializer.Serialize(new { content = "回答" });
response.Should().Be(
$"event: sources\ndata: {sourcesJson}\n\n" +
$"data: {contentJson}\n\n" +
"data: [DONE]\n\n");
endpoint.HttpContext.Response.ContentType.Should().Be("text/event-stream");
endpoint.HttpContext.Response.Headers.CacheControl.ToString().Should().Be("no-cache");
endpoint.HttpContext.Response.Headers.Connection.ToString().Should().Be("keep-alive");
}
private static async IAsyncEnumerable CreateEvents(
IReadOnlyList sources)
{
await Task.Yield();
yield return RAGStreamEvent.FromSources(sources);
yield return RAGStreamEvent.Chunk("回答");
yield return RAGStreamEvent.Done();
}
}