Steps 284-289: Phase 10e — environment layer, host boundary nodes, MCP env tools (200/200 tests)
Completes Sprint 10. Adds EnvironmentSpec AST node, capability validation (E0501), annotation compatibility checks (E0502-E0505), env-aware lowering hints, 3 host boundary AST nodes (HostCall, ScheduleTask, ModuleLoad), and 4 MCP environment tools. 38 MCP tools total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
304
editor/tests/step286_test.cpp
Normal file
304
editor/tests/step286_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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 <nlohmann/json.hpp>
|
||||
|
||||
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<EnvironmentSpec*>(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<EnvironmentSpec*>(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<EnvironmentSpec*>(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<EnvironmentSpec*>(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<EnvironmentSpec*>(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<EnvironmentSpec*>(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<int>() >= 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<std::string>().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;
|
||||
}
|
||||
288
editor/tests/step287_test.cpp
Normal file
288
editor/tests/step287_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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 <nlohmann/json.hpp>
|
||||
|
||||
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<int>() >= 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;
|
||||
}
|
||||
236
editor/tests/step288_test.cpp
Normal file
236
editor/tests/step288_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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 <nlohmann/json.hpp>
|
||||
|
||||
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<HostCall*>(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<ScheduleTask*>(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<ModuleLoad*>(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<HostCall*>(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;
|
||||
}
|
||||
314
editor/tests/step289_test.cpp
Normal file
314
editor/tests/step289_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#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 <nlohmann/json.hpp>
|
||||
|
||||
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<int>() == 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<int>() >= 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<int>() >= 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<int>() == 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<std::string>().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;
|
||||
}
|
||||
26
progress.md
26
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)
|
||||
|
||||
Reference in New Issue
Block a user