Adds token-efficient AST queries: compact mode (<30% of full size), subtree extraction by nodeId, version-tracked AST diff after mutations, and tokenEstimate on all responses. 12/12 tests pass, 17 tools total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
262 lines
10 KiB
C++
262 lines
10 KiB
C++
// Step 248 TDD Test: Compact AST Response Format
|
|
//
|
|
// Tests compact mode, subtree extraction, AST diff, version tracking,
|
|
// and tokenEstimate via HeadlessEditorState + RPC dispatch.
|
|
#include "HeadlessEditorState.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;
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
int passed = 0;
|
|
int failed = 0;
|
|
|
|
// Build a module with 50 functions for size comparison
|
|
std::string src;
|
|
for (int i = 0; i < 50; ++i) {
|
|
src += "def func_" + std::to_string(i) + "(a, b, c):\n";
|
|
src += " x = a + b\n";
|
|
src += " y = x * c\n";
|
|
src += " if y > 0:\n";
|
|
src += " return y\n";
|
|
src += " return x\n\n";
|
|
}
|
|
|
|
HeadlessEditorState state;
|
|
state.workspaceRoot = "/tmp/step248-test";
|
|
state.defaultLanguage = "python";
|
|
state.verbose = false;
|
|
state.openBuffer("multi.py", src, "python");
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
|
|
|
auto rpc = [&](const std::string& method,
|
|
json params = json::object()) {
|
|
json request = {{"jsonrpc", "2.0"}, {"id", 1},
|
|
{"method", method}, {"params", params}};
|
|
return state.processAgentRequest(request, "s1");
|
|
};
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 1: Full AST still works (backward compat)
|
|
// ---------------------------------------------------------------
|
|
json fullResp = rpc("getAST");
|
|
{
|
|
bool hasFull = fullResp.contains("result") &&
|
|
fullResp["result"].contains("ast");
|
|
expect(hasFull, "Full AST response has 'ast' field (backward compat)",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 2: Compact AST returns flat node list
|
|
// ---------------------------------------------------------------
|
|
json compactResp = rpc("getAST", {{"compact", true}});
|
|
{
|
|
bool hasNodes = compactResp.contains("result") &&
|
|
compactResp["result"].contains("nodes") &&
|
|
compactResp["result"]["nodes"].is_array();
|
|
int nodeCount = 0;
|
|
if (hasNodes)
|
|
nodeCount = (int)compactResp["result"]["nodes"].size();
|
|
expect(hasNodes && nodeCount > 0,
|
|
"Compact AST returns flat node list (got " +
|
|
std::to_string(nodeCount) + " nodes)",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 3: Compact AST is <30% the size of full AST
|
|
// ---------------------------------------------------------------
|
|
{
|
|
std::string fullDump = fullResp["result"].dump();
|
|
std::string compactDump = compactResp["result"].dump();
|
|
double ratio = (double)compactDump.size() / (double)fullDump.size();
|
|
expect(ratio < 0.30,
|
|
"Compact AST is <30% size of full (" +
|
|
std::to_string((int)(ratio * 100)) + "%)",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 4: Compact nodes have expected fields
|
|
// ---------------------------------------------------------------
|
|
{
|
|
bool valid = false;
|
|
if (compactResp.contains("result") &&
|
|
compactResp["result"].contains("nodes") &&
|
|
!compactResp["result"]["nodes"].empty()) {
|
|
const auto& first = compactResp["result"]["nodes"][0];
|
|
valid = first.contains("id") && first.contains("type");
|
|
}
|
|
expect(valid,
|
|
"Compact nodes have id and type fields",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 5: tokenEstimate is present and > 0
|
|
// ---------------------------------------------------------------
|
|
{
|
|
int te = 0;
|
|
if (fullResp.contains("result"))
|
|
te = fullResp["result"].value("tokenEstimate", 0);
|
|
expect(te > 0,
|
|
"tokenEstimate is present and > 0 (got " +
|
|
std::to_string(te) + ")",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 6: Version counter starts at 0
|
|
// ---------------------------------------------------------------
|
|
{
|
|
int ver = -1;
|
|
if (fullResp.contains("result"))
|
|
ver = fullResp["result"].value("version", -1);
|
|
expect(ver == 0,
|
|
"Version counter starts at 0",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 7: getASTSubtree returns subtree for valid nodeId
|
|
// ---------------------------------------------------------------
|
|
{
|
|
// Find a function node ID from the compact response
|
|
std::string funcId;
|
|
if (compactResp.contains("result") &&
|
|
compactResp["result"].contains("nodes")) {
|
|
for (const auto& n : compactResp["result"]["nodes"]) {
|
|
if (n.value("type", "") == "Function") {
|
|
funcId = n.value("id", "");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
json subResp = rpc("getASTSubtree", {{"nodeId", funcId}});
|
|
bool hasSub = subResp.contains("result") &&
|
|
subResp["result"].contains("subtree") &&
|
|
subResp["result"]["subtree"].contains("id");
|
|
std::string subId;
|
|
if (hasSub)
|
|
subId = subResp["result"]["subtree"].value("id", "");
|
|
expect(hasSub && subId == funcId,
|
|
"getASTSubtree returns subtree for function node",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 8: getASTSubtree with invalid nodeId returns error
|
|
// ---------------------------------------------------------------
|
|
{
|
|
json resp = rpc("getASTSubtree", {{"nodeId", "nonexistent"}});
|
|
bool isErr = resp.contains("error");
|
|
expect(isErr,
|
|
"getASTSubtree with invalid nodeId returns error",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 9: getASTDiff with no changes returns empty diff
|
|
// ---------------------------------------------------------------
|
|
{
|
|
int curVer = 0;
|
|
if (fullResp.contains("result"))
|
|
curVer = fullResp["result"].value("version", 0);
|
|
json diffResp = rpc("getASTDiff", {{"sinceVersion", curVer}});
|
|
int changedCount = -1;
|
|
if (diffResp.contains("result"))
|
|
changedCount = diffResp["result"].value("changedCount", -1);
|
|
expect(changedCount == 0,
|
|
"getASTDiff with no changes returns 0 changedCount",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 10: Version increments after mutation
|
|
// ---------------------------------------------------------------
|
|
{
|
|
// Find a function node to mutate
|
|
std::string funcId;
|
|
Module* ast = state.activeAST();
|
|
if (ast) {
|
|
for (auto* child : ast->allChildren()) {
|
|
if (child->conceptType == "Function") {
|
|
funcId = child->id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
int verBefore = state.active()->versionTracker.version;
|
|
json mutResp = rpc("applyMutation", {
|
|
{"type", "setProperty"}, {"nodeId", funcId},
|
|
{"property", "name"}, {"value", "renamed_func"}});
|
|
int verAfter = -1;
|
|
if (mutResp.contains("result"))
|
|
verAfter = mutResp["result"].value("version", -1);
|
|
expect(verAfter == verBefore + 1,
|
|
"Version increments after mutation (" +
|
|
std::to_string(verBefore) + " -> " +
|
|
std::to_string(verAfter) + ")",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 11: getASTDiff after mutation returns changed nodes
|
|
// ---------------------------------------------------------------
|
|
{
|
|
json diffResp = rpc("getASTDiff", {{"sinceVersion", 0}});
|
|
int changedCount = 0;
|
|
if (diffResp.contains("result"))
|
|
changedCount = diffResp["result"].value("changedCount", 0);
|
|
expect(changedCount > 0,
|
|
"getASTDiff after mutation returns changed nodes (got " +
|
|
std::to_string(changedCount) + ")",
|
|
passed, failed);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Test 12: Subtree tokenEstimate < full AST tokenEstimate
|
|
// ---------------------------------------------------------------
|
|
{
|
|
std::string funcId;
|
|
if (compactResp.contains("result") &&
|
|
compactResp["result"].contains("nodes")) {
|
|
for (const auto& n : compactResp["result"]["nodes"]) {
|
|
if (n.value("type", "") == "Function") {
|
|
funcId = n.value("id", "");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
json subResp = rpc("getASTSubtree", {{"nodeId", funcId}});
|
|
int subTokens = 0, fullTokens = 0;
|
|
if (subResp.contains("result"))
|
|
subTokens = subResp["result"].value("tokenEstimate", 0);
|
|
if (fullResp.contains("result"))
|
|
fullTokens = fullResp["result"].value("tokenEstimate", 0);
|
|
expect(subTokens > 0 && subTokens < fullTokens,
|
|
"Subtree tokenEstimate (" + std::to_string(subTokens) +
|
|
") < full (" + std::to_string(fullTokens) + ")",
|
|
passed, failed);
|
|
}
|
|
|
|
std::cout << "\n=== Step 248 Results: " << passed << " passed, "
|
|
<< failed << " failed ===\n";
|
|
return failed == 0 ? 0 : 1;
|
|
}
|