// Step 258 TDD Test: Project Model and Workspace Indexing // // Tests openFile/closeFile/listBuffers/setActiveBuffer RPC methods, // workspace indexing, language detection, and MCP tool registration. #include "HeadlessEditorState.h" #include "CompactAST.h" #include "MCPBridge.h" #include #include #include #include namespace fs = std::filesystem; 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); } // Create a temp workspace with some files for indexing tests struct TempWorkspace { std::string root; TempWorkspace() { root = (fs::temp_directory_path() / "whetstone_test_ws_258").string(); fs::create_directories(root); fs::create_directories(root + "/src"); fs::create_directories(root + "/tests"); writeFile("src/main.py", "def main():\n print('hello')\n"); writeFile("src/utils.py", "def helper(x):\n return x * 2\n"); writeFile("src/app.cpp", "#include \nint main() { return 0; }\n"); writeFile("tests/test_main.py", "def test_main():\n assert True\n"); writeFile("README.md", "# Test Project\n"); } ~TempWorkspace() { std::error_code ec; fs::remove_all(root, ec); } void writeFile(const std::string& rel, const std::string& content) { std::string full = root + "/" + rel; fs::create_directories(fs::path(full).parent_path()); std::ofstream f(full); f << content; } }; int main() { int passed = 0; int failed = 0; std::string pySrc = "def greet(name):\n return 'hello ' + name\n"; std::string pySrc2 = "def compute(a, b):\n return a + b\n"; std::string cppSrc = "#include \nint main() { return 0; }\n"; // --------------------------------------------------------------- // Test 1: openFile creates a buffer and parses AST // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); json resp = rpc(state, "s1", "openFile", {{"path", "greet.py"}, {"content", pySrc}, {"language", "python"}}); bool hasPath = resp.contains("result") && resp["result"].value("path", "") == "greet.py"; int bufCount = resp["result"].value("bufferCount", 0); expect(hasPath && bufCount == 1, "openFile creates buffer (bufferCount=1)", passed, failed); } // --------------------------------------------------------------- // Test 2: Open 3 files, listBuffers returns all 3 // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "a.py"}, {"content", pySrc}}); rpc(state, "s1", "openFile", {{"path", "b.py"}, {"content", pySrc2}}); rpc(state, "s1", "openFile", {{"path", "c.cpp"}, {"content", cppSrc}, {"language", "cpp"}}); json resp = rpc(state, "s1", "listBuffers"); int count = resp["result"].value("count", 0); bool hasBuffers = resp["result"].contains("buffers") && resp["result"]["buffers"].is_array(); expect(count == 3 && hasBuffers, "listBuffers returns 3 buffers after opening 3 files", passed, failed); } // --------------------------------------------------------------- // Test 3: setActiveBuffer switches which buffer getAST reads // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "a.py"}, {"content", pySrc}}); rpc(state, "s1", "openFile", {{"path", "b.py"}, {"content", pySrc2}}); // Active should be "a.py" (first opened) json ast1 = rpc(state, "s1", "getAST", {{"compact", true}}); // Switch to b.py json switchResp = rpc(state, "s1", "setActiveBuffer", {{"path", "b.py"}}); std::string activePath = switchResp["result"].value("activeBuffer", ""); json ast2 = rpc(state, "s1", "getAST", {{"compact", true}}); // The two ASTs should differ (different source code) std::string ast1Dump = ast1["result"].dump(); std::string ast2Dump = ast2["result"].dump(); expect(activePath == "b.py" && ast1Dump != ast2Dump, "setActiveBuffer switches AST context", passed, failed); } // --------------------------------------------------------------- // Test 4: closeFile removes buffer from list // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "a.py"}, {"content", pySrc}}); rpc(state, "s1", "openFile", {{"path", "b.py"}, {"content", pySrc2}}); json closeResp = rpc(state, "s1", "closeFile", {{"path", "a.py"}}); int remaining = closeResp["result"].value("bufferCount", 0); json listResp = rpc(state, "s1", "listBuffers"); int count = listResp["result"].value("count", 0); expect(remaining == 1 && count == 1, "closeFile removes buffer (1 remaining)", passed, failed); } // --------------------------------------------------------------- // Test 5: Language auto-detection from file extension // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); json resp = rpc(state, "s1", "openFile", {{"path", "app.cpp"}, {"content", cppSrc}}); std::string lang = resp["result"].value("language", ""); expect(lang == "cpp", "Language auto-detected as 'cpp' for .cpp file", passed, failed); } // --------------------------------------------------------------- // Test 6: listBuffers shows active flag correctly // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); rpc(state, "s1", "openFile", {{"path", "a.py"}, {"content", pySrc}}); rpc(state, "s1", "openFile", {{"path", "b.py"}, {"content", pySrc2}}); rpc(state, "s1", "setActiveBuffer", {{"path", "b.py"}}); json resp = rpc(state, "s1", "listBuffers"); bool aActive = false, bActive = false; for (const auto& buf : resp["result"]["buffers"]) { if (buf.value("path", "") == "a.py") aActive = buf.value("active", false); if (buf.value("path", "") == "b.py") bActive = buf.value("active", false); } expect(!aActive && bActive, "listBuffers active flag: a=false, b=true", passed, failed); } // --------------------------------------------------------------- // Test 7: Workspace indexing scans files // --------------------------------------------------------------- { TempWorkspace ws; HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); json resp = rpc(state, "s1", "indexWorkspace", {{"root", ws.root}}); int fileCount = resp["result"].value("fileCount", 0); int dirCount = resp["result"].value("dirCount", 0); // 5 files: main.py, utils.py, app.cpp, test_main.py, README.md // 2 dirs: src, tests expect(fileCount == 5 && dirCount == 2, "indexWorkspace finds 5 files, 2 dirs", passed, failed); } // --------------------------------------------------------------- // Test 8: openFile reads from disk when no content provided // --------------------------------------------------------------- { TempWorkspace ws; HeadlessEditorState state; state.workspaceRoot = ws.root; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); json resp = rpc(state, "s1", "openFile", {{"path", "src/main.py"}}); bool success = resp.contains("result") && !resp.contains("error"); // Verify AST was parsed from disk content json ast = rpc(state, "s1", "getAST", {{"compact", true}}); bool hasNodes = ast.contains("result") && (ast["result"].contains("nodes") || ast["result"].contains("ast")); expect(success && hasNodes, "openFile reads from disk and parses AST", passed, failed); } // --------------------------------------------------------------- // Test 9: closeFile on nonexistent buffer returns error // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); json resp = rpc(state, "s1", "closeFile", {{"path", "nonexistent.py"}}); bool hasError = resp.contains("error"); expect(hasError, "closeFile on nonexistent buffer returns error", passed, failed); } // --------------------------------------------------------------- // Test 10: Linter role cannot openFile (mutation) // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Linter); json resp = rpc(state, "s1", "openFile", {{"path", "test.py"}, {"content", pySrc}}); bool denied = resp.contains("error"); expect(denied, "Linter role denied openFile (requires Refactor)", passed, failed); } // --------------------------------------------------------------- // Test 11: MCP tools registered for project operations // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.openBuffer("test.py", pySrc, "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); std::set expected = { "whetstone_open_file", "whetstone_close_file", "whetstone_list_buffers", "whetstone_set_active_buffer", "whetstone_index_workspace" }; std::set found; for (const auto& t : toolsJson["result"]["tools"]) { std::string name = t.value("name", ""); if (expected.count(name)) found.insert(name); } int totalTools = (int)toolsJson["result"]["tools"].size(); expect(found.size() == 5 && totalTools == 27, "5 project tools registered (" + std::to_string(totalTools) + " total tools)", passed, failed); } // --------------------------------------------------------------- // Test 12: Full workflow — open, switch, query, close // --------------------------------------------------------------- { HeadlessEditorState state; state.defaultLanguage = "python"; state.setAgentRole("s1", AgentRole::Refactor); // Open two files rpc(state, "s1", "openFile", {{"path", "greet.py"}, {"content", pySrc}}); rpc(state, "s1", "openFile", {{"path", "math.py"}, {"content", pySrc2}}); // Get AST from first (active) json ast1 = rpc(state, "s1", "getAST", {{"compact", true}}); int nodes1 = ast1["result"].value("nodeCount", 0); // Switch and get AST from second rpc(state, "s1", "setActiveBuffer", {{"path", "math.py"}}); json ast2 = rpc(state, "s1", "getAST", {{"compact", true}}); int nodes2 = ast2["result"].value("nodeCount", 0); // Both should have nodes bool bothHaveNodes = nodes1 > 0 && nodes2 > 0; // Close first, verify active stays on second rpc(state, "s1", "closeFile", {{"path", "greet.py"}}); json list = rpc(state, "s1", "listBuffers"); int remaining = list["result"].value("count", 0); std::string active = list["result"].value("activeBuffer", ""); expect(bothHaveNodes && remaining == 1 && active == "math.py", "Full workflow: open→switch→query→close " + std::to_string(nodes1) + "/" + std::to_string(nodes2) + " nodes, 1 remaining", passed, failed); } std::cout << "\n=== Step 258 Results: " << passed << " passed, " << failed << " failed ===\n"; return failed == 0 ? 0 : 1; }