diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index adc9f00..bdc56aa 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1734,4 +1734,52 @@ target_link_libraries(step283_test PRIVATE tree_sitter_java tree_sitter_rust tree_sitter_go tree_sitter_org) +# Step 284: EnvironmentSpec schema & AST node +add_executable(step284_test tests/step284_test.cpp) +target_include_directories(step284_test PRIVATE src) +target_link_libraries(step284_test PRIVATE nlohmann_json::nlohmann_json) + +# Step 285: Capability vocabulary & validation +add_executable(step285_test tests/step285_test.cpp) +target_include_directories(step285_test PRIVATE src) +target_link_libraries(step285_test PRIVATE nlohmann_json::nlohmann_json) + +# Step 286: Environment-aware pipeline hooks +add_executable(step286_test tests/step286_test.cpp) +target_include_directories(step286_test PRIVATE src) +target_link_libraries(step286_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go + tree_sitter_org) + +# Step 287: Environment-aware lowering decisions +add_executable(step287_test tests/step287_test.cpp) +target_include_directories(step287_test PRIVATE src) +target_link_libraries(step287_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go + tree_sitter_org) + +# Step 288: Host boundary AST nodes +add_executable(step288_test tests/step288_test.cpp) +target_include_directories(step288_test PRIVATE src) +target_link_libraries(step288_test PRIVATE nlohmann_json::nlohmann_json) + +# Step 289: Phase 10e integration tests +add_executable(step289_test tests/step289_test.cpp) +target_include_directories(step289_test PRIVATE src) +target_link_libraries(step289_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go + tree_sitter_org) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/MCPServer.h b/editor/src/MCPServer.h index 90c556d..abb8b25 100644 --- a/editor/src/MCPServer.h +++ b/editor/src/MCPServer.h @@ -1161,6 +1161,81 @@ private: }; } + // --------------------------------------------------------------- + // Step 286: Environment layer tools + // --------------------------------------------------------------- + void registerEnvironmentTools() { + // whetstone_set_environment + tools_.push_back({"whetstone_set_environment", + "Set the environment spec on the active module. Defines " + "scheduler, memory model, capabilities, constraints, " + "exception model, and FFI style. Replaces any existing env.", + {{"type", "object"}, {"properties", { + {"envId", {{"type", "string"}, + {"description", "Environment identifier (e.g. posix_process, browser, jvm)"}}}, + {"scheduler", {{"type", "string"}, + {"enum", {"event_loop", "threads", "fibers", "coroutines", "single_thread"}}, + {"description", "Scheduling model"}}}, + {"memory", {{"type", "string"}, + {"enum", {"manual", "raii", "refcount", "tracing_gc", "region"}}, + {"description", "Memory management model"}}}, + {"capabilities", {{"type", "array"}, {"items", {{"type", "string"}}}, + {"description", "Available capabilities (io.fs, io.net, threads, etc.)"}}}, + {"constraints", {{"type", "array"}, {"items", {{"type", "string"}}}, + {"description", "Environment constraints (no_jit, no_threads, etc.)"}}}, + {"exceptions", {{"type", "string"}, + {"enum", {"unwind", "checked", "typed", "untyped"}}, + {"description", "Exception handling model"}}}, + {"ffi", {{"type", "string"}, + {"enum", {"c_abi", "dynamic_linking", "none"}}, + {"description", "Foreign function interface style"}}} + }}, {"required", {"envId"}}} + }); + toolHandlers_["whetstone_set_environment"] = + [this](const json& args) { + return callWhetstone("setEnvironment", args); + }; + + // whetstone_get_environment + tools_.push_back({"whetstone_get_environment", + "Get the current environment spec from the active module. " + "Returns hasEnvironment=false if none is set.", + {{"type", "object"}, {"properties", json::object()}} + }); + toolHandlers_["whetstone_get_environment"] = + [this](const json& args) { + return callWhetstone("getEnvironment", args); + }; + + // whetstone_validate_environment + tools_.push_back({"whetstone_validate_environment", + "Validate the active module against its environment spec. " + "Checks capability requirements (E0501) and annotation " + "compatibility (E0502-E0505). Returns diagnostics array.", + {{"type", "object"}, {"properties", json::object()}} + }); + toolHandlers_["whetstone_validate_environment"] = + [this](const json& args) { + return callWhetstone("validateEnvironment", args); + }; + + // whetstone_get_lowering_hints + tools_.push_back({"whetstone_get_lowering_hints", + "Get environment-aware lowering hints for a node. Returns " + "code generation patterns based on scheduler, memory, and " + "exception models (e.g. callback, std_async, try_catch).", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, + {"description", + "Node ID (optional, defaults to module root)"}}} + }}} + }); + toolHandlers_["whetstone_get_lowering_hints"] = + [this](const json& args) { + return callWhetstone("getLoweringHints", args); + }; + } + void registerWhetstoneTools() { registerASTTools(); registerAnnotationTools(); @@ -1171,5 +1246,6 @@ private: registerSaveUndoTools(); registerSidecarTools(); registerSemanticAnnotationTools(); + registerEnvironmentTools(); } }; diff --git a/editor/tests/step285_test.cpp b/editor/tests/step285_test.cpp index 00f8e95..9b727f0 100644 --- a/editor/tests/step285_test.cpp +++ b/editor/tests/step285_test.cpp @@ -7,6 +7,9 @@ #include "ast/Function.h" #include "ast/Serialization.h" #include "EnvironmentSpec.h" +#include "ASTUtils.h" +#include "CompactAST.h" +#include "SidecarPersistence.h" #include using json = nlohmann::json; diff --git a/editor/tests/step286_test.cpp b/editor/tests/step286_test.cpp new file mode 100644 index 0000000..9477a09 --- /dev/null +++ b/editor/tests/step286_test.cpp @@ -0,0 +1,304 @@ +// Step 286: Environment-Aware Pipeline Hooks (12 tests) +// +// Tests that validateEnvAnnotations detects incompatible annotations, +// that the RPC validateEnvironment method returns combined diagnostics, +// and that MCP environment tools are properly registered. +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" +#include "ast/Serialization.h" +#include "EnvironmentSpec.h" +#include "HeadlessEditorState.h" +#include "MCPServer.h" +#include + +using json = nlohmann::json; + +static int passed = 0, failed = 0; +static void check(bool c, const std::string& n) { + if (c) { std::cout << " PASS: " << n << "\n"; ++passed; } + else { std::cout << " FAIL: " << n << "\n"; ++failed; } +} + +// Helper: build a module with env + annotated function +static Module* makeEnvModule(const std::string& scheduler, + const std::string& memory) { + auto* mod = new Module(); mod->id = "mod1"; mod->name = "envtest"; + auto* env = new EnvironmentSpec(); + env->envId = "test_env"; + env->scheduler = scheduler; + env->memory = memory; + mod->addChild("environment", env); + return mod; +} + +// Test 1: @Parallel in single_thread → E0502 +static void test_parallel_single_thread() { + auto* mod = makeEnvModule("single_thread", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "doWork"; + auto* pa = new ParallelAnnotation(); + pa->kind = "data"; + fn->addChild("annotations", pa); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.size() == 1, "1 diagnostic for @Parallel in single_thread"); + check(diags[0].message.find("E0502") != std::string::npos, "E0502 error code"); + check(diags[0].severity == "error", "severity is error"); + delete mod; +} + +// Test 2: @ThreadModel in single_thread → E0503 +static void test_threadmodel_single_thread() { + auto* mod = makeEnvModule("single_thread", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "spawn"; + auto* tm = new ThreadModelAnnotation(); + tm->model = "os"; + fn->addChild("annotations", tm); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.size() == 1, "1 diagnostic for @ThreadModel in single_thread"); + check(diags[0].message.find("E0503") != std::string::npos, "E0503 error code"); + delete mod; +} + +// Test 3: @Atomic in single_thread → E0504 warning +static void test_atomic_single_thread() { + auto* mod = makeEnvModule("single_thread", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "inc"; + auto* at = new AtomicAnnotation(); + at->consistency = "seq_cst"; + fn->addChild("annotations", at); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.size() == 1, "1 diagnostic for @Atomic in single_thread"); + check(diags[0].severity == "warning", "severity is warning (not error)"); + check(diags[0].message.find("E0504") != std::string::npos, "E0504 code"); + delete mod; +} + +// Test 4: @Exec(async) in single_thread → E0505 +static void test_exec_async_single_thread() { + auto* mod = makeEnvModule("single_thread", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "fetch"; + auto* ea = new ExecAnnotation(); + ea->mode = "async"; + fn->addChild("annotations", ea); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.size() == 1, "1 diagnostic for @Exec(async) in single_thread"); + check(diags[0].message.find("E0505") != std::string::npos, "E0505 code"); + delete mod; +} + +// Test 5: No diagnostics when scheduler is compatible +static void test_compatible_scheduler() { + auto* mod = makeEnvModule("threads", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "doWork"; + auto* pa = new ParallelAnnotation(); + pa->kind = "data"; + fn->addChild("annotations", pa); + auto* ea = new ExecAnnotation(); + ea->mode = "async"; + fn->addChild("annotations", ea); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.empty(), "no diagnostics with threads scheduler"); + delete mod; +} + +// Test 6: Multiple incompatible annotations → multiple diagnostics +static void test_multiple_incompatible() { + auto* mod = makeEnvModule("single_thread", "manual"); + auto* fn = new Function(); fn->id = "f1"; fn->name = "multi"; + auto* pa = new ParallelAnnotation(); pa->kind = "task"; + fn->addChild("annotations", pa); + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fn->addChild("annotations", ea); + auto* at = new AtomicAnnotation(); at->consistency = "relaxed"; + fn->addChild("annotations", at); + mod->addChild("functions", fn); + + auto* env = static_cast(mod->getChildren("environment")[0]); + auto diags = validateEnvAnnotations(mod, env); + check(diags.size() == 3, "3 diagnostics for 3 incompatible annotations"); + delete mod; +} + +// Test 7: validateEnvironment RPC combines cap + annotation diagnostics +static void test_rpc_validate_environment() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", + "def worker():\n pass\n", "python"); + + // Set environment + json setReq = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "single_thread_env"}, + {"scheduler", "single_thread"}, + {"memory", "manual"}, + {"capabilities", json::array({"io.fs"})}}}}; + state.processAgentRequest(setReq, "s1"); + + // Add @Parallel annotation via mutation + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + if (!fns.empty()) { + auto* pa = new ParallelAnnotation(); pa->kind = "data"; + fns[0]->addChild("annotations", pa); + } + + // Validate + json valReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "validateEnvironment"}}; + json resp = state.processAgentRequest(valReq, "s1"); + check(resp.contains("result"), "validateEnvironment returns result"); + auto diagsArr = resp["result"]["diagnostics"]; + check(diagsArr.is_array(), "diagnostics is array"); + check(resp["result"]["count"].get() >= 1, + "at least 1 diagnostic"); + // Should have E0502 for @Parallel in single_thread + bool foundE0502 = false; + for (const auto& d : diagsArr) { + if (d["message"].get().find("E0502") != std::string::npos) + foundE0502 = true; + } + check(foundE0502, "E0502 found in validateEnvironment response"); +} + +// Test 8: setEnvironment + getEnvironment RPC roundtrip +static void test_rpc_set_get_environment() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", "x = 1\n", "python"); + + json setReq = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "browser"}, {"scheduler", "event_loop"}, + {"memory", "tracing_gc"}, {"exceptions", "untyped"}}}}; + json setResp = state.processAgentRequest(setReq, "s1"); + check(setResp["result"]["success"] == true, "setEnvironment success"); + check(setResp["result"]["envId"] == "browser", "envId in response"); + + json getReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getEnvironment"}}; + json getResp = state.processAgentRequest(getReq, "s1"); + check(getResp["result"]["hasEnvironment"] == true, "hasEnvironment true"); + check(getResp["result"]["envId"] == "browser", "retrieved envId"); + check(getResp["result"]["scheduler"] == "event_loop", "scheduler matches"); +} + +// Test 9: getEnvironment with no env set +static void test_rpc_get_no_environment() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", "x = 1\n", "python"); + + json req = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "getEnvironment"}}; + json resp = state.processAgentRequest(req, "s1"); + check(resp["result"]["hasEnvironment"] == false, "hasEnvironment false when no env"); +} + +// Test 10: Linter role can read but not write environment +static void test_linter_permission() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Linter; + state.openBuffer("test.py", "x = 1\n", "python"); + + // Linter cannot setEnvironment + json setReq = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "test"}}}}; + json setResp = state.processAgentRequest(setReq, "s1"); + check(setResp.contains("error"), "Linter denied setEnvironment"); + + // Linter can getEnvironment + json getReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getEnvironment"}}; + json getResp = state.processAgentRequest(getReq, "s1"); + check(!getResp.contains("error") || getResp.contains("result"), + "Linter can getEnvironment"); +} + +// Test 11: MCP environment tools registered +static void test_mcp_env_tools() { + MCPServer mcp; + const auto& tools = mcp.getTools(); + bool foundSetEnv = false, foundGetEnv = false, + foundValidate = false, foundLowering = false; + for (const auto& t : tools) { + if (t.name == "whetstone_set_environment") foundSetEnv = true; + if (t.name == "whetstone_get_environment") foundGetEnv = true; + if (t.name == "whetstone_validate_environment") foundValidate = true; + if (t.name == "whetstone_get_lowering_hints") foundLowering = true; + } + check(foundSetEnv, "whetstone_set_environment registered"); + check(foundGetEnv, "whetstone_get_environment registered"); + check(foundValidate, "whetstone_validate_environment registered"); + check(foundLowering, "whetstone_get_lowering_hints registered"); +} + +// Test 12: setEnvironment replaces existing env +static void test_replace_environment() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", "x = 1\n", "python"); + + // Set first env + json set1 = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "posix"}, {"scheduler", "threads"}}}}; + state.processAgentRequest(set1, "s1"); + + // Replace with second env + json set2 = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "setEnvironment"}, + {"params", {{"envId", "browser"}, {"scheduler", "event_loop"}}}}; + state.processAgentRequest(set2, "s1"); + + // Verify only the new one exists + json getReq = {{"jsonrpc", "2.0"}, {"id", 3}, + {"method", "getEnvironment"}}; + json resp = state.processAgentRequest(getReq, "s1"); + check(resp["result"]["envId"] == "browser", "env replaced to browser"); + check(resp["result"]["scheduler"] == "event_loop", + "scheduler replaced to event_loop"); + + // Verify only 1 environment child + Module* ast = state.activeAST(); + check(ast->getChildren("environment").size() == 1, + "only 1 EnvironmentSpec after replacement"); +} + +int main() { + std::cout << "=== Step 286: Environment-Aware Pipeline Hooks ===\n"; + test_parallel_single_thread(); + test_threadmodel_single_thread(); + test_atomic_single_thread(); + test_exec_async_single_thread(); + test_compatible_scheduler(); + test_multiple_incompatible(); + test_rpc_validate_environment(); + test_rpc_set_get_environment(); + test_rpc_get_no_environment(); + test_linter_permission(); + test_mcp_env_tools(); + test_replace_environment(); + std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n"; + return failed > 0 ? 1 : 0; +} diff --git a/editor/tests/step287_test.cpp b/editor/tests/step287_test.cpp new file mode 100644 index 0000000..fd2af0e --- /dev/null +++ b/editor/tests/step287_test.cpp @@ -0,0 +1,288 @@ +// Step 287: Environment-Aware Lowering Decisions (12 tests) +// +// Tests getLoweringHints — environment-aware code generation patterns +// based on scheduler, memory, and exception models. Also tests +// the getLoweringHints RPC method through HeadlessEditorState. +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" +#include "ast/Serialization.h" +#include "EnvironmentSpec.h" +#include "HeadlessEditorState.h" +#include + +using json = nlohmann::json; + +static int passed = 0, failed = 0; +static void check(bool c, const std::string& n) { + if (c) { std::cout << " PASS: " << n << "\n"; ++passed; } + else { std::cout << " FAIL: " << n << "\n"; ++failed; } +} + +// Test 1: @Exec(async) + event_loop → callback pattern +static void test_async_event_loop() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "fetch"; + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fn->addChild("annotations", ea); + + EnvironmentSpec env; + env.scheduler = "event_loop"; + env.memory = "tracing_gc"; + env.exceptions = "untyped"; + + auto hints = getLoweringHints(fn, &env); + bool foundCallback = false; + for (const auto& h : hints) + if (h.pattern == "callback") foundCallback = true; + check(foundCallback, "async + event_loop → callback pattern"); + delete fn; +} + +// Test 2: @Exec(async) + threads → std_async pattern +static void test_async_threads() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "compute"; + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fn->addChild("annotations", ea); + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "manual"; + env.exceptions = "unwind"; + + auto hints = getLoweringHints(fn, &env); + bool foundStdAsync = false; + for (const auto& h : hints) + if (h.pattern == "std_async") foundStdAsync = true; + check(foundStdAsync, "async + threads → std_async pattern"); + delete fn; +} + +// Test 3: @Exec(async) + coroutines → coroutine pattern +static void test_async_coroutines() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "stream"; + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fn->addChild("annotations", ea); + + EnvironmentSpec env; + env.scheduler = "coroutines"; + env.memory = "raii"; + env.exceptions = "typed"; + + auto hints = getLoweringHints(fn, &env); + bool foundCoroutine = false; + for (const auto& h : hints) + if (h.pattern == "coroutine") foundCoroutine = true; + check(foundCoroutine, "async + coroutines → coroutine pattern"); + delete fn; +} + +// Test 4: tracing_gc → suppress_dealloc +static void test_gc_suppress_dealloc() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "alloc"; + + EnvironmentSpec env; + env.scheduler = "event_loop"; + env.memory = "tracing_gc"; + + auto hints = getLoweringHints(fn, &env); + bool found = false; + for (const auto& h : hints) + if (h.pattern == "suppress_dealloc") found = true; + check(found, "tracing_gc → suppress_dealloc"); + delete fn; +} + +// Test 5: manual memory → explicit_delete +static void test_manual_explicit_delete() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "alloc"; + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "manual"; + + auto hints = getLoweringHints(fn, &env); + bool found = false; + for (const auto& h : hints) + if (h.pattern == "explicit_delete") found = true; + check(found, "manual → explicit_delete"); + delete fn; +} + +// Test 6: RAII → raii_cleanup +static void test_raii_cleanup() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "resource"; + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "raii"; + + auto hints = getLoweringHints(fn, &env); + bool found = false; + for (const auto& h : hints) + if (h.pattern == "raii_cleanup") found = true; + check(found, "raii → raii_cleanup"); + delete fn; +} + +// Test 7: unwind exceptions → try_catch +static void test_unwind_try_catch() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "parse"; + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "manual"; + env.exceptions = "unwind"; + + auto hints = getLoweringHints(fn, &env); + bool found = false; + for (const auto& h : hints) + if (h.pattern == "try_catch") found = true; + check(found, "unwind → try_catch"); + delete fn; +} + +// Test 8: typed exceptions → expected +static void test_typed_expected() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "safe"; + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "raii"; + env.exceptions = "typed"; + + auto hints = getLoweringHints(fn, &env); + bool found = false; + for (const auto& h : hints) + if (h.pattern == "expected") found = true; + check(found, "typed → expected (std::expected)"); + delete fn; +} + +// Test 9: Combined hints — async + manual + unwind +static void test_combined_hints() { + auto* fn = new Function(); fn->id = "f1"; fn->name = "combo"; + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fn->addChild("annotations", ea); + + EnvironmentSpec env; + env.scheduler = "threads"; + env.memory = "manual"; + env.exceptions = "unwind"; + + auto hints = getLoweringHints(fn, &env); + // Should get: std_async + explicit_delete + try_catch + check(hints.size() >= 3, "at least 3 hints for combined env"); + + bool hasAsync = false, hasDelete = false, hasTryCatch = false; + for (const auto& h : hints) { + if (h.pattern == "std_async") hasAsync = true; + if (h.pattern == "explicit_delete") hasDelete = true; + if (h.pattern == "try_catch") hasTryCatch = true; + } + check(hasAsync && hasDelete && hasTryCatch, + "all 3 patterns present in combined hints"); + delete fn; +} + +// Test 10: getLoweringHints RPC method +static void test_rpc_lowering_hints() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", + "def worker():\n pass\n", "python"); + + // Set environment + json setReq = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "posix"}, {"scheduler", "threads"}, + {"memory", "manual"}, {"exceptions", "unwind"}}}}; + state.processAgentRequest(setReq, "s1"); + + // Add @Exec(async) to worker function + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + if (!fns.empty()) { + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fns[0]->addChild("annotations", ea); + } + + // Get lowering hints for the function + std::string fnId = fns.empty() ? "" : fns[0]->id; + json hintReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getLoweringHints"}, + {"params", {{"nodeId", fnId}}}}; + json resp = state.processAgentRequest(hintReq, "s1"); + check(resp.contains("result"), "getLoweringHints returns result"); + check(resp["result"]["hints"].is_array(), "hints is array"); + check(resp["result"]["count"].get() >= 1, "at least 1 hint"); + + // Should include std_async for threads+async + bool foundStdAsync = false; + for (const auto& h : resp["result"]["hints"]) { + if (h["pattern"] == "std_async") foundStdAsync = true; + } + check(foundStdAsync, "RPC hints include std_async"); +} + +// Test 11: getLoweringHints with no nodeId → module-level hints +static void test_rpc_lowering_module_level() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("test.py", "x = 1\n", "python"); + + json setReq = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "browser"}, {"scheduler", "event_loop"}, + {"memory", "tracing_gc"}, {"exceptions", "untyped"}}}}; + state.processAgentRequest(setReq, "s1"); + + // No nodeId → module-level + json hintReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getLoweringHints"}, + {"params", json::object()}}; + json resp = state.processAgentRequest(hintReq, "s1"); + check(resp.contains("result"), "module-level hints returns result"); + // Should get suppress_dealloc from tracing_gc + bool foundSuppressDealloc = false; + for (const auto& h : resp["result"]["hints"]) { + if (h["pattern"] == "suppress_dealloc") foundSuppressDealloc = true; + } + check(foundSuppressDealloc, "module-level includes suppress_dealloc"); +} + +// Test 12: null safety +static void test_null_safety() { + auto hints1 = getLoweringHints(nullptr, nullptr); + check(hints1.empty(), "null node + null env → empty"); + + auto* fn = new Function(); fn->id = "f1"; + auto hints2 = getLoweringHints(fn, nullptr); + check(hints2.empty(), "null env → empty"); + + EnvironmentSpec env; + auto hints3 = getLoweringHints(nullptr, &env); + check(hints3.empty(), "null node → empty"); + delete fn; +} + +int main() { + std::cout << "=== Step 287: Environment-Aware Lowering Decisions ===\n"; + test_async_event_loop(); + test_async_threads(); + test_async_coroutines(); + test_gc_suppress_dealloc(); + test_manual_explicit_delete(); + test_raii_cleanup(); + test_unwind_try_catch(); + test_typed_expected(); + test_combined_hints(); + test_rpc_lowering_hints(); + test_rpc_lowering_module_level(); + test_null_safety(); + std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n"; + return failed > 0 ? 1 : 0; +} diff --git a/editor/tests/step288_test.cpp b/editor/tests/step288_test.cpp new file mode 100644 index 0000000..71bae96 --- /dev/null +++ b/editor/tests/step288_test.cpp @@ -0,0 +1,236 @@ +// Step 288: Host Boundary AST Nodes (12 tests) +// +// Tests HostCall, ScheduleTask, ModuleLoad AST node construction, +// JSON roundtrip serialization, capability validation, and compact +// AST integration. +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/HostBoundary.h" +#include "ast/Serialization.h" +#include "ASTUtils.h" +#include "CompactAST.h" +#include "EnvironmentSpec.h" +#include + +using json = nlohmann::json; + +static int passed = 0, failed = 0; +static void check(bool c, const std::string& n) { + if (c) { std::cout << " PASS: " << n << "\n"; ++passed; } + else { std::cout << " FAIL: " << n << "\n"; ++failed; } +} + +// Test 1: HostCall default construction +static void test_hostcall_default() { + HostCall hc; + check(hc.conceptType == "HostCall", "conceptType is HostCall"); + check(hc.capability.empty(), "default capability empty"); + check(hc.name.empty(), "default name empty"); +} + +// Test 2: HostCall parameterized construction +static void test_hostcall_params() { + HostCall hc("io.fs", "readFile"); + check(hc.conceptType == "HostCall", "conceptType is HostCall"); + check(hc.capability == "io.fs", "capability set to io.fs"); + check(hc.name == "readFile", "name set to readFile"); +} + +// Test 3: HostCall JSON roundtrip +static void test_hostcall_roundtrip() { + auto* hc = new HostCall("io.net", "listen"); + hc->id = "hc1"; + + json j = toJson(hc); + check(j["concept"] == "HostCall", "JSON concept"); + check(j["properties"]["capability"] == "io.net", "JSON capability"); + check(j["properties"]["name"] == "listen", "JSON name"); + + ASTNode* restored = fromJson(j); + check(restored != nullptr, "fromJson non-null"); + check(restored->conceptType == "HostCall", "restored type"); + auto* rhc = static_cast(restored); + check(rhc->capability == "io.net", "roundtrip capability"); + check(rhc->name == "listen", "roundtrip name"); + delete hc; + delete restored; +} + +// Test 4: ScheduleTask default construction +static void test_schedule_default() { + ScheduleTask st; + check(st.conceptType == "ScheduleTask", "conceptType is ScheduleTask"); + check(st.queue.empty(), "default queue empty"); +} + +// Test 5: ScheduleTask JSON roundtrip +static void test_schedule_roundtrip() { + auto* st = new ScheduleTask(); + st->id = "st1"; + st->queue = "microtask"; + + json j = toJson(st); + check(j["concept"] == "ScheduleTask", "JSON concept"); + check(j["properties"]["queue"] == "microtask", "JSON queue"); + + ASTNode* restored = fromJson(j); + check(restored != nullptr, "fromJson non-null"); + auto* rst = static_cast(restored); + check(rst->queue == "microtask", "roundtrip queue"); + delete st; + delete restored; +} + +// Test 6: ModuleLoad default construction +static void test_moduleload_default() { + ModuleLoad ml; + check(ml.conceptType == "ModuleLoad", "conceptType is ModuleLoad"); + check(ml.moduleName.empty(), "default moduleName empty"); + check(ml.mode.empty(), "default mode empty"); +} + +// Test 7: ModuleLoad JSON roundtrip +static void test_moduleload_roundtrip() { + auto* ml = new ModuleLoad(); + ml->id = "ml1"; + ml->moduleName = "math"; + ml->mode = "static"; + + json j = toJson(ml); + check(j["concept"] == "ModuleLoad", "JSON concept"); + check(j["properties"]["moduleName"] == "math", "JSON moduleName"); + check(j["properties"]["mode"] == "static", "JSON mode"); + + ASTNode* restored = fromJson(j); + check(restored != nullptr, "fromJson non-null"); + auto* rml = static_cast(restored); + check(rml->moduleName == "math", "roundtrip moduleName"); + check(rml->mode == "static", "roundtrip mode"); + delete ml; + delete restored; +} + +// Test 8: createNode for all host boundary types +static void test_create_nodes() { + ASTNode* hc = createNode("HostCall"); + check(hc != nullptr && hc->conceptType == "HostCall", + "createNode HostCall"); + delete hc; + + ASTNode* st = createNode("ScheduleTask"); + check(st != nullptr && st->conceptType == "ScheduleTask", + "createNode ScheduleTask"); + delete st; + + ASTNode* ml = createNode("ModuleLoad"); + check(ml != nullptr && ml->conceptType == "ModuleLoad", + "createNode ModuleLoad"); + delete ml; +} + +// Test 9: HostCall in function body AST +static void test_hostcall_in_function() { + auto* mod = new Module(); mod->id = "mod1"; mod->name = "test"; + auto* fn = new Function(); fn->id = "f1"; fn->name = "readConfig"; + auto* hc = new HostCall("io.fs", "readFile"); + hc->id = "hc1"; + fn->addChild("body", hc); + mod->addChild("functions", fn); + + json j = toJson(mod); + ASTNode* restored = fromJson(j); + auto fns = restored->getChildren("functions"); + check(fns.size() == 1, "restored has 1 function"); + auto body = fns[0]->getChildren("body"); + check(body.size() == 1, "function has 1 body node"); + check(body[0]->conceptType == "HostCall", "body node is HostCall"); + auto* rhc = static_cast(body[0]); + check(rhc->capability == "io.fs", "nested HostCall capability"); + check(rhc->name == "readFile", "nested HostCall name"); + delete mod; + delete restored; +} + +// Test 10: Compact AST getNodeName for host boundary nodes +static void test_compact_node_names() { + auto* hc = new HostCall("io.fs", "readFile"); + hc->id = "hc1"; + std::string hcName = getNodeName(hc); + check(hcName == "readFile", "HostCall node name is readFile"); + + auto* st = new ScheduleTask(); + st->id = "st1"; st->queue = "microtask"; + std::string stName = getNodeName(st); + // ScheduleTask may return queue as name or empty — just check no crash + check(!stName.empty() || stName.empty(), "ScheduleTask getNodeName no crash"); + + auto* ml = new ModuleLoad(); + ml->id = "ml1"; ml->moduleName = "math"; + std::string mlName = getNodeName(ml); + check(mlName == "math", "ModuleLoad node name is math"); + + delete hc; delete st; delete ml; +} + +// Test 11: HostCall capability validation against env +static void test_hostcall_cap_validation() { + auto* mod = new Module(); mod->id = "mod1"; mod->name = "test"; + auto* env = new EnvironmentSpec(); + env->envId = "restricted"; + env->capabilities = {"io.fs"}; + mod->addChild("environment", env); + + auto* fn = new Function(); fn->id = "f1"; fn->name = "serve"; + auto* cr = new CapabilityRequirement(); + cr->capability = "io.net"; cr->required = true; + fn->addChild("annotations", cr); + mod->addChild("functions", fn); + + auto diags = validateCapabilities(mod, env); + check(diags.size() == 1, "1 diag for missing io.net"); + check(diags[0].message.find("E0501") != std::string::npos, + "E0501 for missing capability"); + delete mod; +} + +// Test 12: ScheduleTask with body children +static void test_schedule_with_body() { + auto* st = new ScheduleTask(); + st->id = "st1"; + st->queue = "thread"; + + auto* hc = new HostCall("io.fs", "writeFile"); + hc->id = "hc1"; + st->addChild("body", hc); + + json j = toJson(st); + ASTNode* restored = fromJson(j); + check(restored != nullptr, "roundtrip non-null"); + auto body = restored->getChildren("body"); + check(body.size() == 1, "ScheduleTask has 1 body child"); + check(body[0]->conceptType == "HostCall", "body child is HostCall"); + delete st; + delete restored; +} + +int main() { + std::cout << "=== Step 288: Host Boundary AST Nodes ===\n"; + test_hostcall_default(); + test_hostcall_params(); + test_hostcall_roundtrip(); + test_schedule_default(); + test_schedule_roundtrip(); + test_moduleload_default(); + test_moduleload_roundtrip(); + test_create_nodes(); + test_hostcall_in_function(); + test_compact_node_names(); + test_hostcall_cap_validation(); + test_schedule_with_body(); + std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n"; + return failed > 0 ? 1 : 0; +} diff --git a/editor/tests/step289_test.cpp b/editor/tests/step289_test.cpp new file mode 100644 index 0000000..ca2400e --- /dev/null +++ b/editor/tests/step289_test.cpp @@ -0,0 +1,314 @@ +// Step 289: Phase 10e Environment Layer Integration Tests (8 tests) +// +// End-to-end integration tests exercising the full environment layer: +// EnvironmentSpec, capability validation, annotation compatibility, +// lowering hints, host boundary nodes, and MCP tool counts. +#include +#include +#include +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Annotation.h" +#include "ast/HostBoundary.h" +#include "ast/Serialization.h" +#include "EnvironmentSpec.h" +#include "ASTUtils.h" +#include "CompactAST.h" +#include "HeadlessEditorState.h" +#include "MCPServer.h" +#include + +using json = nlohmann::json; + +static int passed = 0, failed = 0; +static void check(bool c, const std::string& n) { + if (c) { std::cout << " PASS: " << n << "\n"; ++passed; } + else { std::cout << " FAIL: " << n << "\n"; ++failed; } +} + +// Test 1: Full workflow — setEnvironment → add annotations → validate → getLoweringHints +static void test_full_env_workflow() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("app.py", + "def fetch_data():\n pass\n" + "def process():\n pass\n", "python"); + + // 1. Set environment + json setEnv = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "posix_server"}, {"scheduler", "threads"}, + {"memory", "manual"}, {"exceptions", "unwind"}, + {"capabilities", json::array({"io.fs", "io.net", "threads"})}}}}; + json envResp = state.processAgentRequest(setEnv, "s1"); + check(envResp["result"]["success"] == true, "environment set"); + + // 2. Add @Exec(async) annotation to fetch_data + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + check(fns.size() == 2, "2 functions parsed"); + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fns[0]->addChild("annotations", ea); + + // 3. Validate environment + json valReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "validateEnvironment"}}; + json valResp = state.processAgentRequest(valReq, "s1"); + check(valResp["result"]["count"].get() == 0, + "0 diagnostics (threads supports async)"); + + // 4. Get lowering hints for fetch_data + json hintReq = {{"jsonrpc", "2.0"}, {"id", 3}, + {"method", "getLoweringHints"}, + {"params", {{"nodeId", fns[0]->id}}}}; + json hintResp = state.processAgentRequest(hintReq, "s1"); + check(hintResp["result"]["count"].get() >= 3, + "multiple hints (std_async + explicit_delete + try_catch)"); +} + +// Test 2: Incompatible env detects issues and changing env clears them +static void test_env_switch_clears_issues() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("ui.py", + "def render():\n pass\n", "python"); + + // Set single_thread env + json setEnv1 = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "embedded"}, {"scheduler", "single_thread"}, + {"memory", "manual"}}}}; + state.processAgentRequest(setEnv1, "s1"); + + // Add @Parallel (incompatible with single_thread) + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + auto* pa = new ParallelAnnotation(); pa->kind = "data"; + fns[0]->addChild("annotations", pa); + + // Validate — should fail + json valReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "validateEnvironment"}}; + json val1 = state.processAgentRequest(valReq, "s1"); + check(val1["result"]["count"].get() >= 1, + "at least 1 diagnostic in single_thread"); + + // Switch to threads env + json setEnv2 = {{"jsonrpc", "2.0"}, {"id", 3}, + {"method", "setEnvironment"}, + {"params", {{"envId", "server"}, {"scheduler", "threads"}, + {"memory", "manual"}}}}; + state.processAgentRequest(setEnv2, "s1"); + + // Validate again — should pass + json val2 = state.processAgentRequest(valReq, "s1"); + check(val2["result"]["count"].get() == 0, + "0 diagnostics after switching to threads"); +} + +// Test 3: Host boundary nodes in a function body survive AST roundtrip +static void test_host_boundary_roundtrip() { + auto* mod = new Module(); mod->id = "mod1"; mod->name = "server"; + auto* fn = new Function(); fn->id = "f1"; fn->name = "handler"; + auto* hc = new HostCall("io.net", "accept"); + hc->id = "hc1"; + fn->addChild("body", hc); + auto* st = new ScheduleTask(); + st->id = "st1"; st->queue = "thread"; + fn->addChild("body", st); + auto* ml = new ModuleLoad(); + ml->id = "ml1"; ml->moduleName = "crypto"; ml->mode = "dynamic"; + fn->addChild("body", ml); + mod->addChild("functions", fn); + + // Full AST roundtrip + json j = toJson(mod); + ASTNode* restored = fromJson(j); + auto rFns = restored->getChildren("functions"); + check(rFns.size() == 1, "1 function restored"); + auto body = rFns[0]->getChildren("body"); + check(body.size() == 3, "3 body nodes restored"); + check(body[0]->conceptType == "HostCall", "body[0] is HostCall"); + check(body[1]->conceptType == "ScheduleTask", "body[1] is ScheduleTask"); + check(body[2]->conceptType == "ModuleLoad", "body[2] is ModuleLoad"); + delete mod; + delete restored; +} + +// Test 4: CapabilityRequirement + EnvironmentSpec validation end-to-end +static void test_capability_validation_e2e() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("net.py", + "def serve():\n pass\n", "python"); + + // Set env without io.net + json setEnv = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "no_net"}, {"scheduler", "threads"}, + {"capabilities", json::array({"io.fs"})}}}}; + state.processAgentRequest(setEnv, "s1"); + + // Add CapabilityRequirement for io.net + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + auto* cr = new CapabilityRequirement(); + cr->capability = "io.net"; cr->required = true; + fns[0]->addChild("annotations", cr); + + // Validate + json valReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "validateEnvironment"}}; + json resp = state.processAgentRequest(valReq, "s1"); + bool foundE0501 = false; + for (const auto& d : resp["result"]["diagnostics"]) { + if (d["message"].get().find("E0501") != std::string::npos) + foundE0501 = true; + } + check(foundE0501, "E0501 for unmet io.net capability"); +} + +// Test 5: Lowering hints change when environment changes +static void test_lowering_hints_change_with_env() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("async.py", + "def fetch():\n pass\n", "python"); + + Module* ast = state.activeAST(); + auto fns = ast->getChildren("functions"); + auto* ea = new ExecAnnotation(); ea->mode = "async"; + fns[0]->addChild("annotations", ea); + + // Set event_loop env + json setEnv1 = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "browser"}, {"scheduler", "event_loop"}, + {"memory", "tracing_gc"}}}}; + state.processAgentRequest(setEnv1, "s1"); + + json hintReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "getLoweringHints"}, + {"params", {{"nodeId", fns[0]->id}}}}; + json hints1 = state.processAgentRequest(hintReq, "s1"); + + bool hasCallback = false; + for (const auto& h : hints1["result"]["hints"]) + if (h["pattern"] == "callback") hasCallback = true; + check(hasCallback, "event_loop env → callback pattern"); + + // Switch to threads env + json setEnv2 = {{"jsonrpc", "2.0"}, {"id", 3}, + {"method", "setEnvironment"}, + {"params", {{"envId", "server"}, {"scheduler", "threads"}, + {"memory", "manual"}}}}; + state.processAgentRequest(setEnv2, "s1"); + + json hints2 = state.processAgentRequest(hintReq, "s1"); + bool hasStdAsync = false; + for (const auto& h : hints2["result"]["hints"]) + if (h["pattern"] == "std_async") hasStdAsync = true; + check(hasStdAsync, "threads env → std_async pattern"); +} + +// Test 6: Batch query with environment methods +static void test_batch_env_queries() { + HeadlessEditorState state; + state.agent.defaultRole = AgentRole::Refactor; + state.openBuffer("batch.py", + "def work():\n pass\n", "python"); + + // Set env + json setEnv = {{"jsonrpc", "2.0"}, {"id", 1}, + {"method", "setEnvironment"}, + {"params", {{"envId", "jvm"}, {"scheduler", "threads"}, + {"memory", "tracing_gc"}, {"exceptions", "unwind"}}}}; + state.processAgentRequest(setEnv, "s1"); + + // Batch query: getEnvironment + validateEnvironment + getLoweringHints + json batchReq = {{"jsonrpc", "2.0"}, {"id", 2}, + {"method", "batchQuery"}, + {"params", {{"queries", json::array({ + {{"method", "getEnvironment"}}, + {{"method", "validateEnvironment"}}, + {{"method", "getLoweringHints"}, {"params", json::object()}} + })}}}}; + json resp = state.processAgentRequest(batchReq, "s1"); + check(resp.contains("result"), "batchQuery returns result"); + auto results = resp["result"]["results"]; + check(results.is_array(), "results is array"); + check(results.size() == 3, "3 sub-query results"); + + // Check getEnvironment result + check(results[0]["result"]["envId"] == "jvm", + "batch[0] getEnvironment returns jvm"); + // Check validateEnvironment result + check(results[1]["result"].contains("diagnostics"), + "batch[1] validateEnvironment has diagnostics"); + // Check getLoweringHints result + check(results[2]["result"].contains("hints"), + "batch[2] getLoweringHints has hints"); +} + +// Test 7: MCP tool count includes environment tools +static void test_mcp_tool_count() { + MCPServer mcp; + const auto& tools = mcp.getTools(); + // Previous: 34 tools from Sprint 9 + sidecar + annotation + // + 4 new environment tools = 38 + int envToolCount = 0; + for (const auto& t : tools) { + if (t.name.find("environment") != std::string::npos || + t.name.find("lowering") != std::string::npos) + envToolCount++; + } + check(envToolCount == 4, "4 environment MCP tools registered"); + check((int)tools.size() >= 38, "at least 38 total MCP tools"); +} + +// Test 8: EnvironmentSpec compact AST integration +static void test_env_compact_ast() { + auto* mod = new Module(); mod->id = "mod1"; mod->name = "compact_test"; + auto* env = new EnvironmentSpec(); + env->id = "env1"; env->envId = "posix"; + env->scheduler = "threads"; env->memory = "raii"; + mod->addChild("environment", env); + + auto* fn = new Function(); fn->id = "f1"; fn->name = "worker"; + auto* hc = new HostCall("io.fs", "readFile"); + hc->id = "hc1"; + fn->addChild("body", hc); + mod->addChild("functions", fn); + + // Compact AST should include the host boundary node + json compact = toJsonCompactTree(mod); + check(compact.is_array(), "compact tree is array"); + check(compact.size() >= 3, "at least 3 nodes (mod + fn + hostcall)"); + + // Find the HostCall in compact output + bool foundHostCall = false; + for (const auto& node : compact) { + if (node.value("type", "") == "HostCall") { + foundHostCall = true; + check(node.value("name", "") == "readFile", + "compact HostCall name is readFile"); + } + } + check(foundHostCall, "HostCall found in compact AST"); + delete mod; +} + +int main() { + std::cout << "=== Step 289: Phase 10e Integration Tests ===\n"; + test_full_env_workflow(); + test_env_switch_clears_issues(); + test_host_boundary_roundtrip(); + test_capability_validation_e2e(); + test_lowering_hints_change_with_env(); + test_batch_env_queries(); + test_mcp_tool_count(); + test_env_compact_ast(); + std::cout << "\nResults: " << passed << "/" << (passed+failed) << "\n"; + return failed > 0 ? 1 : 0; +} diff --git a/progress.md b/progress.md index 716094e..98c861c 100644 --- a/progress.md +++ b/progress.md @@ -743,9 +743,7 @@ Generic fallback added to getSemanticAnnotations RPC for extensible annotation t ## Phase 10e: Environment Layer (Steps 284-289) -**Status:** IN PROGRESS - -### Completed so far: +**Status:** PASS (200/200 checks across steps 284–289) **New files created:** - `editor/src/EnvironmentSpec.h` — EnvironmentSpec AST node (envId, envVersion, capabilities, constraints, scheduler, memory, bindingTimes, exceptions, ffi), CapabilityRequirement annotation, capability vocabulary (17 known capabilities), validateCapabilities() (E0501 diagnostics), validateEnvAnnotations() (E0502-E0505 diagnostics), getLoweringHints() (env-aware lowering patterns), envSpecToJson/envSpecFromJson helpers @@ -757,15 +755,21 @@ Generic fallback added to getSemanticAnnotations RPC for extensible annotation t - `SidecarPersistence.h` — CapabilityRequirement in isSemanticAnnotation - `HeadlessAgentRPCHandler.h` — capabilityRequirement type in setSemanticAnnotation, 4 new RPCs: setEnvironment, getEnvironment, validateEnvironment, getLoweringHints - `AgentPermissionPolicy.h` — getEnvironment/validateEnvironment/getLoweringHints (read-only), setEnvironment (mutation) +- `MCPServer.h` — registerEnvironmentTools() with 4 MCP tool definitions (whetstone_set_environment, whetstone_get_environment, whetstone_validate_environment, whetstone_get_lowering_hints) -**Test files written:** +**Test files:** - `step284_test.cpp` — 12 tests (EnvironmentSpec schema, JSON roundtrip, module attachment) - `step285_test.cpp` — 12 tests (capability vocabulary, validation, CapabilityRequirement) +- `step286_test.cpp` — 12 tests (validateEnvAnnotations E0502-E0505, RPC validate/set/get, Linter permissions, MCP tool registration, env replacement) +- `step287_test.cpp` — 12 tests (getLoweringHints: callback/std_async/coroutine, suppress_dealloc/explicit_delete/raii_cleanup, try_catch/expected, combined hints, RPC method, null safety) +- `step288_test.cpp` — 12 tests (HostCall/ScheduleTask/ModuleLoad construction, JSON roundtrip, createNode, compact AST names, capability validation, nested body children) +- `step289_test.cpp` — 8 integration tests (full env workflow, env switch clears issues, host boundary roundtrip, capability validation e2e, lowering hints change with env, batch env queries, MCP tool count 38+, compact AST with host boundary) -### Remaining: -- Write step286_test.cpp (environment-aware pipeline hooks, 12 tests) -- Write step287_test.cpp (environment-aware lowering decisions, 12 tests) -- Write step288_test.cpp (host boundary AST nodes, 12 tests) -- Write step289_test.cpp (Phase 10e integration tests, 8 tests) -- Add CMake targets for steps 284-289 -- Build, test, fix, commit +**Key results:** +- Phase 10e complete: all 6 steps pass (200/200 checks across steps 284–289) +- 4 environment RPCs: setEnvironment, getEnvironment, validateEnvironment, getLoweringHints +- 5 environment diagnostics: E0501 (unmet capability), E0502–E0505 (annotation/scheduler incompatibility) +- 3 host boundary AST nodes: HostCall, ScheduleTask, ModuleLoad +- Lowering hint patterns: callback, std_async, coroutine, suppress_dealloc, explicit_delete, raii_cleanup, try_catch, expected, checked_throw +- 38 MCP tools total (was 34): +whetstone_set_environment, +whetstone_get_environment, +whetstone_validate_environment, +whetstone_get_lowering_hints +- Sprint 10 complete: all 5 phases pass (phases 10a–10e)