// Step 1991: AST sidecar lifecycle hooks // Verifies that saveBuffer auto-persists annotated AST sidecars, and that // openFile auto-restores those annotations into the newly-parsed live AST. #include "HeadlessEditorState.h" #include "MCPServer.h" #include "SidecarPersistence.h" #include #include #include #include #include namespace fs = std::filesystem; static json rpc(HeadlessEditorState& state, const std::string& method, json params = json::object()) { json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}, {"params", std::move(params)}}; return state.processAgentRequest(req, "test-session"); } static std::string makeTempWorkspace(const std::string& name) { fs::path tmp = fs::temp_directory_path() / name; fs::create_directories(tmp / "src"); std::ofstream f(tmp / "src" / "Foo.h"); f << "namespace foo {\nstruct Foo {\n int x = 0;\n};\n}\n"; return tmp.string(); } int main() { int passed = 0; // Test 1: saveBuffer creates .ast.json sidecar { std::string ws = makeTempWorkspace("step1991_t1"); std::string filePath = ws + "/src/Foo.h"; HeadlessEditorState state; state.workspaceRoot = ws; state.defaultLanguage = "cpp"; rpc(state, "openFile", {{"path", filePath}, {"language", "cpp"}}); auto saveResp = rpc(state, "saveBuffer", {{"path", filePath}}); std::string sc = sidecarPath(ws, filePath); assert(fs::exists(sc)); assert(saveResp["result"].contains("annotationsSaved")); assert(saveResp["result"].contains("sidecarPath")); std::cout << "Test 1 PASSED: saveBuffer creates .ast.json sidecar\n"; ++passed; } // Test 2: saveBuffer response includes semannoPath { std::string ws = makeTempWorkspace("step1991_t2"); std::string filePath = ws + "/src/Foo.h"; HeadlessEditorState state; state.workspaceRoot = ws; state.defaultLanguage = "cpp"; rpc(state, "openFile", {{"path", filePath}, {"language", "cpp"}}); auto saveResp = rpc(state, "saveBuffer", {{"path", filePath}}); assert(saveResp["result"].contains("semannoPath")); assert(saveResp["result"]["success"] == true); std::cout << "Test 2 PASSED: saveBuffer response includes semannoPath\n"; ++passed; } // Test 3: openFile after save returns annotationsRestored in response { std::string ws = makeTempWorkspace("step1991_t3"); std::string filePath = ws + "/src/Foo.h"; HeadlessEditorState state; state.workspaceRoot = ws; state.defaultLanguage = "cpp"; rpc(state, "openFile", {{"path", filePath}, {"language", "cpp"}}); rpc(state, "saveBuffer", {{"path", filePath}}); rpc(state, "closeFile", {{"path", filePath}}); auto reopenResp = rpc(state, "openFile", {{"path", filePath}, {"language", "cpp"}}); assert(!reopenResp.contains("error")); assert(reopenResp["result"].contains("annotationsRestored")); assert(reopenResp["result"].contains("staleAnnotations")); std::cout << "Test 3 PASSED: openFile response includes annotationsRestored\n"; ++passed; } // Test 4: stale annotations (file content replaced) are skipped without error { std::string ws = makeTempWorkspace("step1991_t4"); std::string altPath = ws + "/src/Alt.h"; { std::ofstream f(altPath); f << "namespace alt {\nstruct Alt {\n bool flag = false;\n};\n}\n"; } HeadlessEditorState state; state.workspaceRoot = ws; state.defaultLanguage = "cpp"; rpc(state, "openFile", {{"path", altPath}}); rpc(state, "saveBuffer", {{"path", altPath}}); rpc(state, "closeFile", {{"path", altPath}}); // Replace file content — existing sidecar nodes become stale { std::ofstream f(altPath); f << "// empty\n"; } auto reopenResp = rpc(state, "openFile", {{"path", altPath}}); assert(!reopenResp.contains("error")); assert(reopenResp["result"].contains("annotationsRestored")); std::cout << "Test 4 PASSED: stale annotations skipped without error\n"; ++passed; } // Test 5: openFile with no sidecar returns annotationsRestored=0 silently { std::string ws = makeTempWorkspace("step1991_t5"); std::string freshPath = ws + "/src/Fresh.h"; { std::ofstream f(freshPath); f << "struct Fresh { int v = 0; };\n"; } // Ensure no sidecar exists std::string sc = sidecarPath(ws, freshPath); if (fs::exists(sc)) fs::remove(sc); HeadlessEditorState state; state.workspaceRoot = ws; state.defaultLanguage = "cpp"; auto openResp = rpc(state, "openFile", {{"path", freshPath}}); assert(!openResp.contains("error")); assert(openResp["result"].contains("annotationsRestored")); assert(openResp["result"]["annotationsRestored"] == 0); std::cout << "Test 5 PASSED: openFile with no sidecar returns annotationsRestored=0\n"; ++passed; } std::cout << "\n" << passed << "/5 passed\n"; return passed == 5 ? 0 : 1; }