From 08c8f0b085bef67757dc892debc404f90c982054 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 15 Feb 2026 19:06:15 -0700 Subject: [PATCH] Step 325: Phase 12a Integration Tests (8/8 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end workflow lifecycle: skeleton→workflow, priority ordering, dependency cascading, full lifecycle with results, rejection flow, persistence, and combined 5-item workflow. Phase 12a complete (68/68). Co-Authored-By: Claude Opus 4.6 --- editor/CMakeLists.txt | 10 + editor/src/TaskQueue.h | 4 + editor/tests/step325_test.cpp | 366 ++++++++++++++++++++++++++++++++++ progress.md | 21 ++ 4 files changed, 401 insertions(+) create mode 100644 editor/tests/step325_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 64aff54..d05da85 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1946,4 +1946,14 @@ target_link_libraries(step324_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +# Step 325: Phase 12a Integration Tests +add_executable(step325_test tests/step325_test.cpp) +target_include_directories(step325_test PRIVATE src) +target_link_libraries(step325_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) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/TaskQueue.h b/editor/src/TaskQueue.h index 1cb0978..5ac2a61 100644 --- a/editor/src/TaskQueue.h +++ b/editor/src/TaskQueue.h @@ -144,6 +144,10 @@ public: for (auto& item : items_) { if (item.id == itemId) { item = updated; + // If item became complete, re-evaluate blocked dependencies + if (updated.status == WI_COMPLETE) { + resolveDependencies(); + } return true; } } diff --git a/editor/tests/step325_test.cpp b/editor/tests/step325_test.cpp new file mode 100644 index 0000000..85bf0e6 --- /dev/null +++ b/editor/tests/step325_test.cpp @@ -0,0 +1,366 @@ +// Step 325: Phase 12a Integration Tests (8 tests) +// End-to-end workflow lifecycle from skeleton to completion. + +#include "HeadlessEditorState.h" +#include "HeadlessAgentRPCHandler.h" +#include "WorkflowPersistence.h" +#include +#include +#include +#include + +namespace fs = std::filesystem; +using json = nlohmann::json; + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +static json rpcCall(HeadlessEditorState& state, const std::string& method, + const json& params = json::object()) { + json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}}; + if (!params.empty()) req["params"] = params; + return handleHeadlessAgentRequest(state, req, "test-session"); +} + +// Helper: make state with skeleton and Generator role +static HeadlessEditorState makeState() { + HeadlessEditorState state; + state.defaultLanguage = "python"; + state.setAgentRole("test-session", AgentRole::Generator); + return state; +} + +// Helper: build skeleton module with annotations on a state +static void setupSkeleton(HeadlessEditorState& state, Module* mod) { + state.openBuffer("test", "# skeleton", "python"); + state.activeBuffer->sync.setAST(std::unique_ptr(mod)); +} + +// Helper: transition a work item through assign → in-progress via queue +static void advanceToInProgress(HeadlessEditorState& state, const std::string& itemId) { + rpcCall(state, "assignTask", {{"itemId", itemId}, {"assignee", "worker"}}); + auto item = *state.workflow->queue.getItem(itemId); + transitionWorkItem(item, WI_IN_PROGRESS); + state.workflow->queue.updateItem(itemId, item); +} + +// 1. Create skeleton module → createWorkflow → verify item count +void test_skeleton_to_workflow() { + TEST(skeleton_to_workflow); + auto state = makeState(); + auto* mod = createSkeletonModule("proj", "python"); + addSkeletonFunction(mod, "alpha", {"x"}, "int"); + addSkeletonFunction(mod, "beta", {"y"}, "str"); + addSkeletonFunction(mod, "gamma", {}, "void"); + addSkeletonFunction(mod, "delta", {"a", "b"}, "float"); + setupSkeleton(state, mod); + + auto resp = rpcCall(state, "createWorkflow", {{"projectName", "proj"}}); + CHECK(resp.contains("result"), "has result"); + CHECK(resp["result"]["itemCount"].get() == 4, "4 items from 4 functions"); + PASS(); +} + +// 2. Priority ordering: critical before low +void test_priority_ordering() { + TEST(priority_ordering); + auto state = makeState(); + auto* mod = createSkeletonModule("proj", "python"); + + // Add functions with different priorities via annotations + auto* lowFn = addSkeletonFunction(mod, "lowPri", {"x"}, "int"); + auto* lowPrio = new PriorityAnnotation(); + lowPrio->id = "pa1"; lowPrio->level = "low"; + lowFn->addChild("annotations", lowPrio); + + auto* critFn = addSkeletonFunction(mod, "critPri", {"x"}, "int"); + auto* critPrio = new PriorityAnnotation(); + critPrio->id = "pa2"; critPrio->level = "critical"; + critFn->addChild("annotations", critPrio); + + setupSkeleton(state, mod); + rpcCall(state, "createWorkflow", {{"projectName", "proj"}}); + + auto resp = rpcCall(state, "getReadyTasks"); + auto items = resp["result"]["items"]; + CHECK(items.size() == 2, "2 items"); + + // Critical should be first + CHECK(items[0]["priority"] == "critical", "first=critical, got " + + items[0]["priority"].get()); + CHECK(items[1]["priority"] == "low", "second=low"); + PASS(); +} + +// 3. Dependency chain: A blocks B blocks C +void test_dependency_chain() { + TEST(dependency_chain); + auto state = makeState(); + + // Manually create workflow with dependency chain + state.openBuffer("test", "# dep chain", "python"); + auto* mod = createSkeletonModule("deps", "python"); + addSkeletonFunction(mod, "funcA", {}, "void"); + state.activeBuffer->sync.setAST(std::unique_ptr(mod)); + + // Create workflow then manually add items with deps + state.workflow = WorkflowState("deps"); + WorkItem a, b, c; + a.id = "A"; a.nodeId = "nA"; a.nodeName = "funcA"; a.nodeType = "Function"; + a.priority = "high"; a.status = WI_PENDING; a.createdAt = workItemTimestamp(); + + b.id = "B"; b.nodeId = "nB"; b.nodeName = "funcB"; b.nodeType = "Function"; + b.priority = "high"; b.status = WI_PENDING; b.dependencies = {"A"}; + b.createdAt = workItemTimestamp(); + + c.id = "C"; c.nodeId = "nC"; c.nodeName = "funcC"; c.nodeType = "Function"; + c.priority = "high"; c.status = WI_PENDING; c.dependencies = {"B"}; + c.createdAt = workItemTimestamp(); + + state.workflow->queue.enqueue(a); + state.workflow->queue.enqueue(b); + state.workflow->queue.enqueue(c); + + CHECK(state.workflow->queue.readyCount() == 1, "only A ready"); + + // Complete A + advanceToInProgress(state, "A"); + rpcCall(state, "completeTask", {{"itemId", "A"}}); + CHECK(state.workflow->queue.readyCount() == 1, "B now ready"); + + // Complete B + advanceToInProgress(state, "B"); + rpcCall(state, "completeTask", {{"itemId", "B"}}); + CHECK(state.workflow->queue.readyCount() == 1, "C now ready"); + + auto peek = state.workflow->queue.peek(); + CHECK(peek.has_value() && peek->id == "C", "C is next"); + PASS(); +} + +// 4. Full lifecycle: create → assign → in-progress → complete with result +void test_full_lifecycle() { + TEST(full_lifecycle); + auto state = makeState(); + auto* mod = createSkeletonModule("life", "python"); + addSkeletonFunction(mod, "compute", {"x"}, "int"); + setupSkeleton(state, mod); + rpcCall(state, "createWorkflow", {{"projectName", "life"}}); + + auto ready = rpcCall(state, "getReadyTasks")["result"]["items"]; + std::string itemId = ready[0]["id"].get(); + + // Assign + rpcCall(state, "assignTask", {{"itemId", itemId}, {"assignee", "llm-worker"}}); + auto item = rpcCall(state, "getWorkItem", {{"itemId", itemId}})["result"]; + CHECK(item["status"] == "assigned", "assigned"); + + // In-progress + advanceToInProgress(state, itemId); + + // Complete with result + auto resp = rpcCall(state, "completeTask", { + {"itemId", itemId}, + {"result", {{"generatedCode", "return x * 2"}, {"confidence", 0.9}, {"reasoning", "doubling"}}} + }); + CHECK(resp["result"]["success"].get(), "complete success"); + + // Verify result attached + item = rpcCall(state, "getWorkItem", {{"itemId", itemId}})["result"]; + CHECK(item["status"] == "complete", "complete"); + CHECK(item["result"]["generatedCode"] == "return x * 2", "code attached"); + CHECK(item["result"]["confidence"].get() > 0.89f, "confidence"); + + // Phase should be Complete + auto ws = rpcCall(state, "getWorkflowState")["result"]; + CHECK(ws["phase"] == "complete", "phase=complete"); + PASS(); +} + +// 5. Rejection flow: assign → reject → re-appears in ready +void test_rejection_flow() { + TEST(rejection_flow); + auto state = makeState(); + auto* mod = createSkeletonModule("rej", "python"); + addSkeletonFunction(mod, "validate", {"data"}, "bool"); + setupSkeleton(state, mod); + rpcCall(state, "createWorkflow", {{"projectName", "rej"}}); + + auto ready = rpcCall(state, "getReadyTasks")["result"]["items"]; + std::string itemId = ready[0]["id"].get(); + + // assign → in-progress → review → reject + rpcCall(state, "assignTask", {{"itemId", itemId}}); + auto item = *state.workflow->queue.getItem(itemId); + transitionWorkItem(item, WI_IN_PROGRESS); + state.workflow->queue.updateItem(itemId, item); + item = *state.workflow->queue.getItem(itemId); + transitionWorkItem(item, WI_REVIEW); + state.workflow->queue.updateItem(itemId, item); + + rpcCall(state, "rejectTask", {{"itemId", itemId}, {"reason", "missing edge cases"}}); + + // Should be back in ready queue + ready = rpcCall(state, "getReadyTasks")["result"]["items"]; + CHECK(ready.size() == 1, "1 ready after reject"); + CHECK(ready[0]["id"] == itemId, "same item back in queue"); + PASS(); +} + +// 6. Persistence: save → reload → verify items and history +void test_persistence_roundtrip() { + TEST(persistence_roundtrip); + std::string testDir = "/tmp/whetstone_step325_test_" + std::to_string(getpid()); + fs::create_directories(testDir); + + auto state = makeState(); + state.workspaceRoot = testDir; + auto* mod = createSkeletonModule("persist", "python"); + addSkeletonFunction(mod, "f1", {}, "void"); + addSkeletonFunction(mod, "f2", {}, "void"); + setupSkeleton(state, mod); + rpcCall(state, "createWorkflow", {{"projectName", "persist"}}); + + // Make some changes + auto ready = rpcCall(state, "getReadyTasks")["result"]["items"]; + std::string id0 = ready[0]["id"].get(); + rpcCall(state, "assignTask", {{"itemId", id0}, {"assignee", "w1"}}); + + // Save + rpcCall(state, "saveWorkflow"); + + // Load into new state + auto loaded = loadWorkflow(testDir, "persist"); + CHECK(loaded.has_value(), "loaded"); + CHECK(loaded->queue.size() == 2, "2 items"); + CHECK(loaded->projectName == "persist", "name preserved"); + + // Check history + auto hist = loaded->getHistory(id0); + CHECK(!hist.empty(), "history for assigned item"); + + fs::remove_all(testDir); + PASS(); +} + +// 7. Stats accuracy with mixed statuses +void test_stats_accuracy() { + TEST(stats_accuracy); + auto state = makeState(); + + state.openBuffer("test", "# stats", "python"); + auto* mod = createSkeletonModule("stats", "python"); + addSkeletonFunction(mod, "f1", {}, "void"); + state.activeBuffer->sync.setAST(std::unique_ptr(mod)); + + // Build workflow manually with mixed statuses + state.workflow = WorkflowState("stats"); + auto addItem = [&](const std::string& id, const std::string& status, + const std::string& prio, const std::string& wtype) { + WorkItem wi; + wi.id = id; wi.nodeId = "n" + id; wi.nodeName = "f" + id; + wi.nodeType = "Function"; wi.priority = prio; + wi.workerType = wtype; wi.status = status; + wi.createdAt = workItemTimestamp(); + state.workflow->queue.enqueue(wi); + }; + + addItem("s1", WI_READY, "high", "llm"); + addItem("s2", WI_ASSIGNED, "medium", "llm"); + addItem("s3", WI_COMPLETE, "low", "deterministic"); + addItem("s4", WI_COMPLETE, "critical", "llm"); + + auto resp = rpcCall(state, "getWorkflowState"); + auto stats = resp["result"]["stats"]; + CHECK(stats["total"].get() == 4, "total=4"); + CHECK(stats["ready"].get() == 1, "ready=1"); + CHECK(stats["assigned"].get() == 1, "assigned=1"); + CHECK(stats["complete"].get() == 2, "complete=2"); + CHECK(stats["completionPercent"].get() > 49.0f, "50%"); + PASS(); +} + +// 8. Combined: 5 functions, mixed priorities + dependencies, complete in order +void test_combined_workflow() { + TEST(combined_workflow); + auto state = makeState(); + state.openBuffer("test", "# combined", "python"); + auto* mod = createSkeletonModule("combined", "python"); + addSkeletonFunction(mod, "init", {}, "void"); + state.activeBuffer->sync.setAST(std::unique_ptr(mod)); + + state.workflow = WorkflowState("combined"); + + // 5 items: init (critical, no deps), parse (high, deps on init), + // validate (medium, deps on parse), transform (medium, deps on parse), + // output (low, deps on validate + transform) + auto makeWI = [](const std::string& id, const std::string& prio, + const std::vector& deps) { + WorkItem wi; + wi.id = id; wi.nodeId = "n_" + id; wi.nodeName = id; + wi.nodeType = "Function"; wi.priority = prio; + wi.workerType = "llm"; wi.status = WI_PENDING; + wi.dependencies = deps; + wi.createdAt = workItemTimestamp(); + return wi; + }; + + state.workflow->queue.enqueue(makeWI("init", "critical", {})); + state.workflow->queue.enqueue(makeWI("parse", "high", {"init"})); + state.workflow->queue.enqueue(makeWI("validate", "medium", {"parse"})); + state.workflow->queue.enqueue(makeWI("transform", "medium", {"parse"})); + state.workflow->queue.enqueue(makeWI("output", "low", {"validate", "transform"})); + + CHECK(state.workflow->queue.readyCount() == 1, "only init ready"); + CHECK(state.workflow->getPhase() == WorkflowPhase::Routing, "phase=Routing"); + + // Complete init + advanceToInProgress(state, "init"); + rpcCall(state, "completeTask", {{"itemId", "init"}}); + CHECK(state.workflow->queue.readyCount() == 1, "parse now ready"); + + // Complete parse + advanceToInProgress(state, "parse"); + rpcCall(state, "completeTask", {{"itemId", "parse"}}); + CHECK(state.workflow->queue.readyCount() == 2, "validate + transform ready"); + + // Complete validate and transform + advanceToInProgress(state, "validate"); + rpcCall(state, "completeTask", {{"itemId", "validate"}}); + advanceToInProgress(state, "transform"); + rpcCall(state, "completeTask", {{"itemId", "transform"}}); + CHECK(state.workflow->queue.readyCount() == 1, "output ready"); + + // Complete output + advanceToInProgress(state, "output"); + rpcCall(state, "completeTask", {{"itemId", "output"}}); + + // All complete + CHECK(state.workflow->getPhase() == WorkflowPhase::Complete, "phase=Complete"); + auto stats = state.workflow->getStats(); + CHECK(stats.complete == 5, "5 complete"); + CHECK(stats.completionPercent > 99.0f, "100%"); + PASS(); +} + +int main() { + std::cout << "=== Step 325: Phase 12a Integration Tests ===\n"; + try { + test_skeleton_to_workflow(); + test_priority_ordering(); + test_dependency_chain(); + test_full_lifecycle(); + test_rejection_flow(); + test_persistence_roundtrip(); + test_stats_accuracy(); + test_combined_workflow(); + } catch (const std::exception& e) { + std::cout << "EXCEPTION: " << e.what() << "\n"; + ++failed; + } + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed > 0 ? 1 : 0; +} diff --git a/progress.md b/progress.md index bb4db4a..4a73213 100644 --- a/progress.md +++ b/progress.md @@ -1312,6 +1312,27 @@ access; Refactor/Generator can mutate. **Tool count:** 50+ (42 existing + 8 workflow execution tools) +### Step 325: Phase 12a Integration Tests +**Status:** PASS (8/8 tests) + +End-to-end workflow lifecycle from skeleton to completion. Covers priority +ordering, dependency chains (A→B→C cascade), full lifecycle with results, +rejection flow, persistence roundtrip, stats accuracy, and a combined +5-function workflow with mixed priorities and dependencies. + +**Files created:** +- `editor/tests/step325_test.cpp` — 8 integration tests: skeleton→workflow, + priority ordering, dependency chain cascade, full lifecycle with result, + rejection flow, persistence save/load, stats accuracy, combined 5-item + workflow completing to phase=Complete + +**Files modified:** +- `editor/src/TaskQueue.h` — updateItem triggers resolveDependencies when + item status becomes complete (fixes cascade through RPC path) +- `editor/CMakeLists.txt` — step325_test target + +**Phase 12a complete:** 6/6 steps (320-325), 68/68 tests passing + --- # Roadmap Planning — Sprints 12-25+