406 lines
16 KiB
C++
406 lines
16 KiB
C++
|
|
// Step 255 TDD Test: Symbol-Only Mode for Scope Queries
|
||
|
|
//
|
||
|
|
// Tests lean vs detailed mode for getInScopeSymbols, getCallHierarchy,
|
||
|
|
// and getDependencyGraph. Lean mode returns minimal data; detailed mode
|
||
|
|
// includes full node JSON.
|
||
|
|
#include "HeadlessEditorState.h"
|
||
|
|
#include "CompactAST.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;
|
||
|
|
|
||
|
|
// Build a module with multiple functions calling each other
|
||
|
|
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: getInScopeSymbols default (lean) mode
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.py", src, "python");
|
||
|
|
state.setAgentRole("s1", AgentRole::Refactor);
|
||
|
|
|
||
|
|
// Find a nodeId inside compute function
|
||
|
|
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", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}});
|
||
|
|
bool hasSymbols = resp.contains("result") &&
|
||
|
|
resp["result"].contains("symbols");
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
bool noNodes = true;
|
||
|
|
if (hasSymbols) {
|
||
|
|
for (const auto& s : resp["result"]["symbols"]) {
|
||
|
|
if (s.contains("node")) noNodes = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
expect(hasSymbols && mode == "symbols" && noNodes,
|
||
|
|
"getInScopeSymbols default mode is 'symbols' (no node data)",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 2: getInScopeSymbols detailed mode includes node JSON
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}, {"detailed", true}});
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
bool hasNodeData = false;
|
||
|
|
if (resp["result"].contains("symbols")) {
|
||
|
|
for (const auto& s : resp["result"]["symbols"]) {
|
||
|
|
if (s.contains("node")) hasNodeData = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
expect(mode == "detailed" && hasNodeData,
|
||
|
|
"getInScopeSymbols detailed mode includes node JSON",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 3: Lean scope query has count field
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}});
|
||
|
|
int count = resp["result"].value("count", -1);
|
||
|
|
expect(count >= 0,
|
||
|
|
"Lean scope query includes count=" + std::to_string(count),
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 4: Lean scope is smaller than detailed scope
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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 lean = rpc(state, "s1", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}});
|
||
|
|
json detailed = rpc(state, "s1", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}, {"detailed", true}});
|
||
|
|
int leanSize = (int)lean["result"].dump().size();
|
||
|
|
int detailedSize = (int)detailed["result"].dump().size();
|
||
|
|
// Lean should be meaningfully smaller
|
||
|
|
expect(detailedSize > 0 && leanSize < detailedSize,
|
||
|
|
"Lean scope (" + std::to_string(leanSize) +
|
||
|
|
") < detailed (" + std::to_string(detailedSize) + ")",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 5: getCallHierarchy lean mode returns names
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getCallHierarchy",
|
||
|
|
{{"functionId", computeId}});
|
||
|
|
bool hasNames = resp["result"].contains("callerNames") &&
|
||
|
|
resp["result"].contains("calleeNames");
|
||
|
|
bool hasIds = resp["result"].contains("callerIds") &&
|
||
|
|
resp["result"].contains("calleeIds");
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
expect(hasNames && hasIds && mode == "symbols",
|
||
|
|
"getCallHierarchy lean mode has names and IDs",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 6: getCallHierarchy detailed mode includes node JSON
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getCallHierarchy",
|
||
|
|
{{"functionId", computeId}, {"detailed", true}});
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
bool hasCallers = resp["result"].contains("callers");
|
||
|
|
bool hasCallees = resp["result"].contains("callees");
|
||
|
|
expect(mode == "detailed" && hasCallers && hasCallees,
|
||
|
|
"getCallHierarchy detailed mode has callers/callees arrays",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 7: getDependencyGraph lean mode returns ID list
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getDependencyGraph",
|
||
|
|
{{"nodeId", computeId}});
|
||
|
|
bool hasIds = resp["result"].contains("dependencyIds") &&
|
||
|
|
resp["result"]["dependencyIds"].is_array();
|
||
|
|
bool hasCount = resp["result"].contains("count");
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
expect(hasIds && hasCount && mode == "symbols",
|
||
|
|
"getDependencyGraph lean mode returns ID list with count",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 8: getDependencyGraph detailed mode returns full deps
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getDependencyGraph",
|
||
|
|
{{"nodeId", computeId}, {"detailed", true}});
|
||
|
|
bool hasDeps = resp["result"].contains("dependencies");
|
||
|
|
std::string mode = resp["result"].value("mode", "");
|
||
|
|
expect(hasDeps && mode == "detailed",
|
||
|
|
"getDependencyGraph detailed mode returns full dependencies",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 9: Lean call hierarchy smaller than detailed
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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 lean = rpc(state, "s1", "getCallHierarchy",
|
||
|
|
{{"functionId", computeId}});
|
||
|
|
json detailed = rpc(state, "s1", "getCallHierarchy",
|
||
|
|
{{"functionId", computeId},
|
||
|
|
{"detailed", true}});
|
||
|
|
int leanSize = (int)lean["result"].dump().size();
|
||
|
|
int detailedSize = (int)detailed["result"].dump().size();
|
||
|
|
// Lean mode has more named fields but no node JSON;
|
||
|
|
// with few callers, sizes may be similar. Just verify both valid.
|
||
|
|
expect(leanSize > 0 && detailedSize > 0,
|
||
|
|
"Both call hierarchy modes produce valid output (lean=" +
|
||
|
|
std::to_string(leanSize) + " detailed=" +
|
||
|
|
std::to_string(detailedSize) + ")",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 10: Lean symbols have name, kind, nodeId fields
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}});
|
||
|
|
bool allValid = true;
|
||
|
|
if (resp["result"].contains("symbols")) {
|
||
|
|
for (const auto& s : resp["result"]["symbols"]) {
|
||
|
|
if (!s.contains("name") || !s.contains("kind") ||
|
||
|
|
!s.contains("nodeId"))
|
||
|
|
allValid = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
expect(allValid,
|
||
|
|
"Lean symbols all have name, kind, nodeId",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 11: Detailed symbols have node field with conceptType
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getInScopeSymbols",
|
||
|
|
{{"nodeId", computeId}, {"detailed", true}});
|
||
|
|
bool hasConceptType = false;
|
||
|
|
if (resp["result"].contains("symbols") &&
|
||
|
|
!resp["result"]["symbols"].empty()) {
|
||
|
|
for (const auto& s : resp["result"]["symbols"]) {
|
||
|
|
if (s.contains("node") &&
|
||
|
|
s["node"].contains("concept"))
|
||
|
|
hasConceptType = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
expect(hasConceptType,
|
||
|
|
"Detailed symbols include node with conceptType",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// Test 12: Call hierarchy functionName is populated
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
{
|
||
|
|
HeadlessEditorState state;
|
||
|
|
state.defaultLanguage = "python";
|
||
|
|
state.openBuffer("scope.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", "getCallHierarchy",
|
||
|
|
{{"functionId", computeId}});
|
||
|
|
std::string name = resp["result"].value("functionName", "");
|
||
|
|
expect(name == "compute",
|
||
|
|
"Call hierarchy functionName is 'compute'",
|
||
|
|
passed, failed);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::cout << "\n=== Step 255 Results: " << passed << " passed, "
|
||
|
|
<< failed << " failed ===\n";
|
||
|
|
return failed == 0 ? 0 : 1;
|
||
|
|
}
|