Files
whetstone_DSL/editor/tests/step325_test.cpp
Bill 08c8f0b085 Step 325: Phase 12a Integration Tests (8/8 tests)
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 <noreply@anthropic.com>
2026-02-15 19:06:15 -07:00

367 lines
14 KiB
C++

// 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 <nlohmann/json.hpp>
#include <iostream>
#include <string>
#include <filesystem>
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<Module>(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<int>() == 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<std::string>());
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<Module>(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<std::string>();
// 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<bool>(), "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<float>() > 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<std::string>();
// 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<std::string>();
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<Module>(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<int>() == 4, "total=4");
CHECK(stats["ready"].get<int>() == 1, "ready=1");
CHECK(stats["assigned"].get<int>() == 1, "assigned=1");
CHECK(stats["complete"].get<int>() == 2, "complete=2");
CHECK(stats["completionPercent"].get<float>() > 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<Module>(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<std::string>& 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;
}