307 lines
12 KiB
C++
307 lines
12 KiB
C++
|
|
// Step 317: Architect Tooling — RPC + MCP (12 tests)
|
||
|
|
// 4 RPCs: createSkeleton, addSkeletonNode, getProjectModel, inferAnnotations.
|
||
|
|
// 4 MCP tools. Permission enforcement.
|
||
|
|
|
||
|
|
#include "HeadlessEditorState.h"
|
||
|
|
#include "HeadlessAgentRPCHandler.h"
|
||
|
|
#include "MCPServer.h"
|
||
|
|
#include <nlohmann/json.hpp>
|
||
|
|
#include <iostream>
|
||
|
|
#include <string>
|
||
|
|
#include <memory>
|
||
|
|
|
||
|
|
using json = nlohmann::json;
|
||
|
|
|
||
|
|
static int passed = 0, failed = 0;
|
||
|
|
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||
|
|
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||
|
|
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||
|
|
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||
|
|
|
||
|
|
static json rpcCall(HeadlessEditorState& state, const std::string& method,
|
||
|
|
const json& params = json::object()) {
|
||
|
|
json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}};
|
||
|
|
if (!params.empty()) req["params"] = params;
|
||
|
|
return handleHeadlessAgentRequest(state, req, "test-session");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. createSkeleton creates a new skeleton module
|
||
|
|
void test_create_skeleton_rpc() {
|
||
|
|
TEST(create_skeleton_rpc);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
auto resp = rpcCall(state, "createSkeleton", {{"name", "myapp"}, {"language", "python"}});
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
CHECK(resp["result"]["name"] == "myapp", "Module name");
|
||
|
|
CHECK(resp["result"]["language"] == "python", "Module language");
|
||
|
|
CHECK(resp["result"].contains("bufferId"), "Has bufferId");
|
||
|
|
// Active buffer should be set
|
||
|
|
CHECK(state.activeBuffer != nullptr, "Active buffer set");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. addSkeletonNode adds function with annotations
|
||
|
|
void test_add_skeleton_node_rpc() {
|
||
|
|
TEST(add_skeleton_node_rpc);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
rpcCall(state, "createSkeleton", {{"name", "myapp"}, {"language", "python"}});
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "addSkeletonNode", {
|
||
|
|
{"nodeType", "function"}, {"name", "process"},
|
||
|
|
{"parameters", json::array({"data", "config"})},
|
||
|
|
{"contextWidth", "file"}, {"automatability", "template"},
|
||
|
|
{"priority", "high"}
|
||
|
|
});
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
CHECK(resp["result"]["name"] == "process", "Node name");
|
||
|
|
CHECK(resp["result"]["nodeType"] == "function", "Node type");
|
||
|
|
CHECK(resp["result"].contains("nodeId"), "Has nodeId");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. addSkeletonNode adds class
|
||
|
|
void test_add_skeleton_class_rpc() {
|
||
|
|
TEST(add_skeleton_class_rpc);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
rpcCall(state, "createSkeleton", {{"name", "myapp"}, {"language", "python"}});
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "addSkeletonNode", {
|
||
|
|
{"nodeType", "class"}, {"name", "UserService"},
|
||
|
|
{"methods", json::array({"create", "delete", "find"})}
|
||
|
|
});
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
CHECK(resp["result"]["name"] == "UserService", "Class name");
|
||
|
|
CHECK(resp["result"]["nodeType"] == "class", "Node type");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. getProjectModel returns summary + task list
|
||
|
|
void test_get_project_model_rpc() {
|
||
|
|
TEST(get_project_model_rpc);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
rpcCall(state, "createSkeleton", {{"name", "myapp"}, {"language", "python"}});
|
||
|
|
rpcCall(state, "addSkeletonNode", {{"name", "task_a"}, {"priority", "critical"}});
|
||
|
|
rpcCall(state, "addSkeletonNode", {{"name", "task_b"},
|
||
|
|
{"contextWidth", "project"}, {"automatability", "llm"}});
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "getProjectModel");
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
auto& r = resp["result"];
|
||
|
|
CHECK(r["totalNodes"].get<int>() == 2, "2 total nodes, got: " + std::to_string(r["totalNodes"].get<int>()));
|
||
|
|
CHECK(r["skeletonNodes"].get<int>() >= 1, "Has skeleton nodes");
|
||
|
|
CHECK(r.contains("tasks"), "Has tasks");
|
||
|
|
CHECK(r["tasks"].is_array(), "Tasks is array");
|
||
|
|
CHECK(r["tasks"].size() == 2, "2 tasks");
|
||
|
|
|
||
|
|
// Check task fields
|
||
|
|
auto& t1 = r["tasks"][0];
|
||
|
|
CHECK(t1.contains("nodeName"), "Task has nodeName");
|
||
|
|
CHECK(t1.contains("contextWidth"), "Task has contextWidth");
|
||
|
|
CHECK(t1.contains("automatability"), "Task has automatability");
|
||
|
|
CHECK(t1.contains("priority"), "Task has priority");
|
||
|
|
CHECK(t1.contains("status"), "Task has status");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. inferAnnotations returns suggestions
|
||
|
|
void test_infer_annotations_rpc() {
|
||
|
|
TEST(infer_annotations_rpc);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
// Parse some Python code into the buffer
|
||
|
|
state.openBuffer("test", "def process():\n return 42\n", "python");
|
||
|
|
auto* ast = state.activeAST();
|
||
|
|
CHECK(ast != nullptr, "Has AST");
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "inferAnnotations");
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
auto& r = resp["result"];
|
||
|
|
CHECK(r.contains("suggestions"), "Has suggestions");
|
||
|
|
CHECK(r["suggestions"].is_array(), "Suggestions is array");
|
||
|
|
CHECK(r.contains("count"), "Has count");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. Permission: Linter cannot createSkeleton
|
||
|
|
void test_linter_permission_denied() {
|
||
|
|
TEST(linter_permission_denied);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Linter;
|
||
|
|
auto resp = rpcCall(state, "createSkeleton", {{"name", "x"}, {"language", "python"}});
|
||
|
|
CHECK(resp.contains("error"), "Should get error");
|
||
|
|
CHECK(resp["error"]["code"] == -32031, "Permission denied code");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 7. Permission: Linter can getProjectModel
|
||
|
|
void test_linter_can_read() {
|
||
|
|
TEST(linter_can_read);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
rpcCall(state, "createSkeleton", {{"name", "myapp"}, {"language", "python"}});
|
||
|
|
state.agent.defaultRole = AgentRole::Linter;
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "getProjectModel");
|
||
|
|
CHECK(resp.contains("result"), "Linter can call getProjectModel");
|
||
|
|
|
||
|
|
auto resp2 = rpcCall(state, "inferAnnotations");
|
||
|
|
CHECK(resp2.contains("result"), "Linter can call inferAnnotations");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 8. MCP tool registration — 4 workflow tools
|
||
|
|
void test_mcp_tool_registration() {
|
||
|
|
TEST(mcp_tool_registration);
|
||
|
|
MCPServer server;
|
||
|
|
json initReq = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"},
|
||
|
|
{"params", {{"protocolVersion", "2024-11-05"},
|
||
|
|
{"capabilities", json::object()},
|
||
|
|
{"clientInfo", {{"name", "test"}}}}}};
|
||
|
|
server.handleRequest(initReq);
|
||
|
|
|
||
|
|
json listReq = {{"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}};
|
||
|
|
json resp = server.handleRequest(listReq);
|
||
|
|
CHECK(resp.contains("result"), "Has result");
|
||
|
|
auto& tools = resp["result"]["tools"];
|
||
|
|
|
||
|
|
bool hasSkeleton = false, hasAddNode = false, hasModel = false, hasInfer = false;
|
||
|
|
for (const auto& t : tools) {
|
||
|
|
std::string name = t["name"];
|
||
|
|
if (name == "whetstone_create_skeleton") hasSkeleton = true;
|
||
|
|
if (name == "whetstone_add_skeleton_node") hasAddNode = true;
|
||
|
|
if (name == "whetstone_get_project_model") hasModel = true;
|
||
|
|
if (name == "whetstone_infer_annotations") hasInfer = true;
|
||
|
|
}
|
||
|
|
CHECK(hasSkeleton, "Has create_skeleton tool");
|
||
|
|
CHECK(hasAddNode, "Has add_skeleton_node tool");
|
||
|
|
CHECK(hasModel, "Has get_project_model tool");
|
||
|
|
CHECK(hasInfer, "Has infer_annotations tool");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 9. MCP tool count (42+)
|
||
|
|
void test_mcp_tool_count() {
|
||
|
|
TEST(mcp_tool_count);
|
||
|
|
MCPServer server;
|
||
|
|
json initReq = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"},
|
||
|
|
{"params", {{"protocolVersion", "2024-11-05"},
|
||
|
|
{"capabilities", json::object()},
|
||
|
|
{"clientInfo", {{"name", "test"}}}}}};
|
||
|
|
server.handleRequest(initReq);
|
||
|
|
|
||
|
|
json listReq = {{"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}};
|
||
|
|
json resp = server.handleRequest(listReq);
|
||
|
|
int count = (int)resp["result"]["tools"].size();
|
||
|
|
CHECK(count >= 42, "42+ tools, got: " + std::to_string(count));
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 10. Full workflow: create → add nodes → get model
|
||
|
|
void test_full_workflow() {
|
||
|
|
TEST(full_workflow);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
rpcCall(state, "createSkeleton", {{"name", "webapp"}, {"language", "python"}});
|
||
|
|
|
||
|
|
rpcCall(state, "addSkeletonNode", {
|
||
|
|
{"name", "auth_handler"}, {"contextWidth", "file"},
|
||
|
|
{"automatability", "llm"}, {"priority", "critical"}});
|
||
|
|
rpcCall(state, "addSkeletonNode", {
|
||
|
|
{"name", "data_validator"}, {"contextWidth", "local"},
|
||
|
|
{"automatability", "template"}, {"priority", "high"},
|
||
|
|
{"blockedBy", json::array({"auth_handler"})}});
|
||
|
|
rpcCall(state, "addSkeletonNode", {
|
||
|
|
{"nodeType", "class"}, {"name", "UserModel"},
|
||
|
|
{"methods", json::array({"create", "update", "delete"})}});
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "getProjectModel");
|
||
|
|
auto& r = resp["result"];
|
||
|
|
// 2 functions + 1 class + 3 methods = 6
|
||
|
|
CHECK(r["totalNodes"].get<int>() >= 5, "5+ total nodes");
|
||
|
|
CHECK(r["tasks"].size() >= 5, "5+ tasks");
|
||
|
|
|
||
|
|
// Verify routing annotations extracted
|
||
|
|
bool foundCritical = false;
|
||
|
|
for (const auto& t : r["tasks"]) {
|
||
|
|
if (t["nodeName"] == "auth_handler") {
|
||
|
|
CHECK(t["contextWidth"] == "file", "auth_handler contextWidth");
|
||
|
|
CHECK(t["automatability"] == "llm", "auth_handler automatability");
|
||
|
|
CHECK(t["priority"] == "critical", "auth_handler priority");
|
||
|
|
foundCritical = true;
|
||
|
|
}
|
||
|
|
if (t["nodeName"] == "data_validator") {
|
||
|
|
CHECK(t["dependencies"].size() == 1, "data_validator has dep");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
CHECK(foundCritical, "Found critical task");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 11. inferAnnotations response fields
|
||
|
|
void test_infer_response_fields() {
|
||
|
|
TEST(infer_response_fields);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
state.openBuffer("test",
|
||
|
|
"async def fetch():\n data = await get_data()\n return data\n",
|
||
|
|
"python");
|
||
|
|
|
||
|
|
auto resp = rpcCall(state, "inferAnnotations");
|
||
|
|
auto& suggestions = resp["result"]["suggestions"];
|
||
|
|
if (!suggestions.empty()) {
|
||
|
|
auto& s = suggestions[0];
|
||
|
|
CHECK(s.contains("nodeId"), "Suggestion has nodeId");
|
||
|
|
CHECK(s.contains("annotationType"), "Suggestion has annotationType");
|
||
|
|
CHECK(s.contains("confidence"), "Suggestion has confidence");
|
||
|
|
CHECK(s.contains("reason"), "Suggestion has reason");
|
||
|
|
double conf = s["confidence"].get<double>();
|
||
|
|
CHECK(conf >= 0.0 && conf <= 1.0, "Confidence in range");
|
||
|
|
}
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 12. No active buffer error
|
||
|
|
void test_no_active_buffer_error() {
|
||
|
|
TEST(no_active_buffer_error);
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.agent.defaultRole = AgentRole::Refactor;
|
||
|
|
// Don't create any buffer
|
||
|
|
|
||
|
|
auto resp1 = rpcCall(state, "getProjectModel");
|
||
|
|
CHECK(resp1.contains("error"), "getProjectModel error without buffer");
|
||
|
|
|
||
|
|
auto resp2 = rpcCall(state, "inferAnnotations");
|
||
|
|
CHECK(resp2.contains("error"), "inferAnnotations error without buffer");
|
||
|
|
|
||
|
|
auto resp3 = rpcCall(state, "addSkeletonNode", {{"name", "x"}});
|
||
|
|
CHECK(resp3.contains("error"), "addSkeletonNode error without buffer");
|
||
|
|
PASS();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
std::cout << "Step 317: Architect Tooling — RPC + MCP\n";
|
||
|
|
try {
|
||
|
|
test_create_skeleton_rpc();
|
||
|
|
test_add_skeleton_node_rpc();
|
||
|
|
test_add_skeleton_class_rpc();
|
||
|
|
test_get_project_model_rpc();
|
||
|
|
test_infer_annotations_rpc();
|
||
|
|
test_linter_permission_denied();
|
||
|
|
test_linter_can_read();
|
||
|
|
test_mcp_tool_registration();
|
||
|
|
test_mcp_tool_count();
|
||
|
|
test_full_workflow();
|
||
|
|
test_infer_response_fields();
|
||
|
|
test_no_active_buffer_error();
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::cout << "\nFATAL: " << e.what() << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||
|
|
return failed > 0 ? 1 : 0;
|
||
|
|
}
|