yams/tests/unit/extraction/jsonrpc_client_test.cpp
2025-12-11 22:07:22 -05:00

150 lines
5.1 KiB
C++

#include <filesystem>
#include <catch2/catch_test_macros.hpp>
#include <yams/extraction/jsonrpc_client.hpp>
#include <yams/extraction/plugin_process.hpp>
using namespace yams::extraction;
using json = nlohmann::json;
namespace {
// Helper: Get platform-appropriate Python executable
std::string getPythonExecutable() {
const char* python_env = std::getenv("PYTHON");
if (python_env) {
return python_env;
}
#ifdef _WIN32
return "python"; // Windows uses 'python' not 'python3'
#else
return "python3"; // Unix typically uses python3
#endif
}
// Helper: Get mock plugin path relative to test source file
std::filesystem::path getMockPluginPath() {
// __FILE__ is tests/unit/extraction/jsonrpc_client_test.cpp
// Navigate up 3 levels to tests/, then into fixtures/
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path() / "fixtures" /
"mock_plugin.py";
}
} // namespace
TEST_CASE("JsonRpcClient basic communication", "[extraction][jsonrpc]") {
std::string python = getPythonExecutable();
std::filesystem::path mock_plugin = getMockPluginPath();
INFO("Python executable: " << python);
INFO("Mock plugin path: " << mock_plugin.string());
REQUIRE(std::filesystem::exists(mock_plugin));
PluginProcessConfig config{.executable = python, .args = {mock_plugin.string()}};
PluginProcess process{std::move(config)};
REQUIRE(process.is_alive());
JsonRpcClient client{process};
SECTION("Handshake manifest") {
auto result = client.call("handshake.manifest");
REQUIRE(result.has_value());
REQUIRE((*result)["name"] == "mock_plugin");
REQUIRE((*result)["version"] == "1.0.0");
REQUIRE((*result)["interfaces"].is_array());
REQUIRE((*result)["interfaces"].size() > 0);
}
SECTION("Plugin init") {
auto result = client.call("plugin.init", json::object());
REQUIRE(result.has_value());
REQUIRE((*result)["status"] == "ok");
}
SECTION("Plugin health") {
auto result = client.call("plugin.health");
REQUIRE(result.has_value());
REQUIRE((*result)["status"] == "ok");
}
SECTION("Extractor supports") {
json params = {{"mime_type", "text/plain"}, {"extension", ".txt"}};
auto result = client.call("extractor.supports", params);
REQUIRE(result.has_value());
REQUIRE((*result)["supported"] == true);
}
SECTION("Extractor extract") {
json params = {{"source", {{"type", "bytes"}, {"data", "SGVsbG8gV29ybGQ="}}}};
auto result = client.call("extractor.extract", params);
REQUIRE(result.has_value());
REQUIRE((*result)["text"] == "Mock extracted text from file");
REQUIRE((*result)["metadata"]["extractor"] == "mock_plugin");
}
SECTION("Multiple sequential calls") {
for (int i = 0; i < 5; ++i) {
auto result = client.call("plugin.health");
REQUIRE(result.has_value());
REQUIRE((*result)["status"] == "ok");
}
}
SECTION("Invalid method") {
auto result = client.call("nonexistent.method");
// Should get an error response or nullopt
REQUIRE_FALSE(result.has_value());
}
}
TEST_CASE("JsonRpcClient typed calls", "[extraction][jsonrpc]") {
std::string python = getPythonExecutable();
std::filesystem::path mock_plugin = getMockPluginPath();
INFO("Python executable: " << python);
INFO("Mock plugin path: " << mock_plugin.string());
REQUIRE(std::filesystem::exists(mock_plugin));
PluginProcessConfig config{.executable = python, .args = {mock_plugin.string()}};
PluginProcess process{std::move(config)};
JsonRpcClient client{process};
SECTION("Typed call with correct type") {
auto result = client.call<json>("handshake.manifest");
REQUIRE(result.has_value());
REQUIRE((*result)["name"] == "mock_plugin");
}
SECTION("Notification (no response expected)") {
client.notify("plugin.log", {{"level", "info"}, {"message", "test"}});
}
}
TEST_CASE("JsonRpcClient stale response handling", "[extraction][jsonrpc]") {
std::string python = getPythonExecutable();
std::filesystem::path mock_plugin =
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path() / "fixtures" /
"mock_plugin_stale.py";
INFO("Mock plugin path: " << mock_plugin.string());
if (!std::filesystem::exists(mock_plugin)) {
SKIP("mock_plugin_stale.py not found");
}
PluginProcessConfig config{.executable = python, .args = {mock_plugin.string()}};
PluginProcess process{std::move(config)};
REQUIRE(process.is_alive());
JsonRpcClient client{process};
SECTION("Skips stale responses to find matching ID") {
auto result = client.call("test.with_stale", json::object());
REQUIRE(result.has_value());
REQUIRE((*result)["status"] == "ok");
}
SECTION("Handles multiple stale responses") {
auto result = client.call("test.with_multiple_stale", json::object());
REQUIRE(result.has_value());
REQUIRE((*result)["count"] == 3);
}
}