yams/tests/unit/daemon/streaming_processor_catch2_test.cpp

205 lines
7.9 KiB
C++

// Catch2 migration of streaming_processor_test.cpp
// Migration: yams-3s4 (daemon unit tests)
#include <catch2/catch_test_macros.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <yams/daemon/ipc/request_handler.h>
#include <yams/daemon/ipc/streaming_processor.h>
using namespace yams::daemon;
namespace {
class DummyProcessor : public RequestProcessor {
public:
boost::asio::awaitable<Response> process(const Request& request) override {
if (auto* be = std::get_if<BatchEmbeddingRequest>(&request)) {
BatchEmbeddingResponse resp;
resp.dimensions = 1;
resp.modelUsed = be->modelName;
resp.processingTimeMs = 0;
resp.successCount = be->texts.size();
resp.failureCount = 0;
resp.embeddings.resize(be->texts.size());
co_return Response{std::move(resp)};
}
if (auto* ed = std::get_if<EmbedDocumentsRequest>(&request)) {
EmbedDocumentsResponse resp{};
resp.requested = ed->documentHashes.size();
resp.embedded = resp.requested;
resp.skipped = 0;
resp.failed = 0;
co_return Response{std::move(resp)};
}
co_return Response{SuccessResponse{"ok"}};
}
};
template <typename T> static T get_as(const Response& r) {
if (auto* v = std::get_if<T>(&r))
return *v;
FAIL("Unexpected response variant");
return T{};
}
} // namespace
TEST_CASE("StreamingProcessor batch embedding streams working and final", "[daemon][streaming]") {
#ifdef _WIN32
// Skip on Windows: ProtoSerializer round-trip in streaming processor
// loses Request variant data due to cross-TU/ODR normalization issues
SKIP("Streaming processor tests skipped on Windows (protobuf serialization issue)");
#endif
// Run io_context synchronously from main thread to avoid coroutine frame destruction races
boost::asio::io_context io;
auto delegate = std::make_shared<DummyProcessor>();
RequestHandler::Config cfg;
cfg.worker_executor = io.get_executor();
auto sp = std::make_unique<StreamingRequestProcessor>(delegate, cfg);
// Create request and verify variant is valid
BatchEmbeddingRequest beReq{};
beReq.modelName = "all-MiniLM-L6-v2";
beReq.normalize = true;
beReq.batchSize = 4;
beReq.texts = {"a", "b", "c", "d", "e", "f", "g", "h", "i"};
Request request{std::move(beReq)};
INFO("Test: Request variant index=" << request.index() << " holds_BE="
<< std::holds_alternative<BatchEmbeddingRequest>(request));
REQUIRE_FALSE(request.valueless_by_exception());
REQUIRE(std::holds_alternative<BatchEmbeddingRequest>(request));
const size_t expectedTextCount = 9;
// Spawn coroutine and run io_context until it completes
bool process_done = false;
std::optional<Response> opt;
boost::asio::co_spawn(io, sp->process_streaming(std::move(request)),
[&](std::exception_ptr, std::optional<Response> result) {
opt = std::move(result);
process_done = true;
});
io.run();
io.restart();
REQUIRE(process_done);
REQUIRE_FALSE(opt.has_value()); // streaming mode returns nullopt
// First chunk: started keepalive or immediate working event
RequestProcessor::ResponseChunk ch1;
std::exception_ptr ex1;
boost::asio::co_spawn(io, sp->next_chunk(),
[&](std::exception_ptr e, RequestProcessor::ResponseChunk result) {
ex1 = e;
if (!e)
ch1 = std::move(result);
});
io.run();
io.restart();
if (ex1)
std::rethrow_exception(ex1);
if (std::holds_alternative<EmbeddingEvent>(ch1.data)) {
auto ev = get_as<EmbeddingEvent>(ch1.data);
REQUIRE_FALSE(ev.modelName.empty());
}
// Drive a couple more chunks; ensure we ultimately get a final BatchEmbeddingResponse
bool got_final = false;
for (int i = 0; i < 20 && !got_final; ++i) {
RequestProcessor::ResponseChunk ch;
std::exception_ptr ex;
boost::asio::co_spawn(io, sp->next_chunk(),
[&](std::exception_ptr e, RequestProcessor::ResponseChunk result) {
ex = e;
if (!e)
ch = std::move(result);
});
io.run();
io.restart();
if (ex)
std::rethrow_exception(ex);
if (std::holds_alternative<BatchEmbeddingResponse>(ch.data)) {
auto resp = get_as<BatchEmbeddingResponse>(ch.data);
REQUIRE(resp.successCount == expectedTextCount);
REQUIRE(ch.is_last_chunk);
got_final = true;
}
}
REQUIRE(got_final);
// Cleanup - all coroutine frames already destroyed since io.run() completed
sp.reset();
}
TEST_CASE("StreamingProcessor embed documents streams working and final", "[daemon][streaming]") {
#ifdef _WIN32
SKIP("Streaming processor tests skipped on Windows (protobuf serialization issue)");
#endif
// Run io_context synchronously from main thread to avoid coroutine frame destruction races
boost::asio::io_context io;
auto delegate = std::make_shared<DummyProcessor>();
RequestHandler::Config cfg;
cfg.worker_executor = io.get_executor();
auto sp = std::make_unique<StreamingRequestProcessor>(delegate, cfg);
// Create request and verify variant is valid
EmbedDocumentsRequest edReq{};
edReq.modelName = "all-MiniLM-L6-v2";
edReq.normalize = true;
edReq.batchSize = 3;
edReq.documentHashes = {"h1", "h2", "h3", "h4", "h5", "h6", "h7"};
Request request{std::move(edReq)};
INFO("Test: Request variant index=" << request.index() << " holds_ED="
<< std::holds_alternative<EmbedDocumentsRequest>(request));
REQUIRE_FALSE(request.valueless_by_exception());
REQUIRE(std::holds_alternative<EmbedDocumentsRequest>(request));
const size_t expectedHashCount = 7;
// Spawn coroutine and run io_context until it completes
bool process_done = false;
std::optional<Response> opt;
boost::asio::co_spawn(io, sp->process_streaming(std::move(request)),
[&](std::exception_ptr, std::optional<Response> result) {
opt = std::move(result);
process_done = true;
});
io.run();
io.restart();
REQUIRE(process_done);
REQUIRE_FALSE(opt.has_value()); // streaming mode returns nullopt
// First chunk
RequestProcessor::ResponseChunk ch1;
boost::asio::co_spawn(io, sp->next_chunk(),
[&](std::exception_ptr, RequestProcessor::ResponseChunk result) {
ch1 = std::move(result);
});
io.run();
io.restart();
if (std::holds_alternative<EmbeddingEvent>(ch1.data)) {
auto ev = get_as<EmbeddingEvent>(ch1.data);
REQUIRE_FALSE(ev.modelName.empty());
}
bool got_final = false;
for (int i = 0; i < 20 && !got_final; ++i) {
RequestProcessor::ResponseChunk ch;
boost::asio::co_spawn(io, sp->next_chunk(),
[&](std::exception_ptr, RequestProcessor::ResponseChunk result) {
ch = std::move(result);
});
io.run();
io.restart();
if (std::holds_alternative<EmbedDocumentsResponse>(ch.data)) {
auto resp = get_as<EmbedDocumentsResponse>(ch.data);
REQUIRE(resp.requested == expectedHashCount);
REQUIRE(ch.is_last_chunk);
got_final = true;
}
}
REQUIRE(got_final);
// Cleanup - all coroutine frames already destroyed since io.run() completed
sp.reset();
}