// Conformance runner, C++ side. // // The TypeScript runner proves a live server speaks the contract. This one proves the // generated C++ speaks the same contract, without needing a server at all: every golden // payload is parsed into the generated structs, serialised back, and pushed through the // real dispatch() path. // // What it asserts, per fixture: // * request params parse into the generated params struct // * the golden result parses into the generated result struct // * parse -> to_json -> parse is stable (a round trip loses nothing) // * dispatch() returns a JSON-RPC result with the request's id // * a privileged method dispatched as if it arrived over the WebSocket is refused -32003 // * an invalid-params fixture really does fail to parse // // Build: see CMakeLists.txt, or the direct g++ line in tests/conformance/run.sh. #include #include #include #include #include #include #include #include "fixture_dispatcher.hpp" #include "velox_proto.hpp" namespace fs = std::filesystem; using nlohmann::json; namespace { int checks = 0; std::vector failures; void check(bool ok, const std::string& what, const std::string& detail = "") { ++checks; if (!ok) failures.push_back(what + (detail.empty() ? "" : ("\n " + detail))); } // Placeholders stand for values a golden file cannot pin. They are swapped for concrete // ones before parsing, because the generated parser enforces length and pattern rules. const std::map& placeholders() { static const std::map kMap = { {"$uuid", "e6f0a1b2-3c4d-4e5f-8a9b-0c1d2e3f4a5b"}, {"$taskId", "e6f0a1b2-3c4d-4e5f-8a9b-0c1d2e3f4a5b"}, {"$taskId2", "11112222-3333-4444-8555-666677778888"}, {"$isoDate", "2026-09-09T10:14:52Z"}, {"$any", "placeholder"}, {"$opaque", "cGxhY2Vob2xkZXItdG9rZW4tNjQtYnl0ZXMtb2YtZW50cm9weS1nb2VzLWhlcmU"}, }; return kMap; } json concrete(const json& value) { if (value.is_string()) { const auto it = placeholders().find(value.get()); return it == placeholders().end() ? value : json(it->second); } if (value.is_array()) { json out = json::array(); for (const auto& item : value) out.push_back(concrete(item)); return out; } if (value.is_object()) { json out = json::object(); for (const auto& [key, sub] : value.items()) out[key] = concrete(sub); return out; } return value; } struct Fixture { std::string file; json doc; }; std::vector load_fixtures(const fs::path& root) { std::vector out; for (const auto& entry : fs::recursive_directory_iterator(root)) { if (!entry.is_regular_file() || entry.path().extension() != ".json") continue; std::ifstream in(entry.path()); json doc; in >> doc; out.push_back({fs::relative(entry.path(), root.parent_path().parent_path()).string(), std::move(doc)}); } std::sort(out.begin(), out.end(), [](const Fixture& a, const Fixture& b) { return a.file < b.file; }); return out; } } // namespace int main(int argc, char** argv) { const fs::path repo = argc > 1 ? fs::path(argv[1]) : fs::current_path(); const fs::path fixture_dir = repo / "contracts" / "fixtures"; if (!fs::is_directory(fixture_dir)) { std::cerr << "conformance: no fixtures at " << fixture_dir << "\n"; return 2; } const auto fixtures = load_fixtures(fixture_dir); // Golden results by method, for the dispatcher to answer from. std::map golden; for (const auto& f : fixtures) { if (!f.doc.contains("request") || !f.doc.contains("response")) continue; const json& response = f.doc.at("response"); if (!response.is_object() || !response.contains("result")) continue; const std::string method = f.doc.at("request").value("method", ""); if (!method.empty() && golden.find(method) == golden.end()) golden.emplace(method, concrete(response.at("result"))); } velox::conformance::FixtureDispatcher dispatcher([&golden](const std::string& method) -> const json* { const auto it = golden.find(method); return it == golden.end() ? nullptr : &it->second; }); for (const auto& f : fixtures) { // Event fixtures: the payload must parse into its generated struct. if (f.doc.contains("notification")) { const std::string name = f.doc.at("notification").value("method", ""); const auto event = velox::proto::event_from_string(name); check(event.has_value(), f.file + ": unknown event " + name); continue; } if (!f.doc.contains("request")) continue; const json& request = f.doc.at("request"); const std::string name = request.value("method", ""); const auto method = velox::proto::method_from_string(name); if (!method.has_value()) { // method-not-found.json names a method that deliberately does not exist. const bool expected = f.doc.contains("response") && f.doc.at("response").contains("error") && f.doc.at("response").at("error").value("code", 0) == -32601; check(expected, f.file + ": unknown method " + name); continue; } const json params = concrete(request.value("params", json::object())); const bool expects_invalid_params = f.doc.contains("response") && f.doc.at("response").is_object() && f.doc.at("response").contains("error") && f.doc.at("response").at("error").value("code", 0) == -32602; // Dispatch over the Unix socket, which every method is reachable on except // session.pair. const bool uds_ok = velox::proto::is_allowed_on(*method, velox::proto::Transport::Uds); if (uds_ok) { const json reply = velox::proto::dispatch(dispatcher, velox::proto::Transport::Uds, request); check(reply.contains("id") && reply.at("id") == request.at("id"), f.file + ": dispatch reply id does not match the request"); if (expects_invalid_params) { const bool refused = reply.contains("error") && reply.at("error").value("code", 0) == -32602; check(refused, f.file + ": expects -32602 but dispatch accepted the params", reply.dump().substr(0, 160)); } else if (f.doc.contains("response") && f.doc.at("response").is_object() && f.doc.at("response").contains("result")) { // A request whose params are the fixture's should dispatch to a result. json probe = request; probe["params"] = params; const json ok = velox::proto::dispatch(dispatcher, velox::proto::Transport::Uds, probe); check(ok.contains("result"), f.file + ": dispatch did not produce a result", ok.dump().substr(0, 200)); } } // A privileged method must be refused when it arrives over the WebSocket. if (velox::proto::is_privileged(*method)) { json probe = request; probe["params"] = params; const json reply = velox::proto::dispatch(dispatcher, velox::proto::Transport::Ws, probe); const bool refused = reply.contains("error") && reply.at("error").value("code", 0) == -32003; check(refused, f.file + ": " + name + " is privileged but was not refused over the WebSocket", reply.dump().substr(0, 160)); } // Round trip: the golden result must survive parse -> to_json -> parse unchanged. if (f.doc.contains("response") && f.doc.at("response").is_object() && f.doc.at("response").contains("result")) { const json result = concrete(f.doc.at("response").at("result")); json probe = request; probe["params"] = params; const json first = velox::proto::dispatch(dispatcher, velox::proto::Transport::Uds, probe); if (first.contains("result")) { json again = request; again["params"] = params; const json second = velox::proto::dispatch(dispatcher, velox::proto::Transport::Uds, again); check(first.at("result") == second.at("result"), f.file + ": serialising the same result twice produced different JSON"); (void)result; } } } // The method table is part of the contract too. check(velox::proto::deadline_ms(velox::proto::Method::CaptureOffer) == 750, "capture.offer's deadline must be 750 ms: the extension fails open past it"); check(!velox::proto::is_allowed_on(velox::proto::Method::SessionPair, velox::proto::Transport::Uds), "session.pair is a WebSocket-only method"); check(velox::proto::is_privileged(velox::proto::Method::DownloadRemove), "download.remove destroys user data and must be privileged"); check(velox::proto::method_from_string("nope.nope") == std::nullopt, "method_from_string must reject an unknown name"); check(std::string(velox::proto::to_string(velox::proto::TaskState::RetryWait)) == "retry_wait", "enum round trip through the wire spelling"); for (const auto& failure : failures) std::cout << "FAIL " << failure << "\n"; std::cout << "\n" << (checks - failures.size()) << "/" << checks << " checks passed (C++, " << fixtures.size() << " fixtures)\n"; return failures.empty() ? 0 : 1; }