Step 329: Routing RPC + MCP Tools (12/12 tests)
4 new RPC methods and MCP tools for routing and worker dispatch: routeTask, routeAllReady, executeTask, getRoutingExplanation. Template workers auto-approve getter/setter patterns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1971,4 +1971,14 @@ add_executable(step328_test tests/step328_test.cpp)
|
||||
target_include_directories(step328_test PRIVATE src)
|
||||
target_link_libraries(step328_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 329: Routing RPC + MCP Tools
|
||||
add_executable(step329_test tests/step329_test.cpp)
|
||||
target_include_directories(step329_test PRIVATE src)
|
||||
target_link_libraries(step329_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
@@ -68,7 +68,8 @@ struct AgentPermissionPolicy {
|
||||
method == "inferAnnotations" ||
|
||||
method == "getWorkflowState" ||
|
||||
method == "getReadyTasks" ||
|
||||
method == "getWorkItem") {
|
||||
method == "getWorkItem" ||
|
||||
method == "getRoutingExplanation") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -97,7 +98,10 @@ struct AgentPermissionPolicy {
|
||||
method == "assignTask" ||
|
||||
method == "completeTask" ||
|
||||
method == "rejectTask" ||
|
||||
method == "saveWorkflow") {
|
||||
method == "saveWorkflow" ||
|
||||
method == "routeTask" ||
|
||||
method == "routeAllReady" ||
|
||||
method == "executeTask") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -2442,5 +2442,157 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
});
|
||||
}
|
||||
|
||||
// --- routeTask ---
|
||||
if (method == "routeTask") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
auto decision = state.routingEngine.route(*item);
|
||||
|
||||
// Apply routing to the item
|
||||
WorkItem updated = *item;
|
||||
updated.workerType = decision.workerType;
|
||||
updated.reviewRequired = decision.reviewRequired;
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
|
||||
return headlessRpcResult(id, {{"decision", decision.toJson()}});
|
||||
}
|
||||
|
||||
// --- routeAllReady ---
|
||||
if (method == "routeAllReady") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
|
||||
auto ready = state.workflow->queue.getReady();
|
||||
json decisions = json::array();
|
||||
int routed = 0;
|
||||
|
||||
for (const auto& wi : ready) {
|
||||
auto decision = state.routingEngine.route(wi);
|
||||
WorkItem updated = wi;
|
||||
updated.workerType = decision.workerType;
|
||||
updated.reviewRequired = decision.reviewRequired;
|
||||
state.workflow->queue.updateItem(wi.id, updated);
|
||||
|
||||
json entry = decision.toJson();
|
||||
entry["itemId"] = wi.id;
|
||||
decisions.push_back(entry);
|
||||
routed++;
|
||||
}
|
||||
|
||||
return headlessRpcResult(id, {{"routed", routed}, {"decisions", decisions}});
|
||||
}
|
||||
|
||||
// --- executeTask ---
|
||||
if (method == "executeTask") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
// Get worker for this item's type
|
||||
auto* worker = state.workerRegistry.getWorker(item->workerType);
|
||||
if (!worker)
|
||||
return headlessRpcError(id, -32000,
|
||||
"No worker for type: " + item->workerType);
|
||||
|
||||
// Build context
|
||||
std::map<std::string, BufferInfo> bufferInfos;
|
||||
for (const auto& [path, buf] : state.bufferStates) {
|
||||
BufferInfo bi;
|
||||
bi.path = path;
|
||||
bi.content = buf->editBuf;
|
||||
bi.compactAst = json::array();
|
||||
bufferInfos[path] = bi;
|
||||
}
|
||||
|
||||
WorkerContext ctx = state.contextAssembler.assembleContext(
|
||||
*item, bufferInfos, item->contextWidth);
|
||||
|
||||
// Execute
|
||||
auto result = worker->execute(*item, ctx);
|
||||
|
||||
// Update item with result
|
||||
WorkItem updated = *item;
|
||||
updated.result = result;
|
||||
|
||||
bool autoApproved = false;
|
||||
// Deterministic/template with high confidence → auto-approve
|
||||
if ((item->workerType == "deterministic" || item->workerType == "template") &&
|
||||
result.confidence >= 0.8f && !item->reviewRequired) {
|
||||
autoApproved = true;
|
||||
}
|
||||
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
|
||||
return headlessRpcResult(id, {
|
||||
{"result", result.toJson()},
|
||||
{"workerType", item->workerType},
|
||||
{"autoApproved", autoApproved}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getRoutingExplanation ---
|
||||
if (method == "getRoutingExplanation") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
auto decision = state.routingEngine.route(*item);
|
||||
|
||||
json annotationsUsed = json::array();
|
||||
if (!item->workerType.empty()) annotationsUsed.push_back("@Automatability");
|
||||
if (!item->contextWidth.empty()) annotationsUsed.push_back("@ContextWidth");
|
||||
if (item->reviewRequired) annotationsUsed.push_back("@Review");
|
||||
|
||||
json rulesApplied = json::array();
|
||||
if (decision.reasoning.find("Explicit") != std::string::npos)
|
||||
rulesApplied.push_back("explicit-override");
|
||||
if (decision.reasoning.find("etter") != std::string::npos)
|
||||
rulesApplied.push_back("getter-setter-pattern");
|
||||
if (decision.reasoning.find("Cross") != std::string::npos ||
|
||||
decision.reasoning.find("cross") != std::string::npos)
|
||||
rulesApplied.push_back("context-width-escalation");
|
||||
if (decision.reasoning.find("default") != std::string::npos ||
|
||||
decision.reasoning.find("Default") != std::string::npos)
|
||||
rulesApplied.push_back("default-routing");
|
||||
|
||||
return headlessRpcResult(id, {
|
||||
{"reasoning", decision.reasoning},
|
||||
{"annotationsUsed", annotationsUsed},
|
||||
{"rulesApplied", rulesApplied},
|
||||
{"decision", decision.toJson()}
|
||||
});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
#include "SkeletonAST.h"
|
||||
#include "WorkflowState.h"
|
||||
#include "WorkflowPersistence.h"
|
||||
#include "RoutingEngine.h"
|
||||
#include "WorkerRegistry.h"
|
||||
#include "ContextAssembler.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
@@ -137,6 +140,9 @@ struct HeadlessEditorState {
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
std::optional<WorkflowState> workflow;
|
||||
RoutingEngine routingEngine;
|
||||
WorkerRegistry workerRegistry = WorkerRegistry::getDefaultRegistry();
|
||||
ContextAssembler contextAssembler;
|
||||
|
||||
// --- Buffer access ---
|
||||
|
||||
|
||||
@@ -1466,6 +1466,62 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void registerRoutingTools() {
|
||||
// whetstone_route_task
|
||||
tools_.push_back({"whetstone_route_task",
|
||||
"Route a single work item — determine worker type, context width, "
|
||||
"budget, and review requirements based on annotations.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to route"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_route_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("routeTask", args);
|
||||
};
|
||||
|
||||
// whetstone_route_all_ready
|
||||
tools_.push_back({"whetstone_route_all_ready",
|
||||
"Route all ready work items in the workflow, applying routing "
|
||||
"decisions based on their annotations.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_route_all_ready"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("routeAllReady", args);
|
||||
};
|
||||
|
||||
// whetstone_execute_task
|
||||
tools_.push_back({"whetstone_execute_task",
|
||||
"Execute a work item using its assigned worker. Deterministic and "
|
||||
"template workers produce code directly; agent/human workers prepare "
|
||||
"context bundles for external invocation.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to execute"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_execute_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("executeTask", args);
|
||||
};
|
||||
|
||||
// whetstone_get_routing_explanation
|
||||
tools_.push_back({"whetstone_get_routing_explanation",
|
||||
"Explain why a work item was routed to a specific worker type. "
|
||||
"Returns reasoning, annotations used, and rules applied.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to explain"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_routing_explanation"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getRoutingExplanation", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
registerASTTools();
|
||||
registerAnnotationTools();
|
||||
@@ -1480,5 +1536,6 @@ private:
|
||||
registerTrainingDataTools();
|
||||
registerWorkflowTools();
|
||||
registerWorkflowExecutionTools();
|
||||
registerRoutingTools();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,10 +134,10 @@ public:
|
||||
// Rule 6: Pattern defaults (no complexity annotations available
|
||||
// through WorkItem, so use heuristics)
|
||||
|
||||
// Getter/setter/accessor → deterministic
|
||||
// Getter/setter/accessor → template
|
||||
if (isGetterSetterPattern(item.nodeName)) {
|
||||
decision.workerType = "deterministic";
|
||||
decision.reasoning = "Getter/setter pattern → deterministic";
|
||||
decision.workerType = "template";
|
||||
decision.reasoning = "Getter/setter pattern → template";
|
||||
decision.confidence = 0.9f;
|
||||
decision.agentRole = "refactor";
|
||||
return decision;
|
||||
|
||||
@@ -58,7 +58,7 @@ void test_getter_deterministic() {
|
||||
TEST(getter_deterministic);
|
||||
auto wi = makeItem("getName", "", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "deterministic", "getter → deterministic");
|
||||
CHECK(d.workerType == "template", "getter → template");
|
||||
CHECK(d.agentRole == "refactor", "agent role = refactor");
|
||||
PASS();
|
||||
}
|
||||
@@ -68,7 +68,7 @@ void test_setter_deterministic() {
|
||||
TEST(setter_deterministic);
|
||||
auto wi = makeItem("setValue", "", "local");
|
||||
auto d = engine.route(wi);
|
||||
CHECK(d.workerType == "deterministic", "setter → deterministic");
|
||||
CHECK(d.workerType == "template", "setter → template");
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ void test_batch_routing() {
|
||||
};
|
||||
auto decisions = engine.routeBatch(items);
|
||||
CHECK(decisions.size() == 3, "3 decisions");
|
||||
CHECK(decisions[0].workerType == "deterministic", "first=deterministic");
|
||||
CHECK(decisions[0].workerType == "template", "first=template");
|
||||
CHECK(decisions[1].workerType == "llm", "second=llm");
|
||||
CHECK(decisions[2].workerType == "human", "third=human");
|
||||
PASS();
|
||||
|
||||
307
editor/tests/step329_test.cpp
Normal file
307
editor/tests/step329_test.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
// Step 329: Routing RPC + MCP Tools (12 tests)
|
||||
// Tests routeTask, routeAllReady, executeTask, getRoutingExplanation,
|
||||
// Linter restrictions, MCP registration.
|
||||
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "HeadlessAgentRPCHandler.h"
|
||||
#include "MCPServer.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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(),
|
||||
const std::string& session = "test-session") {
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}};
|
||||
if (!params.empty()) req["params"] = params;
|
||||
return handleHeadlessAgentRequest(state, req, session);
|
||||
}
|
||||
|
||||
// Setup: state with skeleton workflow
|
||||
static HeadlessEditorState makeState() {
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
|
||||
auto* mod = createSkeletonModule("proj", "python");
|
||||
addSkeletonFunction(mod, "getName", {}, "str"); // getter → deterministic
|
||||
addSkeletonFunction(mod, "processData", {"x"}, "int"); // local → slm
|
||||
addSkeletonFunction(mod, "orchestrate", {"ctx"}, "void"); // will set to project
|
||||
|
||||
state.openBuffer("test", "# skeleton", "python");
|
||||
state.activeBuffer->sync.setAST(std::unique_ptr<Module>(mod));
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
// 1. routeTask returns valid decision
|
||||
void test_route_task() {
|
||||
TEST(route_task);
|
||||
auto state = makeState();
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
auto resp = rpcCall(state, "routeTask", {{"itemId", itemId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
auto decision = resp["result"]["decision"];
|
||||
CHECK(decision.contains("workerType"), "has workerType");
|
||||
CHECK(decision.contains("contextWidth"), "has contextWidth");
|
||||
CHECK(decision.contains("contextBudgetTokens"), "has budget");
|
||||
CHECK(decision.contains("reasoning"), "has reasoning");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 2. routeAllReady batch processes queue
|
||||
void test_route_all_ready() {
|
||||
TEST(route_all_ready);
|
||||
auto state = makeState();
|
||||
|
||||
auto resp = rpcCall(state, "routeAllReady");
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["routed"].get<int>() == 3, "3 items routed");
|
||||
CHECK(resp["result"]["decisions"].size() == 3, "3 decisions");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 3. executeTask runs deterministic worker — produces code
|
||||
void test_execute_deterministic() {
|
||||
TEST(execute_deterministic);
|
||||
auto state = makeState();
|
||||
|
||||
// Route all first to set worker types
|
||||
rpcCall(state, "routeAllReady");
|
||||
|
||||
// Find the getter item (should be deterministic)
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string getterId;
|
||||
for (const auto& item : ready) {
|
||||
if (item["nodeName"] == "getName") {
|
||||
getterId = item["id"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(!getterId.empty(), "found getter item");
|
||||
|
||||
auto resp = rpcCall(state, "executeTask", {{"itemId", getterId}});
|
||||
CHECK(resp.contains("result"), "has result: " + resp.dump().substr(0, 200));
|
||||
CHECK(resp["result"]["workerType"] == "template", "template worker");
|
||||
CHECK(!resp["result"]["result"]["generatedCode"].get<std::string>().empty(),
|
||||
"produced code");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 4. Agent items marked as prepared, not executed
|
||||
void test_agent_prepared() {
|
||||
TEST(agent_prepared);
|
||||
auto state = makeState();
|
||||
rpcCall(state, "routeAllReady");
|
||||
|
||||
// Find an SLM-routed item
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string slmId;
|
||||
for (const auto& item : ready) {
|
||||
if (item["workerType"] == "slm") {
|
||||
slmId = item["id"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(!slmId.empty(), "found slm item");
|
||||
|
||||
auto resp = rpcCall(state, "executeTask", {{"itemId", slmId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["result"]["confidence"].get<float>() == 0.0f,
|
||||
"deferred confidence");
|
||||
CHECK(resp["result"]["result"]["reasoning"].get<std::string>().find("MCP") !=
|
||||
std::string::npos, "mentions MCP");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 5. Human items marked for review
|
||||
void test_human_marked() {
|
||||
TEST(human_marked);
|
||||
auto state = makeState();
|
||||
|
||||
// Manually set an item to human worker type
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
auto item = *state.workflow->queue.getItem(itemId);
|
||||
item.workerType = "human";
|
||||
state.workflow->queue.updateItem(itemId, item);
|
||||
|
||||
auto resp = rpcCall(state, "executeTask", {{"itemId", itemId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["workerType"] == "human", "human worker");
|
||||
CHECK(resp["result"]["result"]["reasoning"].get<std::string>().find("human") !=
|
||||
std::string::npos, "mentions human");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 6. Routing explanation includes annotation references
|
||||
void test_routing_explanation() {
|
||||
TEST(routing_explanation);
|
||||
auto state = makeState();
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
auto resp = rpcCall(state, "getRoutingExplanation", {{"itemId", itemId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"].contains("reasoning"), "has reasoning");
|
||||
CHECK(resp["result"].contains("annotationsUsed"), "has annotationsUsed");
|
||||
CHECK(resp["result"].contains("rulesApplied"), "has rulesApplied");
|
||||
CHECK(resp["result"].contains("decision"), "has decision");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 7. Linter can read explanation but not route
|
||||
void test_linter_restrictions() {
|
||||
TEST(linter_restrictions);
|
||||
auto state = makeState();
|
||||
state.setAgentRole("lint", AgentRole::Linter);
|
||||
|
||||
auto ready = rpcCall(state, "getReadyTasks", {}, "lint")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
// Read: should work
|
||||
auto resp1 = rpcCall(state, "getRoutingExplanation",
|
||||
{{"itemId", itemId}}, "lint");
|
||||
CHECK(resp1.contains("result"), "Linter can read explanation");
|
||||
|
||||
// Mutate: should fail
|
||||
auto resp2 = rpcCall(state, "routeTask", {{"itemId", itemId}}, "lint");
|
||||
CHECK(resp2.contains("error"), "Linter cannot route");
|
||||
|
||||
auto resp3 = rpcCall(state, "executeTask", {{"itemId", itemId}}, "lint");
|
||||
CHECK(resp3.contains("error"), "Linter cannot execute");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 8. MCP tool registration (4 new routing tools)
|
||||
void test_mcp_registration() {
|
||||
TEST(mcp_registration);
|
||||
MCPServer server;
|
||||
const auto& tools = server.getTools();
|
||||
|
||||
int routingCount = 0;
|
||||
for (const auto& t : tools) {
|
||||
if (t.name == "whetstone_route_task" ||
|
||||
t.name == "whetstone_route_all_ready" ||
|
||||
t.name == "whetstone_execute_task" ||
|
||||
t.name == "whetstone_get_routing_explanation") {
|
||||
routingCount++;
|
||||
}
|
||||
}
|
||||
CHECK(routingCount == 4, "4 routing tools, got " + std::to_string(routingCount));
|
||||
CHECK((int)tools.size() >= 54, "54+ total tools, got " + std::to_string(tools.size()));
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 9. routeTask updates item workerType
|
||||
void test_route_updates_item() {
|
||||
TEST(route_updates_item);
|
||||
auto state = makeState();
|
||||
|
||||
// Find getter item
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string getterId;
|
||||
for (const auto& item : ready) {
|
||||
if (item["nodeName"] == "getName") {
|
||||
getterId = item["id"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(!getterId.empty(), "found getter");
|
||||
|
||||
rpcCall(state, "routeTask", {{"itemId", getterId}});
|
||||
|
||||
auto itemResp = rpcCall(state, "getWorkItem", {{"itemId", getterId}});
|
||||
CHECK(itemResp["result"]["workerType"] == "template",
|
||||
"workerType updated to template");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 10. executeTask with no worker returns error
|
||||
void test_execute_no_worker() {
|
||||
TEST(execute_no_worker);
|
||||
auto state = makeState();
|
||||
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
// Set to unknown worker type
|
||||
auto item = *state.workflow->queue.getItem(itemId);
|
||||
item.workerType = "quantum";
|
||||
state.workflow->queue.updateItem(itemId, item);
|
||||
|
||||
auto resp = rpcCall(state, "executeTask", {{"itemId", itemId}});
|
||||
CHECK(resp.contains("error"), "error for unknown worker");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 11. routeAllReady returns decisions with itemIds
|
||||
void test_route_all_has_item_ids() {
|
||||
TEST(route_all_has_item_ids);
|
||||
auto state = makeState();
|
||||
auto resp = rpcCall(state, "routeAllReady");
|
||||
|
||||
for (const auto& d : resp["result"]["decisions"]) {
|
||||
CHECK(d.contains("itemId"), "decision has itemId");
|
||||
CHECK(d.contains("workerType"), "decision has workerType");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 12. executeTask autoApproved for deterministic with high confidence
|
||||
void test_auto_approved() {
|
||||
TEST(auto_approved);
|
||||
auto state = makeState();
|
||||
rpcCall(state, "routeAllReady");
|
||||
|
||||
// Find deterministic (getter) item
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string getterId;
|
||||
for (const auto& item : ready) {
|
||||
if (item["workerType"] == "template") {
|
||||
getterId = item["id"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK(!getterId.empty(), "found template item");
|
||||
|
||||
auto resp = rpcCall(state, "executeTask", {{"itemId", getterId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
// Template/deterministic with high confidence → autoApproved
|
||||
CHECK(resp["result"]["autoApproved"].get<bool>(), "auto approved");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "=== Step 329: Routing RPC + MCP Tools ===\n";
|
||||
try {
|
||||
test_route_task();
|
||||
test_route_all_ready();
|
||||
test_execute_deterministic();
|
||||
test_agent_prepared();
|
||||
test_human_marked();
|
||||
test_routing_explanation();
|
||||
test_linter_restrictions();
|
||||
test_mcp_registration();
|
||||
test_route_updates_item();
|
||||
test_execute_no_worker();
|
||||
test_route_all_has_item_ids();
|
||||
test_auto_approved();
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "EXCEPTION: " << e.what() << "\n";
|
||||
++failed;
|
||||
}
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
28
progress.md
28
progress.md
@@ -1393,6 +1393,34 @@ siblings > buffer content > project summaries under truncation.
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — step328_test target
|
||||
|
||||
### Step 329: Routing RPC + MCP Tools
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Exposes routing and worker dispatch through 4 RPC methods and 4 MCP tools.
|
||||
routeTask/routeAllReady apply routing decisions, executeTask runs workers
|
||||
(deterministic/template produce code, agent/human prepare context bundles),
|
||||
getRoutingExplanation shows reasoning.
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step329_test.cpp` — 12 tests: routeTask, routeAllReady batch,
|
||||
execute deterministic, agent prepared, human marked, routing explanation,
|
||||
Linter restrictions, MCP registration (4 new, 54+ total), route updates item,
|
||||
no-worker error, itemIds in batch, auto-approved for template
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/HeadlessEditorState.h` — added RoutingEngine, WorkerRegistry,
|
||||
ContextAssembler members
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` — 4 new RPC methods: routeTask,
|
||||
routeAllReady, executeTask, getRoutingExplanation
|
||||
- `editor/src/AgentPermissionPolicy.h` — read-only: getRoutingExplanation;
|
||||
mutation: routeTask, routeAllReady, executeTask
|
||||
- `editor/src/MCPServer.h` — registerRoutingTools() with 4 tools
|
||||
- `editor/src/RoutingEngine.h` — getter/setter routes to "template" (not
|
||||
"deterministic") for correct auto-approve with TemplateWorker
|
||||
- `editor/CMakeLists.txt` — step329_test target
|
||||
|
||||
**Tool count:** 54+ (50 existing + 4 routing tools)
|
||||
|
||||
---
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
Reference in New Issue
Block a user