glove/tests/mcp/lazy_init_test.cpp
2026-07-21 22:23:19 -04:00

121 lines
4.4 KiB
C++

// Lazy-init decorator: validates that initialize() runs at most once across
// any combination of explicit and implicit triggers, and that the cached
// server_info is returned on repeat explicit calls.
#include "glove/mcp/client.hpp"
#include "glove/mcp/lazy_init.hpp"
#include "glove/mcp/messages.hpp"
#include "glove/mcp/transport.hpp"
#include "src/mcp/codec.hpp"
#include <atomic>
#include <cstdio>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace {
#define REQUIRE(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \
return 1; \
} \
} while (0)
std::atomic<int> initialize_calls{0};
std::atomic<int> tools_list_calls{0};
std::atomic<int> tools_call_calls{0};
auto fake_server(std::string_view req) -> std::optional<std::string> {
if (req.find("notifications/initialized") != std::string_view::npos) {
return std::nullopt;
}
auto request = glove::mcp::codec::decode_request(req);
if (!request || !request->id) {
return R"({"jsonrpc":"2.0","id":0,"error":{"code":-32600,"message":"invalid request"}})";
}
if (request->method == "initialize") {
initialize_calls.fetch_add(1, std::memory_order_relaxed);
auto response = glove::mcp::codec::encode_response_with_result(
*request->id,
R"({"protocolVersion":"2024-11-05","serverInfo":{"name":"f","version":"0"},"capabilities":{}})"
);
return response ? std::optional<std::string>{std::move(*response)} : std::nullopt;
}
if (request->method == "tools/list") {
tools_list_calls.fetch_add(1, std::memory_order_relaxed);
auto response =
glove::mcp::codec::encode_response_with_result(*request->id, R"({"tools":[]})");
return response ? std::optional<std::string>{std::move(*response)} : std::nullopt;
}
if (request->method == "tools/call") {
tools_call_calls.fetch_add(1, std::memory_order_relaxed);
auto response = glove::mcp::codec::encode_response_with_result(
*request->id, R"({"content":[{"type":"text","text":"ok"}],"isError":false})"
);
return response ? std::optional<std::string>{std::move(*response)} : std::nullopt;
}
return std::string{R"({"jsonrpc":"2.0","id":0,"error":{"code":-32603,"message":"x"}})"};
}
auto run_implicit_first() -> int {
initialize_calls.store(0);
tools_list_calls.store(0);
tools_call_calls.store(0);
auto inner = glove::mcp::make_client(glove::mcp::make_in_memory_transport(fake_server));
auto client = glove::mcp::make_lazy_init_client(std::move(inner), "g", "0");
REQUIRE(client->list_tools().has_value());
REQUIRE(initialize_calls.load() == 1);
REQUIRE(tools_list_calls.load() == 1);
REQUIRE(client->call_tool({.name = "x", .arguments_json = "{}"}).has_value());
REQUIRE(initialize_calls.load() == 1); // still one
REQUIRE(tools_call_calls.load() == 1);
REQUIRE(client->list_tools().has_value());
REQUIRE(initialize_calls.load() == 1);
REQUIRE(tools_list_calls.load() == 2);
return 0;
}
auto run_explicit_first() -> int {
initialize_calls.store(0);
tools_list_calls.store(0);
auto inner = glove::mcp::make_client(glove::mcp::make_in_memory_transport(fake_server));
auto client = glove::mcp::make_lazy_init_client(std::move(inner), "g", "0");
auto info1 = client->initialize("", "");
REQUIRE(info1.has_value());
REQUIRE(info1->name == "f");
REQUIRE(initialize_calls.load() == 1);
auto info2 = client->initialize("", "");
REQUIRE(info2.has_value());
REQUIRE(info2->name == "f");
REQUIRE(initialize_calls.load() == 1); // cached
REQUIRE(client->list_tools().has_value());
REQUIRE(initialize_calls.load() == 1);
REQUIRE(tools_list_calls.load() == 1);
return 0;
}
} // namespace
auto main() -> int {
if (run_implicit_first() != 0) {
return 1;
}
if (run_explicit_first() != 0) {
return 1;
}
return 0;
}