Step 324: Workflow RPC + MCP Tools (12/12 tests)
8 new RPC methods and MCP tools for workflow lifecycle management: create, inspect, assign, complete, reject, save. Role-based access control enforced — Linter read-only, Refactor/Generator can mutate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1936,4 +1936,14 @@ add_executable(step323_test tests/step323_test.cpp)
|
||||
target_include_directories(step323_test PRIVATE src)
|
||||
target_link_libraries(step323_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 324: Workflow RPC + MCP Tools
|
||||
add_executable(step324_test tests/step324_test.cpp)
|
||||
target_include_directories(step324_test PRIVATE src)
|
||||
target_link_libraries(step324_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)
|
||||
|
||||
@@ -65,7 +65,10 @@ struct AgentPermissionPolicy {
|
||||
method == "validateEnvironment" ||
|
||||
method == "getLoweringHints" ||
|
||||
method == "getProjectModel" ||
|
||||
method == "inferAnnotations") {
|
||||
method == "inferAnnotations" ||
|
||||
method == "getWorkflowState" ||
|
||||
method == "getReadyTasks" ||
|
||||
method == "getWorkItem") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -89,7 +92,12 @@ struct AgentPermissionPolicy {
|
||||
method == "removeSemanticAnnotation" ||
|
||||
method == "setEnvironment" ||
|
||||
method == "createSkeleton" ||
|
||||
method == "addSkeletonNode") {
|
||||
method == "addSkeletonNode" ||
|
||||
method == "createWorkflow" ||
|
||||
method == "assignTask" ||
|
||||
method == "completeTask" ||
|
||||
method == "rejectTask" ||
|
||||
method == "saveWorkflow") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
|
||||
@@ -2246,5 +2246,201 @@ inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
});
|
||||
}
|
||||
|
||||
// --- createWorkflow ---
|
||||
if (method == "createWorkflow") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string projectName = params.value("projectName", "");
|
||||
if (projectName.empty())
|
||||
return headlessRpcError(id, -32602, "Missing projectName");
|
||||
if (!state.activeBuffer || !state.activeAST())
|
||||
return headlessRpcError(id, -32000, "No active buffer with AST");
|
||||
|
||||
state.workflow = WorkflowState(projectName);
|
||||
std::string bufferId = state.activeBuffer->path;
|
||||
int count = state.workflow->populateFromSkeleton(state.activeAST(), bufferId);
|
||||
|
||||
auto stats = state.workflow->getStats();
|
||||
return headlessRpcResult(id, {
|
||||
{"itemCount", count},
|
||||
{"phase", workflowPhaseToString(state.workflow->getPhase())},
|
||||
{"stats", stats.toJson()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getWorkflowState ---
|
||||
if (method == "getWorkflowState") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
|
||||
auto stats = state.workflow->getStats();
|
||||
return headlessRpcResult(id, {
|
||||
{"phase", workflowPhaseToString(state.workflow->getPhase())},
|
||||
{"stats", stats.toJson()},
|
||||
{"readyCount", state.workflow->queue.readyCount()},
|
||||
{"blockedCount", state.workflow->queue.blockedCount()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getReadyTasks ---
|
||||
if (method == "getReadyTasks") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
|
||||
auto ready = state.workflow->queue.getReady();
|
||||
json items = json::array();
|
||||
for (const auto& wi : ready) {
|
||||
items.push_back(workItemToJson(wi));
|
||||
}
|
||||
return headlessRpcResult(id, {{"items", items}, {"count", (int)items.size()}});
|
||||
}
|
||||
|
||||
// --- getWorkItem ---
|
||||
if (method == "getWorkItem") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
return headlessRpcResult(id, workItemToJson(*item));
|
||||
}
|
||||
|
||||
// --- assignTask ---
|
||||
if (method == "assignTask") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
std::string assignee = params.value("assignee", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
WorkItem updated = *item;
|
||||
bool ok = transitionWorkItem(updated, WI_ASSIGNED);
|
||||
if (!ok)
|
||||
return headlessRpcError(id, -32000, "Cannot assign item in status: " + updated.status);
|
||||
updated.assignee = assignee;
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
state.workflow->recordChange(itemId, item->status, WI_ASSIGNED,
|
||||
"agent:" + sessionId);
|
||||
|
||||
return headlessRpcResult(id, {{"success", true}, {"item", workItemToJson(updated)}});
|
||||
}
|
||||
|
||||
// --- completeTask ---
|
||||
if (method == "completeTask") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
// Attach result
|
||||
WorkItem updated = *item;
|
||||
if (params.contains("result")) {
|
||||
auto r = params["result"];
|
||||
updated.result.generatedCode = r.value("generatedCode", "");
|
||||
updated.result.confidence = r.value("confidence", 0.0f);
|
||||
updated.result.reasoning = r.value("reasoning", "");
|
||||
}
|
||||
|
||||
// Transition: must be in-progress (or review) to complete
|
||||
if (updated.status == WI_IN_PROGRESS || updated.status == WI_REVIEW) {
|
||||
transitionWorkItem(updated, WI_COMPLETE);
|
||||
} else {
|
||||
return headlessRpcError(id, -32000,
|
||||
"Cannot complete item in status: " + updated.status);
|
||||
}
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
state.workflow->recordChange(itemId, item->status, WI_COMPLETE,
|
||||
"agent:" + sessionId);
|
||||
|
||||
// Re-evaluate dependencies via complete()
|
||||
// (already handled internally since we set status to complete)
|
||||
// Check for newly ready items
|
||||
auto ready = state.workflow->queue.getReady();
|
||||
json newlyReady = json::array();
|
||||
for (const auto& wi : ready) {
|
||||
newlyReady.push_back(workItemToJson(wi));
|
||||
}
|
||||
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true},
|
||||
{"newlyReady", newlyReady}
|
||||
});
|
||||
}
|
||||
|
||||
// --- rejectTask ---
|
||||
if (method == "rejectTask") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
auto params = request.contains("params") ? request["params"] : json::object();
|
||||
std::string itemId = params.value("itemId", "");
|
||||
std::string reason = params.value("reason", "");
|
||||
if (itemId.empty())
|
||||
return headlessRpcError(id, -32602, "Missing itemId");
|
||||
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
if (!item)
|
||||
return headlessRpcError(id, -32602, "Work item not found");
|
||||
|
||||
if (item->status != WI_REVIEW)
|
||||
return headlessRpcError(id, -32000,
|
||||
"Cannot reject item in status: " + item->status);
|
||||
|
||||
bool ok = state.workflow->queue.reject(itemId, reason);
|
||||
if (!ok)
|
||||
return headlessRpcError(id, -32000, "Reject failed");
|
||||
|
||||
state.workflow->recordChange(itemId, "review", WI_READY,
|
||||
"human:" + sessionId, reason);
|
||||
|
||||
return headlessRpcResult(id, {{"success", true}});
|
||||
}
|
||||
|
||||
// --- saveWorkflow ---
|
||||
if (method == "saveWorkflow") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.workflow)
|
||||
return headlessRpcError(id, -32000, "No active workflow");
|
||||
|
||||
std::string root = state.workspaceRoot.empty() ? "." : state.workspaceRoot;
|
||||
auto sr = ::saveWorkflow(root, *state.workflow);
|
||||
return headlessRpcResult(id, {
|
||||
{"success", sr.success},
|
||||
{"path", sr.path},
|
||||
{"bytesWritten", sr.bytesWritten}
|
||||
});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
#include "SidecarPersistence.h"
|
||||
#include "AnnotationInference.h"
|
||||
#include "SkeletonAST.h"
|
||||
#include "WorkflowState.h"
|
||||
#include "WorkflowPersistence.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
@@ -134,6 +136,7 @@ struct HeadlessEditorState {
|
||||
std::string workspaceRoot;
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
std::optional<WorkflowState> workflow;
|
||||
|
||||
// --- Buffer access ---
|
||||
|
||||
|
||||
@@ -1355,6 +1355,117 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void registerWorkflowExecutionTools() {
|
||||
// whetstone_create_workflow
|
||||
tools_.push_back({"whetstone_create_workflow",
|
||||
"Create a workflow from the active buffer's skeleton AST. "
|
||||
"Populates work items from skeleton tasks with routing annotations.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"projectName", {{"type", "string"},
|
||||
{"description", "Name for the workflow project"}}}
|
||||
}}, {"required", json::array({"projectName"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_create_workflow"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("createWorkflow", args);
|
||||
};
|
||||
|
||||
// whetstone_get_workflow_state
|
||||
tools_.push_back({"whetstone_get_workflow_state",
|
||||
"Get current workflow state including phase, stats, ready and blocked counts.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_workflow_state"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getWorkflowState", args);
|
||||
};
|
||||
|
||||
// whetstone_get_ready_tasks
|
||||
tools_.push_back({"whetstone_get_ready_tasks",
|
||||
"Get work items ready for assignment, ordered by priority.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_ready_tasks"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getReadyTasks", args);
|
||||
};
|
||||
|
||||
// whetstone_get_work_item
|
||||
tools_.push_back({"whetstone_get_work_item",
|
||||
"Get full details of a single work item including result if present.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_get_work_item"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("getWorkItem", args);
|
||||
};
|
||||
|
||||
// whetstone_assign_task
|
||||
tools_.push_back({"whetstone_assign_task",
|
||||
"Assign a ready work item to a worker.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to assign"}}},
|
||||
{"assignee", {{"type", "string"},
|
||||
{"description", "Worker identifier"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_assign_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("assignTask", args);
|
||||
};
|
||||
|
||||
// whetstone_complete_task
|
||||
tools_.push_back({"whetstone_complete_task",
|
||||
"Mark a work item as complete with generated result. "
|
||||
"Triggers dependency cascade — blocked items may become ready.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to complete"}}},
|
||||
{"result", {{"type", "object"}, {"properties", {
|
||||
{"generatedCode", {{"type", "string"},
|
||||
{"description", "Generated code output"}}},
|
||||
{"confidence", {{"type", "number"},
|
||||
{"description", "Confidence score 0.0-1.0"}}},
|
||||
{"reasoning", {{"type", "string"},
|
||||
{"description", "Explanation of decisions"}}}
|
||||
}}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_complete_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("completeTask", args);
|
||||
};
|
||||
|
||||
// whetstone_reject_task
|
||||
tools_.push_back({"whetstone_reject_task",
|
||||
"Reject a work item back to the queue with feedback.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"itemId", {{"type", "string"},
|
||||
{"description", "Work item ID to reject"}}},
|
||||
{"reason", {{"type", "string"},
|
||||
{"description", "Rejection reason/feedback"}}}
|
||||
}}, {"required", json::array({"itemId"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_reject_task"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("rejectTask", args);
|
||||
};
|
||||
|
||||
// whetstone_save_workflow
|
||||
tools_.push_back({"whetstone_save_workflow",
|
||||
"Persist the current workflow state to a sidecar JSON file.",
|
||||
{{"type", "object"}, {"properties", json::object()}}
|
||||
});
|
||||
toolHandlers_["whetstone_save_workflow"] =
|
||||
[this](const json& args) {
|
||||
return callWhetstone("saveWorkflow", args);
|
||||
};
|
||||
}
|
||||
|
||||
void registerWhetstoneTools() {
|
||||
registerASTTools();
|
||||
registerAnnotationTools();
|
||||
@@ -1368,5 +1479,6 @@ private:
|
||||
registerEnvironmentTools();
|
||||
registerTrainingDataTools();
|
||||
registerWorkflowTools();
|
||||
registerWorkflowExecutionTools();
|
||||
}
|
||||
};
|
||||
|
||||
324
editor/tests/step324_test.cpp
Normal file
324
editor/tests/step324_test.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
// Step 324: Workflow RPC + MCP Tools (12 tests)
|
||||
// Tests createWorkflow, getReadyTasks, assignTask, completeTask,
|
||||
// rejectTask, getWorkItem, Linter role restrictions, MCP tool registration,
|
||||
// saveWorkflow round-trip, getWorkflowState.
|
||||
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "HeadlessAgentRPCHandler.h"
|
||||
#include "MCPServer.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(),
|
||||
const std::string& session = "test-session") {
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", method}};
|
||||
if (!params.empty()) req["params"] = params;
|
||||
return handleHeadlessAgentRequest(state, req, session);
|
||||
}
|
||||
|
||||
static HeadlessEditorState makeStateWithSkeleton() {
|
||||
HeadlessEditorState state;
|
||||
state.defaultLanguage = "python";
|
||||
// Create a skeleton module with functions
|
||||
auto* mod = createSkeletonModule("testmod", "python");
|
||||
addSkeletonFunction(mod, "compute", {"x", "y"}, "int");
|
||||
addSkeletonFunction(mod, "validate", {"data"}, "bool");
|
||||
addSkeletonFunction(mod, "transform", {"input"}, "string");
|
||||
|
||||
// Open buffer with some source and replace AST
|
||||
state.openBuffer("test", "# skeleton", "python");
|
||||
// Set the skeleton AST directly
|
||||
state.activeBuffer->sync.setAST(std::unique_ptr<Module>(mod));
|
||||
return state;
|
||||
}
|
||||
|
||||
// 1. createWorkflow from skeleton module
|
||||
void test_create_workflow() {
|
||||
TEST(create_workflow);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
|
||||
auto resp = rpcCall(state, "createWorkflow", {{"projectName", "myproj"}});
|
||||
CHECK(resp.contains("result"), "has result: " + resp.dump());
|
||||
auto r = resp["result"];
|
||||
CHECK(r["itemCount"].get<int>() == 3, "3 items created");
|
||||
CHECK(r.contains("phase"), "has phase");
|
||||
CHECK(r.contains("stats"), "has stats");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 2. getReadyTasks ordering
|
||||
void test_get_ready_tasks() {
|
||||
TEST(get_ready_tasks);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
auto resp = rpcCall(state, "getReadyTasks");
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
auto r = resp["result"];
|
||||
CHECK(r["count"].get<int>() == 3, "3 ready tasks");
|
||||
CHECK(r["items"].is_array(), "items is array");
|
||||
CHECK(r["items"].size() == 3, "3 items in array");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 3. assignTask transitions
|
||||
void test_assign_task() {
|
||||
TEST(assign_task);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
// Get a ready task
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
auto resp = rpcCall(state, "assignTask", {{"itemId", itemId}, {"assignee", "worker-1"}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["success"].get<bool>(), "success");
|
||||
CHECK(resp["result"]["item"]["status"] == "assigned", "status=assigned");
|
||||
CHECK(resp["result"]["item"]["assignee"] == "worker-1", "assignee set");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 4. completeTask triggers dependency cascade
|
||||
void test_complete_task() {
|
||||
TEST(complete_task);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
// Get and assign a task
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
rpcCall(state, "assignTask", {{"itemId", itemId}});
|
||||
|
||||
// Transition to in-progress manually (via queue update)
|
||||
auto item = state.workflow->queue.getItem(itemId);
|
||||
WorkItem updated = *item;
|
||||
transitionWorkItem(updated, WI_IN_PROGRESS);
|
||||
state.workflow->queue.updateItem(itemId, updated);
|
||||
|
||||
// Complete with result
|
||||
auto resp = rpcCall(state, "completeTask", {
|
||||
{"itemId", itemId},
|
||||
{"result", {{"generatedCode", "return x + y"}, {"confidence", 0.95}, {"reasoning", "simple"}}}
|
||||
});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["success"].get<bool>(), "success");
|
||||
|
||||
// Verify the item is complete
|
||||
auto itemResp = rpcCall(state, "getWorkItem", {{"itemId", itemId}});
|
||||
CHECK(itemResp["result"]["status"] == "complete", "item complete");
|
||||
CHECK(itemResp["result"]["result"]["generatedCode"] == "return x + y", "code attached");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 5. rejectTask re-enqueues
|
||||
void test_reject_task() {
|
||||
TEST(reject_task);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
// assign → in-progress → review
|
||||
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);
|
||||
|
||||
auto resp = rpcCall(state, "rejectTask", {{"itemId", itemId}, {"reason", "needs tests"}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["success"].get<bool>(), "success");
|
||||
|
||||
// Item should be back to ready
|
||||
auto itemResp = rpcCall(state, "getWorkItem", {{"itemId", itemId}});
|
||||
CHECK(itemResp["result"]["status"] == "ready", "back to ready");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 6. getWorkItem returns full details
|
||||
void test_get_work_item() {
|
||||
TEST(get_work_item);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
auto ready = rpcCall(state, "getReadyTasks")["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
auto resp = rpcCall(state, "getWorkItem", {{"itemId", itemId}});
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
auto r = resp["result"];
|
||||
CHECK(r.contains("id"), "has id");
|
||||
CHECK(r.contains("nodeId"), "has nodeId");
|
||||
CHECK(r.contains("nodeName"), "has nodeName");
|
||||
CHECK(r.contains("nodeType"), "has nodeType");
|
||||
CHECK(r.contains("priority"), "has priority");
|
||||
CHECK(r.contains("status"), "has status");
|
||||
CHECK(r.contains("result"), "has result field");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 7. Linter role can read but not mutate
|
||||
void test_linter_restrictions() {
|
||||
TEST(linter_restrictions);
|
||||
auto state = makeStateWithSkeleton();
|
||||
// Create workflow as Generator first
|
||||
state.setAgentRole("gen-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}}, "gen-session");
|
||||
|
||||
// Now try as Linter
|
||||
state.setAgentRole("lint-session", AgentRole::Linter);
|
||||
|
||||
// Read operations should work
|
||||
auto resp1 = rpcCall(state, "getWorkflowState", {}, "lint-session");
|
||||
CHECK(resp1.contains("result"), "Linter can getWorkflowState");
|
||||
|
||||
auto resp2 = rpcCall(state, "getReadyTasks", {}, "lint-session");
|
||||
CHECK(resp2.contains("result"), "Linter can getReadyTasks");
|
||||
|
||||
// Mutation should fail
|
||||
auto ready = resp2["result"]["items"];
|
||||
std::string itemId = ready[0]["id"].get<std::string>();
|
||||
|
||||
auto resp3 = rpcCall(state, "assignTask", {{"itemId", itemId}}, "lint-session");
|
||||
CHECK(resp3.contains("error"), "Linter cannot assignTask");
|
||||
|
||||
auto resp4 = rpcCall(state, "createWorkflow", {{"projectName", "x"}}, "lint-session");
|
||||
CHECK(resp4.contains("error"), "Linter cannot createWorkflow");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 8. MCP tool registration (8 new tools, 50+ total)
|
||||
void test_mcp_registration() {
|
||||
TEST(mcp_registration);
|
||||
MCPServer server;
|
||||
|
||||
const auto& tools = server.getTools();
|
||||
// Count workflow execution tools
|
||||
int workflowExecCount = 0;
|
||||
for (const auto& t : tools) {
|
||||
std::string name = t.name;
|
||||
if (name == "whetstone_create_workflow" ||
|
||||
name == "whetstone_get_workflow_state" ||
|
||||
name == "whetstone_get_ready_tasks" ||
|
||||
name == "whetstone_get_work_item" ||
|
||||
name == "whetstone_assign_task" ||
|
||||
name == "whetstone_complete_task" ||
|
||||
name == "whetstone_reject_task" ||
|
||||
name == "whetstone_save_workflow") {
|
||||
workflowExecCount++;
|
||||
}
|
||||
}
|
||||
CHECK(workflowExecCount == 8, "8 workflow execution tools, got " +
|
||||
std::to_string(workflowExecCount));
|
||||
CHECK((int)tools.size() >= 50, "50+ total tools, got " +
|
||||
std::to_string(tools.size()));
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 9. saveWorkflow round-trip via RPC
|
||||
void test_save_workflow_rpc() {
|
||||
TEST(save_workflow_rpc);
|
||||
std::string testDir = "/tmp/whetstone_step324_test_" + std::to_string(getpid());
|
||||
fs::create_directories(testDir);
|
||||
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.workspaceRoot = testDir;
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "savetest"}});
|
||||
|
||||
auto resp = rpcCall(state, "saveWorkflow");
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
CHECK(resp["result"]["success"].get<bool>(), "save success");
|
||||
CHECK(resp["result"]["bytesWritten"].get<int>() > 0, "bytes written");
|
||||
|
||||
// Verify file exists
|
||||
std::string path = testDir + "/.whetstone/savetest.workflow.json";
|
||||
CHECK(fs::exists(path), "sidecar file exists");
|
||||
|
||||
fs::remove_all(testDir);
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 10. getWorkflowState returns stats
|
||||
void test_get_workflow_state() {
|
||||
TEST(get_workflow_state);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
auto resp = rpcCall(state, "getWorkflowState");
|
||||
CHECK(resp.contains("result"), "has result");
|
||||
auto r = resp["result"];
|
||||
CHECK(r.contains("phase"), "has phase");
|
||||
CHECK(r.contains("stats"), "has stats");
|
||||
CHECK(r.contains("readyCount"), "has readyCount");
|
||||
CHECK(r.contains("blockedCount"), "has blockedCount");
|
||||
CHECK(r["readyCount"].get<int>() == 3, "3 ready");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 11. No workflow returns error
|
||||
void test_no_workflow_error() {
|
||||
TEST(no_workflow_error);
|
||||
HeadlessEditorState state;
|
||||
auto resp = rpcCall(state, "getWorkflowState");
|
||||
CHECK(resp.contains("error"), "error when no workflow");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 12. getWorkItem nonexistent returns error
|
||||
void test_get_nonexistent_item() {
|
||||
TEST(get_nonexistent_item);
|
||||
auto state = makeStateWithSkeleton();
|
||||
state.setAgentRole("test-session", AgentRole::Generator);
|
||||
rpcCall(state, "createWorkflow", {{"projectName", "proj"}});
|
||||
|
||||
auto resp = rpcCall(state, "getWorkItem", {{"itemId", "nonexistent"}});
|
||||
CHECK(resp.contains("error"), "error for nonexistent item");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "=== Step 324: Workflow RPC + MCP Tools ===\n";
|
||||
try {
|
||||
test_create_workflow();
|
||||
test_get_ready_tasks();
|
||||
test_assign_task();
|
||||
test_complete_task();
|
||||
test_reject_task();
|
||||
test_get_work_item();
|
||||
test_linter_restrictions();
|
||||
test_mcp_registration();
|
||||
test_save_workflow_rpc();
|
||||
test_get_workflow_state();
|
||||
test_no_workflow_error();
|
||||
test_get_nonexistent_item();
|
||||
} 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;
|
||||
}
|
||||
28
progress.md
28
progress.md
@@ -1284,6 +1284,34 @@ delete, and multiple workflows in the same workspace.
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — step323_test target
|
||||
|
||||
### Step 324: Workflow RPC + MCP Tools
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Exposes workflow management through 8 RPC methods and 8 MCP tools so agents
|
||||
can create, inspect, and advance workflows. Linter role restricted to read-only
|
||||
access; Refactor/Generator can mutate.
|
||||
|
||||
**Files created:**
|
||||
- `editor/tests/step324_test.cpp` — 12 tests: createWorkflow, getReadyTasks
|
||||
ordering, assignTask transitions, completeTask with result + cascade,
|
||||
rejectTask re-enqueue, getWorkItem details, Linter restrictions, MCP
|
||||
registration (8 new tools, 50+ total), saveWorkflow RPC, getWorkflowState,
|
||||
no-workflow error, nonexistent item error
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/HeadlessEditorState.h` — added WorkflowState/WorkflowPersistence
|
||||
includes, optional<WorkflowState> workflow member
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` — 8 new RPC methods: createWorkflow,
|
||||
getWorkflowState, getReadyTasks, getWorkItem, assignTask, completeTask,
|
||||
rejectTask, saveWorkflow
|
||||
- `editor/src/AgentPermissionPolicy.h` — read-only: getWorkflowState,
|
||||
getReadyTasks, getWorkItem; mutation: createWorkflow, assignTask,
|
||||
completeTask, rejectTask, saveWorkflow
|
||||
- `editor/src/MCPServer.h` — registerWorkflowExecutionTools() with 8 tools
|
||||
- `editor/CMakeLists.txt` — step324_test target
|
||||
|
||||
**Tool count:** 50+ (42 existing + 8 workflow execution tools)
|
||||
|
||||
---
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
Reference in New Issue
Block a user