// Step 259 TDD Test: Cross-File Symbol Resolution // // Tests crossFile parameter on getInScopeSymbols, import graph tracking, // exported symbol collection across open buffers, and getImportGraph RPC. #include "HeadlessEditorState.h" #include "CompactAST.h" #include #include #include 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; // Module A: defines helper and compute, imports utils std::string srcA = "import utils\n\n" "def helper(x):\n" " return x * 2\n\n" "def compute(a, b):\n" " c = helper(a)\n" " return c + b\n"; // Module B: defines transform and process std::string srcB = "def transform(data):\n" " return data + 1\n\n" "def process(items):\n" " result = transform(items)\n" " return result\n"; // Module C: imports both A and B std::string srcC = "import app\n" "import utils\n\n" "def main():\n" " x = 42\n" " return x\n"; // --------------------------------------------------------------- // Test 1: getInScopeSymbols without crossFile (baseline) // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "setActiveBuffer", {{"path", "app.py"}}); 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", 0); bool noCrossFile = !resp["result"].contains("crossFile"); // Should only have symbols from app.py bool noFileField = true; for (const auto& s : resp["result"]["symbols"]) { if (s.contains("file")) noFileField = false; } expect(count > 0 && noCrossFile && noFileField, "Baseline: " + std::to_string(count) + " local symbols, no crossFile flag", passed, failed); } // --------------------------------------------------------------- // Test 2: getInScopeSymbols with crossFile includes other buffers // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "setActiveBuffer", {{"path", "app.py"}}); 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}, {"crossFile", true}}); int count = resp["result"].value("count", 0); bool hasCrossFile = resp["result"].value("crossFile", false); // Should include symbols from utils.py (transform, process) std::set names; for (const auto& s : resp["result"]["symbols"]) names.insert(s.value("name", "")); bool hasTransform = names.count("transform") > 0; bool hasProcess = names.count("process") > 0; expect(hasCrossFile && hasTransform && hasProcess, "crossFile=true includes transform, process from utils.py (" + std::to_string(count) + " total)", passed, failed); } // --------------------------------------------------------------- // Test 3: Cross-file symbols include file path // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "setActiveBuffer", {{"path", "app.py"}}); 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}, {"crossFile", true}}); bool hasFileField = false; std::string fileVal; for (const auto& s : resp["result"]["symbols"]) { if (s.contains("file")) { hasFileField = true; fileVal = s.value("file", ""); break; } } expect(hasFileField && fileVal == "utils.py", "Cross-file symbols have file='utils.py'", passed, failed); } // --------------------------------------------------------------- // Test 4: Import graph populated on openFile // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); json resp = rpc(state, "s1", "getImportGraph", {{"file", "app.py"}}); bool hasImports = resp["result"].contains("imports") && resp["result"]["imports"].is_array(); int importCount = hasImports ? (int)resp["result"]["imports"].size() : 0; expect(hasImports && importCount == 1, "app.py imports 1 module (utils)", passed, failed); } // --------------------------------------------------------------- // Test 5: Full import graph across multiple files // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "openFile", {{"path", "main.py"}, {"content", srcC}}); json resp = rpc(state, "s1", "getImportGraph"); int fileCount = resp["result"].value("fileCount", 0); bool hasEdges = resp["result"].contains("edges"); // app.py imports utils; main.py imports app and utils // utils.py has no imports expect(hasEdges && fileCount == 2, "Import graph has 2 files with imports", passed, failed); } // --------------------------------------------------------------- // Test 6: importedBy query — who imports "utils"? // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "main.py"}, {"content", srcC}}); json resp = rpc(state, "s1", "getImportGraph", {{"file", "utils.py"}}); bool hasImportedBy = resp["result"].contains("importedBy") && resp["result"]["importedBy"].is_array(); int importerCount = hasImportedBy ? (int)resp["result"]["importedBy"].size() : 0; // Both app.py and main.py import "utils" expect(hasImportedBy && importerCount == 2, "utils imported by 2 files (app.py, main.py)", passed, failed); } // --------------------------------------------------------------- // Test 7: Cross-file detailed mode includes node JSON // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "setActiveBuffer", {{"path", "app.py"}}); 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}, {"crossFile", true}, {"detailed", true}}); bool hasNodeOnCrossFile = false; for (const auto& s : resp["result"]["symbols"]) { if (s.contains("file") && s.contains("node")) hasNodeOnCrossFile = true; } expect(hasNodeOnCrossFile, "Cross-file detailed mode includes node JSON", passed, failed); } // --------------------------------------------------------------- // Test 8: Cross-file count is larger than local-only // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); rpc(state, "s1", "setActiveBuffer", {{"path", "app.py"}}); std::string computeId; for (auto* c : state.activeAST()->allChildren()) { if (c->conceptType == "Function" && getNodeName(c) == "compute") { computeId = c->id; break; } } json local = rpc(state, "s1", "getInScopeSymbols", {{"nodeId", computeId}}); json cross = rpc(state, "s1", "getInScopeSymbols", {{"nodeId", computeId}, {"crossFile", true}}); int localCount = local["result"].value("count", 0); int crossCount = cross["result"].value("count", 0); expect(crossCount > localCount, "Cross-file count (" + std::to_string(crossCount) + ") > local count (" + std::to_string(localCount) + ")", passed, failed); } // --------------------------------------------------------------- // Test 9: closeFile removes from import graph // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "s1", "openFile", {{"path", "main.py"}, {"content", srcC}}); // Before close: 2 files in import graph json before = rpc(state, "s1", "getImportGraph"); int beforeCount = before["result"].value("fileCount", 0); rpc(state, "s1", "closeFile", {{"path", "main.py"}}); // After close: only 1 file should remain json after = rpc(state, "s1", "getImportGraph"); int afterCount = after["result"].value("fileCount", 0); expect(beforeCount == 2 && afterCount == 1, "closeFile removes from import graph (2→1)", passed, failed); } // --------------------------------------------------------------- // Test 10: Cross-file with no other buffers returns local only // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "solo.py"}, {"content", srcA}}); std::string computeId; for (auto* c : state.activeAST()->allChildren()) { if (c->conceptType == "Function" && getNodeName(c) == "compute") { computeId = c->id; break; } } json local = rpc(state, "s1", "getInScopeSymbols", {{"nodeId", computeId}}); json cross = rpc(state, "s1", "getInScopeSymbols", {{"nodeId", computeId}, {"crossFile", true}}); int localCount = local["result"].value("count", 0); int crossCount = cross["result"].value("count", 0); // With only one buffer, crossFile adds nothing expect(localCount == crossCount, "Single buffer: crossFile=local (" + std::to_string(crossCount) + ")", passed, failed); } // --------------------------------------------------------------- // Test 11: collectExportedSymbols extracts module-level symbols // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "utils.py"}, {"content", srcB}}); auto it = state.bufferStates.find("utils.py"); Module* ast = it->second->sync.getAST(); auto exports = collectExportedSymbols(ast, "utils.py"); std::set names; for (const auto& ex : exports) names.insert(ex.name); bool hasTransform = names.count("transform") > 0; bool hasProcess = names.count("process") > 0; expect(hasTransform && hasProcess && exports.size() == 2, "collectExportedSymbols: transform, process (2 total)", passed, failed); } // --------------------------------------------------------------- // Test 12: Linter role can read import graph and cross-file scope // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("admin", AgentRole::Refactor); state.setAgentRole("s1", AgentRole::Linter); rpc(state, "admin", "openFile", {{"path", "app.py"}, {"content", srcA}}); rpc(state, "admin", "openFile", {{"path", "utils.py"}, {"content", srcB}}); // Linter can read import graph json graphResp = rpc(state, "s1", "getImportGraph"); bool graphOk = graphResp.contains("result"); // Linter can query cross-file scope state.setActiveBuffer("app.py"); std::string computeId; for (auto* c : state.activeAST()->allChildren()) { if (c->conceptType == "Function" && getNodeName(c) == "compute") { computeId = c->id; break; } } json scopeResp = rpc(state, "s1", "getInScopeSymbols", {{"nodeId", computeId}, {"crossFile", true}}); bool scopeOk = scopeResp.contains("result"); expect(graphOk && scopeOk, "Linter role can read import graph and cross-file scope", passed, failed); } std::cout << "\n=== Step 259 Results: " << passed << " passed, " << failed << " failed ===\n"; return failed == 0 ? 0 : 1; }