Step 256: batch query endpoint for multi-query round-trips
batchQuery accepts array of sub-queries, returns all results in one response. Errors in individual sub-queries don't affect others. 22 MCP tools total. 12/12 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1489,4 +1489,14 @@ target_link_libraries(step255_test PRIVATE
|
|||||||
tree_sitter_java tree_sitter_rust tree_sitter_go
|
tree_sitter_java tree_sitter_rust tree_sitter_go
|
||||||
tree_sitter_org)
|
tree_sitter_org)
|
||||||
|
|
||||||
|
add_executable(step256_test tests/step256_test.cpp)
|
||||||
|
target_include_directories(step256_test PRIVATE src)
|
||||||
|
target_link_libraries(step256_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
|
||||||
|
tree_sitter_org)
|
||||||
|
|
||||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ struct AgentPermissionPolicy {
|
|||||||
method == "getASTDiff" ||
|
method == "getASTDiff" ||
|
||||||
method == "getDiagnostics" ||
|
method == "getDiagnostics" ||
|
||||||
method == "getDiagnosticsDelta" ||
|
method == "getDiagnosticsDelta" ||
|
||||||
method == "getQuickFixes") {
|
method == "getQuickFixes" ||
|
||||||
|
method == "batchQuery") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -932,5 +932,35 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
|||||||
return headlessRpcResult(id, {{"files", files}});
|
return headlessRpcResult(id, {{"files", files}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- batchQuery ---
|
||||||
|
if (method == "batchQuery") {
|
||||||
|
auto params = request.contains("params") ? request["params"]
|
||||||
|
: json::object();
|
||||||
|
if (!params.contains("queries") || !params["queries"].is_array())
|
||||||
|
return headlessRpcError(id, -32602, "Missing queries array");
|
||||||
|
json results = json::array();
|
||||||
|
for (const auto& q : params["queries"]) {
|
||||||
|
std::string subMethod = q.value("method", "");
|
||||||
|
json subParams = q.contains("params") ? q["params"]
|
||||||
|
: json::object();
|
||||||
|
json subReq = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||||
|
{"method", subMethod}, {"params", subParams}};
|
||||||
|
json subResp = handleHeadlessAgentRequest(
|
||||||
|
state, subReq, sessionId);
|
||||||
|
// Extract result or error from the sub-response
|
||||||
|
if (subResp.contains("result"))
|
||||||
|
results.push_back({{"method", subMethod},
|
||||||
|
{"result", subResp["result"]}});
|
||||||
|
else if (subResp.contains("error"))
|
||||||
|
results.push_back({{"method", subMethod},
|
||||||
|
{"error", subResp["error"]}});
|
||||||
|
else
|
||||||
|
results.push_back({{"method", subMethod},
|
||||||
|
{"error", "Unknown response"}});
|
||||||
|
}
|
||||||
|
return headlessRpcResult(id,
|
||||||
|
{{"results", results}, {"count", (int)results.size()}});
|
||||||
|
}
|
||||||
|
|
||||||
return headlessRpcError(id, -32601, "Method not found");
|
return headlessRpcError(id, -32601, "Method not found");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -721,10 +721,39 @@ private:
|
|||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Register all tools
|
// Register all tools
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Batch query tool
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
void registerBatchTools() {
|
||||||
|
// whetstone_batch_query
|
||||||
|
tools_.push_back({"whetstone_batch_query",
|
||||||
|
"Execute multiple queries in a single round-trip. "
|
||||||
|
"Pass an array of {method, params} objects. Each sub-query "
|
||||||
|
"runs independently; errors in one don't affect others.",
|
||||||
|
{{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"queries", {{"type", "array"},
|
||||||
|
{"items", {{"type", "object"},
|
||||||
|
{"properties", {
|
||||||
|
{"method", {{"type", "string"}}},
|
||||||
|
{"params", {{"type", "object"}}}
|
||||||
|
}},
|
||||||
|
{"required", json::array({"method"})}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
{"required", json::array({"queries"})}}
|
||||||
|
});
|
||||||
|
toolHandlers_["whetstone_batch_query"] = [this](const json& args) {
|
||||||
|
return callWhetstone("batchQuery", args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
void registerWhetstoneTools() {
|
void registerWhetstoneTools() {
|
||||||
registerASTTools();
|
registerASTTools();
|
||||||
registerAnnotationTools();
|
registerAnnotationTools();
|
||||||
registerFileTools();
|
registerFileTools();
|
||||||
registerDiagnosticTools();
|
registerDiagnosticTools();
|
||||||
|
registerBatchTools();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
358
editor/tests/step256_test.cpp
Normal file
358
editor/tests/step256_test.cpp
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
// Step 256 TDD Test: Batch Query Endpoint
|
||||||
|
//
|
||||||
|
// Tests batchQuery method: multiple queries in a single round-trip,
|
||||||
|
// independent error handling, and MCP tool registration.
|
||||||
|
#include "HeadlessEditorState.h"
|
||||||
|
#include "CompactAST.h"
|
||||||
|
#include "MCPBridge.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static void expect(bool cond, const std::string& name,
|
||||||
|
int& passed, int& failed) {
|
||||||
|
if (cond) {
|
||||||
|
std::cout << "Test " << (passed + failed + 1)
|
||||||
|
<< " PASS: " << name << "\n";
|
||||||
|
++passed;
|
||||||
|
} else {
|
||||||
|
std::cout << "Test " << (passed + failed + 1)
|
||||||
|
<< " FAIL: " << name << "\n";
|
||||||
|
++failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static json rpc(HeadlessEditorState& state, const std::string& session,
|
||||||
|
const std::string& method,
|
||||||
|
json params = json::object()) {
|
||||||
|
json request = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||||
|
{"method", method}, {"params", params}};
|
||||||
|
return state.processAgentRequest(request, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int passed = 0;
|
||||||
|
int failed = 0;
|
||||||
|
|
||||||
|
std::string src =
|
||||||
|
"def helper(x):\n"
|
||||||
|
" return x * 2\n\n"
|
||||||
|
"def compute(a, b):\n"
|
||||||
|
" c = helper(a)\n"
|
||||||
|
" d = helper(b)\n"
|
||||||
|
" return c + d\n\n"
|
||||||
|
"def main():\n"
|
||||||
|
" result = compute(1, 2)\n"
|
||||||
|
" return result\n";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 1: Basic batch with 3 queries returns 3 results
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getAST"}, {"params", {{"compact", true}}}},
|
||||||
|
{{"method", "getDiagnostics"}},
|
||||||
|
{{"method", "ping"}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
bool hasResults = resp.contains("result") &&
|
||||||
|
resp["result"].contains("results") &&
|
||||||
|
resp["result"]["results"].is_array();
|
||||||
|
int count = resp["result"].value("count", 0);
|
||||||
|
expect(hasResults && count == 3,
|
||||||
|
"Batch of 3 queries returns 3 results",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 2: Each result has method field
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getAST"}},
|
||||||
|
{{"method", "ping"}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
bool allHaveMethod = true;
|
||||||
|
for (const auto& r : resp["result"]["results"]) {
|
||||||
|
if (!r.contains("method")) allHaveMethod = false;
|
||||||
|
}
|
||||||
|
expect(allHaveMethod,
|
||||||
|
"Each batch result has method field",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 3: Successful sub-queries have result field
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "ping"}},
|
||||||
|
{{"method", "getAST"}, {"params", {{"compact", true}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
bool pingOk = resp["result"]["results"][0].contains("result");
|
||||||
|
bool astOk = resp["result"]["results"][1].contains("result");
|
||||||
|
expect(pingOk && astOk,
|
||||||
|
"Successful sub-queries have result field",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 4: Error in one sub-query doesn't fail others
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "ping"}},
|
||||||
|
{{"method", "nonExistentMethod"}},
|
||||||
|
{{"method", "getAST"}, {"params", {{"compact", true}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
bool pingOk = resp["result"]["results"][0].contains("result");
|
||||||
|
bool errorOk = resp["result"]["results"][1].contains("error");
|
||||||
|
bool astOk = resp["result"]["results"][2].contains("result");
|
||||||
|
expect(pingOk && errorOk && astOk,
|
||||||
|
"Error in sub-query #2 doesn't affect #1 or #3",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 5: Batch with AST + diagnostics + scope
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
std::string computeId;
|
||||||
|
for (auto* c : state.activeAST()->allChildren()) {
|
||||||
|
if (c->conceptType == "Function" &&
|
||||||
|
getNodeName(c) == "compute") {
|
||||||
|
computeId = c->id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getAST"}, {"params", {{"compact", true}}}},
|
||||||
|
{{"method", "getDiagnostics"}},
|
||||||
|
{{"method", "getInScopeSymbols"},
|
||||||
|
{"params", {{"nodeId", computeId}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
auto& results = resp["result"]["results"];
|
||||||
|
bool astHasNodes = results[0]["result"].contains("nodes") ||
|
||||||
|
results[0]["result"].contains("ast");
|
||||||
|
bool diagHasDiags = results[1]["result"].contains("diagnostics");
|
||||||
|
bool scopeHasSymbols = results[2]["result"].contains("symbols");
|
||||||
|
expect(astHasNodes && diagHasDiags && scopeHasSymbols,
|
||||||
|
"Batch [getAST, getDiagnostics, getInScopeSymbols] all succeed",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 6: Empty batch returns empty results
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array()}
|
||||||
|
});
|
||||||
|
int count = resp["result"].value("count", -1);
|
||||||
|
expect(count == 0,
|
||||||
|
"Empty batch returns count=0",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 7: Missing queries parameter returns error
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", json::object());
|
||||||
|
bool hasError = resp.contains("error");
|
||||||
|
expect(hasError,
|
||||||
|
"Missing queries parameter returns error",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 8: Batch with budget params passes through
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getAST"},
|
||||||
|
{"params", {{"compact", true}, {"budget", 200}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
auto& r = resp["result"]["results"][0]["result"];
|
||||||
|
bool hasTruncated = r.contains("truncated");
|
||||||
|
expect(hasTruncated,
|
||||||
|
"Budget parameter passes through in batch sub-query",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 9: Batch uses single round-trip for multiple queries
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
// Batch call returns all 3 results in a single response
|
||||||
|
json batchResp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getAST"}, {"params", {{"compact", true}}}},
|
||||||
|
{{"method", "getDiagnostics"}},
|
||||||
|
{{"method", "ping"}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
int batchCount = batchResp["result"].value("count", 0);
|
||||||
|
bool allHaveResults = true;
|
||||||
|
for (const auto& r : batchResp["result"]["results"]) {
|
||||||
|
if (!r.contains("result") && !r.contains("error"))
|
||||||
|
allHaveResults = false;
|
||||||
|
}
|
||||||
|
// One envelope, three results — that's the protocol win
|
||||||
|
expect(batchCount == 3 && allHaveResults,
|
||||||
|
"Single round-trip returns 3 results in one envelope",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 10: Permission denied in sub-query returns error
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Linter);
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "ping"}},
|
||||||
|
{{"method", "applyMutation"},
|
||||||
|
{"params", {{"type", "deleteNode"},
|
||||||
|
{"nodeId", "bad"}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
bool pingOk = resp["result"]["results"][0].contains("result");
|
||||||
|
bool mutErr = resp["result"]["results"][1].contains("error");
|
||||||
|
expect(pingOk && mutErr,
|
||||||
|
"Permission-denied sub-query returns error, others succeed",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 11: MCP tool registration
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("mcp-session", AgentRole::Refactor);
|
||||||
|
|
||||||
|
MCPBridge bridge;
|
||||||
|
bridge.setRequestHandler([&state](const json& request) -> json {
|
||||||
|
return state.processAgentRequest(request, "mcp-session");
|
||||||
|
});
|
||||||
|
|
||||||
|
json initReq = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||||
|
{"method", "initialize"},
|
||||||
|
{"params", {{"protocolVersion", "2024-11-05"},
|
||||||
|
{"clientInfo", {{"name", "test"}}}}}};
|
||||||
|
bridge.processMessage(initReq.dump());
|
||||||
|
|
||||||
|
json toolsReq = {{"jsonrpc", "2.0"}, {"id", 2},
|
||||||
|
{"method", "tools/list"}};
|
||||||
|
std::string toolsResp = bridge.processMessage(toolsReq.dump());
|
||||||
|
json toolsJson = json::parse(toolsResp);
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& t : toolsJson["result"]["tools"]) {
|
||||||
|
if (t["name"] == "whetstone_batch_query") found = true;
|
||||||
|
}
|
||||||
|
expect(found,
|
||||||
|
"whetstone_batch_query registered in MCP tools/list",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Test 12: Batch with detailed and lean scope queries
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
HeadlessEditorState state;
|
||||||
|
state.defaultLanguage = "python";
|
||||||
|
state.openBuffer("batch.py", src, "python");
|
||||||
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||||||
|
|
||||||
|
std::string computeId;
|
||||||
|
for (auto* c : state.activeAST()->allChildren()) {
|
||||||
|
if (c->conceptType == "Function" &&
|
||||||
|
getNodeName(c) == "compute") {
|
||||||
|
computeId = c->id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json resp = rpc(state, "s1", "batchQuery", {
|
||||||
|
{"queries", json::array({
|
||||||
|
{{"method", "getInScopeSymbols"},
|
||||||
|
{"params", {{"nodeId", computeId}}}},
|
||||||
|
{{"method", "getInScopeSymbols"},
|
||||||
|
{"params", {{"nodeId", computeId}, {"detailed", true}}}}
|
||||||
|
})}
|
||||||
|
});
|
||||||
|
auto& results = resp["result"]["results"];
|
||||||
|
std::string leanMode = results[0]["result"].value("mode", "");
|
||||||
|
std::string detMode = results[1]["result"].value("mode", "");
|
||||||
|
expect(leanMode == "symbols" && detMode == "detailed",
|
||||||
|
"Batch lean + detailed scope queries return correct modes",
|
||||||
|
passed, failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n=== Step 256 Results: " << passed << " passed, "
|
||||||
|
<< failed << " failed ===\n";
|
||||||
|
return failed == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
30
progress.md
30
progress.md
@@ -339,3 +339,33 @@ getDependencyGraph. Default (lean) mode returns minimal symbol data
|
|||||||
- Lean scope responses ~7% the size of detailed (302 vs 4329 chars)
|
- Lean scope responses ~7% the size of detailed (302 vs 4329 chars)
|
||||||
- getDependencyGraph lean mode converts vector<string> IDs to JSON array
|
- getDependencyGraph lean mode converts vector<string> IDs to JSON array
|
||||||
- Uses getNodeName() from CompactAST.h for cross-type name extraction
|
- Uses getNodeName() from CompactAST.h for cross-type name extraction
|
||||||
|
|
||||||
|
### Step 256: Batch Query Endpoint
|
||||||
|
**Status:** PASS (12/12 tests)
|
||||||
|
|
||||||
|
`batchQuery` method accepts an array of sub-queries, executes each
|
||||||
|
independently, and returns all results in a single round-trip. Errors
|
||||||
|
in one sub-query don't affect others.
|
||||||
|
|
||||||
|
**Files created:**
|
||||||
|
- `editor/tests/step256_test.cpp` — 12 test cases: 3-query batch, method fields,
|
||||||
|
result fields, error isolation, AST+diagnostics+scope batch, empty batch,
|
||||||
|
missing params error, budget passthrough, single-envelope multi-result,
|
||||||
|
permission-denied isolation, MCP registration, lean+detailed mixed batch
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `editor/src/HeadlessAgentRPCHandler.h` — batchQuery RPC method: iterates
|
||||||
|
sub-queries, calls handleHeadlessAgentRequest recursively, collects
|
||||||
|
results/errors independently
|
||||||
|
- `editor/src/AgentPermissionPolicy.h` — batchQuery allowed for all roles
|
||||||
|
(sub-queries enforce their own permissions)
|
||||||
|
- `editor/src/MCPServer.h` — registerBatchTools() with whetstone_batch_query
|
||||||
|
tool definition and callWhetstone handler
|
||||||
|
- `editor/CMakeLists.txt` — step256_test target
|
||||||
|
|
||||||
|
**Key design decisions:**
|
||||||
|
- Sub-queries reuse the same dispatch function (recursive call)
|
||||||
|
- Each sub-query result tagged with its method name for correlation
|
||||||
|
- Error isolation: failed sub-query returns error object, others unaffected
|
||||||
|
- batchQuery itself requires no special permission; sub-queries enforce their own
|
||||||
|
- tools/list now returns 22 tools (was 21): +whetstone_batch_query
|
||||||
|
|||||||
Reference in New Issue
Block a user