Step 322: WorkflowState — Project-Level Workflow Tracking (12/12 tests)
Top-level workflow state with auto-detected phases, statistics, audit trail, skeleton population, and full JSON serialization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1926,4 +1926,9 @@ add_executable(step321_test tests/step321_test.cpp)
|
||||
target_include_directories(step321_test PRIVATE src)
|
||||
target_link_libraries(step321_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 322: WorkflowState — Project-Level Workflow Tracking
|
||||
add_executable(step322_test tests/step322_test.cpp)
|
||||
target_include_directories(step322_test PRIVATE src)
|
||||
target_link_libraries(step322_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
259
editor/src/WorkflowState.h
Normal file
259
editor/src/WorkflowState.h
Normal file
@@ -0,0 +1,259 @@
|
||||
#pragma once
|
||||
// Step 322: WorkflowState — Project-Level Workflow Tracking
|
||||
//
|
||||
// Top-level state managing the full workflow lifecycle for a project: from
|
||||
// skeleton creation through routing, execution, review, and completion.
|
||||
|
||||
#include "TaskQueue.h"
|
||||
#include "SkeletonAST.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// --- WorkflowPhase ---
|
||||
|
||||
enum class WorkflowPhase {
|
||||
Modeling, // architect is creating/annotating skeletons
|
||||
Routing, // routing engine is assigning worker types
|
||||
Executing, // workers are producing results
|
||||
Reviewing, // human review in progress
|
||||
Complete // all work items complete
|
||||
};
|
||||
|
||||
inline std::string workflowPhaseToString(WorkflowPhase p) {
|
||||
switch (p) {
|
||||
case WorkflowPhase::Modeling: return "modeling";
|
||||
case WorkflowPhase::Routing: return "routing";
|
||||
case WorkflowPhase::Executing: return "executing";
|
||||
case WorkflowPhase::Reviewing: return "reviewing";
|
||||
case WorkflowPhase::Complete: return "complete";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
inline WorkflowPhase workflowPhaseFromString(const std::string& s) {
|
||||
if (s == "modeling") return WorkflowPhase::Modeling;
|
||||
if (s == "routing") return WorkflowPhase::Routing;
|
||||
if (s == "executing") return WorkflowPhase::Executing;
|
||||
if (s == "reviewing") return WorkflowPhase::Reviewing;
|
||||
if (s == "complete") return WorkflowPhase::Complete;
|
||||
return WorkflowPhase::Modeling;
|
||||
}
|
||||
|
||||
// --- StatusChange: audit trail entry ---
|
||||
|
||||
struct StatusChange {
|
||||
std::string itemId;
|
||||
std::string fromStatus;
|
||||
std::string toStatus;
|
||||
std::string timestamp;
|
||||
std::string actor;
|
||||
std::string note;
|
||||
|
||||
json toJson() const {
|
||||
return json{
|
||||
{"itemId", itemId}, {"fromStatus", fromStatus},
|
||||
{"toStatus", toStatus}, {"timestamp", timestamp},
|
||||
{"actor", actor}, {"note", note}
|
||||
};
|
||||
}
|
||||
|
||||
static StatusChange fromJson(const json& j) {
|
||||
StatusChange sc;
|
||||
if (j.contains("itemId")) sc.itemId = j["itemId"].get<std::string>();
|
||||
if (j.contains("fromStatus")) sc.fromStatus = j["fromStatus"].get<std::string>();
|
||||
if (j.contains("toStatus")) sc.toStatus = j["toStatus"].get<std::string>();
|
||||
if (j.contains("timestamp")) sc.timestamp = j["timestamp"].get<std::string>();
|
||||
if (j.contains("actor")) sc.actor = j["actor"].get<std::string>();
|
||||
if (j.contains("note")) sc.note = j["note"].get<std::string>();
|
||||
return sc;
|
||||
}
|
||||
};
|
||||
|
||||
// --- WorkflowStats ---
|
||||
|
||||
struct WorkflowStats {
|
||||
int total = 0;
|
||||
int pending = 0;
|
||||
int ready = 0;
|
||||
int assigned = 0;
|
||||
int inProgress = 0;
|
||||
int review = 0;
|
||||
int complete = 0;
|
||||
int rejected = 0;
|
||||
std::map<std::string, int> byWorkerType;
|
||||
std::map<std::string, int> byPriority;
|
||||
float completionPercent = 0.0f;
|
||||
|
||||
json toJson() const {
|
||||
json j;
|
||||
j["total"] = total;
|
||||
j["pending"] = pending;
|
||||
j["ready"] = ready;
|
||||
j["assigned"] = assigned;
|
||||
j["inProgress"] = inProgress;
|
||||
j["review"] = review;
|
||||
j["complete"] = complete;
|
||||
j["rejected"] = rejected;
|
||||
j["byWorkerType"] = byWorkerType;
|
||||
j["byPriority"] = byPriority;
|
||||
j["completionPercent"] = completionPercent;
|
||||
return j;
|
||||
}
|
||||
};
|
||||
|
||||
// --- WorkflowState ---
|
||||
|
||||
class WorkflowState {
|
||||
public:
|
||||
TaskQueue queue;
|
||||
std::string projectName;
|
||||
std::string createdAt;
|
||||
|
||||
WorkflowState() : createdAt(workItemTimestamp()) {}
|
||||
|
||||
explicit WorkflowState(const std::string& name)
|
||||
: projectName(name), createdAt(workItemTimestamp()) {}
|
||||
|
||||
// Walk skeleton AST, create WorkItems, enqueue all. Returns count.
|
||||
int populateFromSkeleton(const ASTNode* module, const std::string& bufferId) {
|
||||
auto tasks = skeletonToTaskList(module);
|
||||
int count = 0;
|
||||
for (const auto& task : tasks) {
|
||||
WorkItem wi = createWorkItem(task, bufferId);
|
||||
recordChange(wi.id, "", WI_PENDING, "system", "created from skeleton");
|
||||
queue.enqueue(wi);
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Get workflow statistics
|
||||
WorkflowStats getStats() const {
|
||||
WorkflowStats stats;
|
||||
stats.total = queue.size();
|
||||
|
||||
auto count = [&](const std::string& status) {
|
||||
return static_cast<int>(queue.getByStatus(status).size());
|
||||
};
|
||||
stats.pending = count(WI_PENDING);
|
||||
stats.ready = count(WI_READY);
|
||||
stats.assigned = count(WI_ASSIGNED);
|
||||
stats.inProgress = count(WI_IN_PROGRESS);
|
||||
stats.review = count(WI_REVIEW);
|
||||
stats.complete = count(WI_COMPLETE);
|
||||
stats.rejected = count(WI_REJECTED);
|
||||
|
||||
// Aggregate by worker type and priority
|
||||
for (const auto& status : {WI_PENDING, WI_READY, WI_ASSIGNED, WI_IN_PROGRESS,
|
||||
WI_REVIEW, WI_COMPLETE, WI_REJECTED}) {
|
||||
for (const auto& item : queue.getByStatus(status)) {
|
||||
if (!item.workerType.empty()) stats.byWorkerType[item.workerType]++;
|
||||
if (!item.priority.empty()) stats.byPriority[item.priority]++;
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.total > 0) {
|
||||
stats.completionPercent = 100.0f * static_cast<float>(stats.complete)
|
||||
/ static_cast<float>(stats.total);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Auto-compute phase from item statuses
|
||||
WorkflowPhase getPhase() const {
|
||||
if (queue.size() == 0) return WorkflowPhase::Modeling;
|
||||
|
||||
auto stats = getStats();
|
||||
|
||||
// All complete → Complete
|
||||
if (stats.complete == stats.total) return WorkflowPhase::Complete;
|
||||
|
||||
// Any in review → Reviewing
|
||||
if (stats.review > 0) return WorkflowPhase::Reviewing;
|
||||
|
||||
// Any assigned or in-progress → Executing
|
||||
if (stats.assigned > 0 || stats.inProgress > 0) return WorkflowPhase::Executing;
|
||||
|
||||
// Any ready (routed) → Routing
|
||||
if (stats.ready > 0) return WorkflowPhase::Routing;
|
||||
|
||||
// All pending → Modeling
|
||||
return WorkflowPhase::Modeling;
|
||||
}
|
||||
|
||||
// Audit trail for a specific item
|
||||
std::vector<StatusChange> getHistory(const std::string& itemId) const {
|
||||
std::vector<StatusChange> result;
|
||||
for (const auto& sc : history_) {
|
||||
if (sc.itemId == itemId) result.push_back(sc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Record a status change in the audit trail
|
||||
void recordChange(const std::string& itemId, const std::string& from,
|
||||
const std::string& to, const std::string& actor,
|
||||
const std::string& note = "") {
|
||||
StatusChange sc;
|
||||
sc.itemId = itemId;
|
||||
sc.fromStatus = from;
|
||||
sc.toStatus = to;
|
||||
sc.timestamp = workItemTimestamp();
|
||||
sc.actor = actor;
|
||||
sc.note = note;
|
||||
history_.push_back(sc);
|
||||
}
|
||||
|
||||
// Full state serialization
|
||||
json toJson() const {
|
||||
json j;
|
||||
j["projectName"] = projectName;
|
||||
j["createdAt"] = createdAt;
|
||||
j["phase"] = workflowPhaseToString(getPhase());
|
||||
|
||||
// Serialize all items from the queue
|
||||
json items = json::array();
|
||||
for (const auto& status : {WI_PENDING, WI_READY, WI_ASSIGNED, WI_IN_PROGRESS,
|
||||
WI_REVIEW, WI_COMPLETE, WI_REJECTED}) {
|
||||
for (const auto& item : queue.getByStatus(status)) {
|
||||
items.push_back(workItemToJson(item));
|
||||
}
|
||||
}
|
||||
j["items"] = items;
|
||||
|
||||
// Serialize history
|
||||
json hist = json::array();
|
||||
for (const auto& sc : history_) {
|
||||
hist.push_back(sc.toJson());
|
||||
}
|
||||
j["history"] = hist;
|
||||
|
||||
j["stats"] = getStats().toJson();
|
||||
return j;
|
||||
}
|
||||
|
||||
// Deserialize from JSON
|
||||
static WorkflowState fromJson(const json& j) {
|
||||
WorkflowState ws;
|
||||
if (j.contains("projectName")) ws.projectName = j["projectName"].get<std::string>();
|
||||
if (j.contains("createdAt")) ws.createdAt = j["createdAt"].get<std::string>();
|
||||
|
||||
if (j.contains("items")) {
|
||||
for (const auto& itemJ : j["items"]) {
|
||||
WorkItem wi = workItemFromJson(itemJ);
|
||||
ws.queue.enqueue(wi);
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("history")) {
|
||||
for (const auto& scJ : j["history"]) {
|
||||
ws.history_.push_back(StatusChange::fromJson(scJ));
|
||||
}
|
||||
}
|
||||
return ws;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<StatusChange> history_;
|
||||
};
|
||||
296
editor/tests/step322_test.cpp
Normal file
296
editor/tests/step322_test.cpp
Normal file
@@ -0,0 +1,296 @@
|
||||
// Step 322: WorkflowState — Project-Level Workflow Tracking (12 tests)
|
||||
// Tests populateFromSkeleton, phase auto-detection, stats, history,
|
||||
// JSON roundtrip, empty workflow, phase transitions.
|
||||
|
||||
#include "WorkflowState.h"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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 {}
|
||||
|
||||
// Helper: build a skeleton module with N functions
|
||||
static Module* makeSkeletonModule(const std::string& name, int funcCount) {
|
||||
auto* mod = createSkeletonModule(name, "python");
|
||||
for (int i = 0; i < funcCount; ++i) {
|
||||
addSkeletonFunction(mod, "func_" + std::to_string(i), {"x"}, "int");
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
// 1. populateFromSkeleton creates correct item count
|
||||
void test_populate_from_skeleton() {
|
||||
TEST(populate_from_skeleton);
|
||||
auto* mod = makeSkeletonModule("proj1", 5);
|
||||
WorkflowState ws("proj1");
|
||||
int count = ws.populateFromSkeleton(mod, "buf-1");
|
||||
CHECK(count == 5, "5 items created, got " + std::to_string(count));
|
||||
CHECK(ws.queue.size() == 5, "queue has 5 items");
|
||||
delete mod;
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 2. Phase auto-detection: empty → Modeling
|
||||
void test_phase_empty() {
|
||||
TEST(phase_empty);
|
||||
WorkflowState ws("empty");
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Modeling, "empty = Modeling");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 3. Phase: all pending → Modeling
|
||||
void test_phase_all_pending() {
|
||||
TEST(phase_all_pending);
|
||||
WorkflowState ws("proj");
|
||||
// Enqueue items with dependencies so they stay pending
|
||||
WorkItem a;
|
||||
a.id = "a"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function";
|
||||
a.priority = "medium"; a.status = WI_PENDING;
|
||||
a.dependencies = {"external_dep"};
|
||||
a.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(a);
|
||||
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Modeling, "all pending = Modeling");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 4. Phase: some assigned → Executing
|
||||
void test_phase_executing() {
|
||||
TEST(phase_executing);
|
||||
WorkflowState ws("proj");
|
||||
|
||||
WorkItem a;
|
||||
a.id = "a1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function";
|
||||
a.priority = "medium"; a.status = WI_ASSIGNED;
|
||||
a.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(a);
|
||||
|
||||
WorkItem b;
|
||||
b.id = "b1"; b.nodeId = "n2"; b.nodeName = "f2"; b.nodeType = "Function";
|
||||
b.priority = "medium"; b.status = WI_READY;
|
||||
b.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(b);
|
||||
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Executing, "some assigned = Executing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 5. Phase: some in review → Reviewing
|
||||
void test_phase_reviewing() {
|
||||
TEST(phase_reviewing);
|
||||
WorkflowState ws("proj");
|
||||
|
||||
WorkItem a;
|
||||
a.id = "r1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function";
|
||||
a.priority = "medium"; a.status = WI_REVIEW;
|
||||
a.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(a);
|
||||
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Reviewing, "some in review = Reviewing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 6. Phase: all complete → Complete
|
||||
void test_phase_complete() {
|
||||
TEST(phase_complete);
|
||||
WorkflowState ws("proj");
|
||||
|
||||
WorkItem a;
|
||||
a.id = "c1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function";
|
||||
a.priority = "medium"; a.status = WI_COMPLETE;
|
||||
a.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(a);
|
||||
|
||||
WorkItem b;
|
||||
b.id = "c2"; b.nodeId = "n2"; b.nodeName = "f2"; b.nodeType = "Function";
|
||||
b.priority = "medium"; b.status = WI_COMPLETE;
|
||||
b.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(b);
|
||||
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Complete, "all complete = Complete");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 7. Stats accuracy
|
||||
void test_stats_accuracy() {
|
||||
TEST(stats_accuracy);
|
||||
WorkflowState ws("proj");
|
||||
|
||||
auto addItem = [&](const std::string& id, const std::string& status,
|
||||
const std::string& wtype, const std::string& prio) {
|
||||
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();
|
||||
ws.queue.enqueue(wi);
|
||||
};
|
||||
|
||||
addItem("s1", WI_READY, "llm", "high");
|
||||
addItem("s2", WI_READY, "llm", "medium");
|
||||
addItem("s3", WI_COMPLETE, "deterministic", "low");
|
||||
addItem("s4", WI_ASSIGNED, "llm", "high");
|
||||
|
||||
auto stats = ws.getStats();
|
||||
CHECK(stats.total == 4, "total=4");
|
||||
CHECK(stats.ready == 2, "ready=2");
|
||||
CHECK(stats.complete == 1, "complete=1");
|
||||
CHECK(stats.assigned == 1, "assigned=1");
|
||||
CHECK(stats.byWorkerType["llm"] == 3, "llm=3");
|
||||
CHECK(stats.byWorkerType["deterministic"] == 1, "deterministic=1");
|
||||
CHECK(stats.byPriority["high"] == 2, "high=2");
|
||||
CHECK(stats.completionPercent > 24.0f && stats.completionPercent < 26.0f, "25%");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 8. History tracking
|
||||
void test_history_tracking() {
|
||||
TEST(history_tracking);
|
||||
WorkflowState ws("proj");
|
||||
ws.recordChange("item1", "", "pending", "system", "created");
|
||||
ws.recordChange("item1", "pending", "ready", "system", "deps met");
|
||||
ws.recordChange("item1", "ready", "assigned", "routing-engine", "routed to llm");
|
||||
|
||||
auto hist = ws.getHistory("item1");
|
||||
CHECK(hist.size() == 3, "3 history entries");
|
||||
CHECK(hist[0].fromStatus == "", "first from empty");
|
||||
CHECK(hist[0].toStatus == "pending", "first to pending");
|
||||
CHECK(hist[2].actor == "routing-engine", "third actor");
|
||||
|
||||
// Other item has no history
|
||||
auto hist2 = ws.getHistory("item2");
|
||||
CHECK(hist2.empty(), "no history for item2");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 9. JSON roundtrip — full state preserved
|
||||
void test_json_roundtrip() {
|
||||
TEST(json_roundtrip);
|
||||
WorkflowState ws("myproject");
|
||||
|
||||
WorkItem a;
|
||||
a.id = "j1"; a.nodeId = "n1"; a.nodeName = "compute"; a.nodeType = "Function";
|
||||
a.priority = "high"; a.workerType = "llm"; a.status = WI_READY;
|
||||
a.createdAt = "2026-01-01T00:00:00Z";
|
||||
ws.queue.enqueue(a);
|
||||
|
||||
WorkItem b;
|
||||
b.id = "j2"; b.nodeId = "n2"; b.nodeName = "validate"; b.nodeType = "Function";
|
||||
b.priority = "medium"; b.workerType = "deterministic"; b.status = WI_COMPLETE;
|
||||
b.createdAt = "2026-01-01T00:01:00Z";
|
||||
ws.queue.enqueue(b);
|
||||
|
||||
ws.recordChange("j1", "", "ready", "system");
|
||||
ws.recordChange("j2", "", "complete", "worker:det-001");
|
||||
|
||||
json j = ws.toJson();
|
||||
WorkflowState ws2 = WorkflowState::fromJson(j);
|
||||
|
||||
CHECK(ws2.projectName == "myproject", "projectName");
|
||||
CHECK(ws2.queue.size() == 2, "2 items");
|
||||
CHECK(ws2.getHistory("j1").size() == 1, "j1 history preserved, got " +
|
||||
std::to_string(ws2.getHistory("j1").size()));
|
||||
CHECK(ws2.getHistory("j2").size() == 1, "j2 history preserved");
|
||||
|
||||
auto item1 = ws2.queue.getItem("j1");
|
||||
CHECK(item1.has_value(), "j1 exists");
|
||||
CHECK(item1->nodeName == "compute", "j1 name");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 10. Empty workflow behavior
|
||||
void test_empty_workflow() {
|
||||
TEST(empty_workflow);
|
||||
WorkflowState ws("empty");
|
||||
auto stats = ws.getStats();
|
||||
CHECK(stats.total == 0, "total=0");
|
||||
CHECK(stats.completionPercent == 0.0f, "0% complete");
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Modeling, "Modeling phase");
|
||||
CHECK(ws.getHistory("any").empty(), "no history");
|
||||
|
||||
json j = ws.toJson();
|
||||
CHECK(j.contains("projectName"), "has projectName");
|
||||
CHECK(j["items"].size() == 0, "no items");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 11. Phase transitions through lifecycle
|
||||
void test_phase_transitions() {
|
||||
TEST(phase_transitions);
|
||||
WorkflowState ws("proj");
|
||||
|
||||
// Start empty → Modeling
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Modeling, "start=Modeling");
|
||||
|
||||
// Add ready items → Routing
|
||||
WorkItem a;
|
||||
a.id = "pt1"; a.nodeId = "n1"; a.nodeName = "f1"; a.nodeType = "Function";
|
||||
a.priority = "medium"; a.status = WI_READY;
|
||||
a.createdAt = workItemTimestamp();
|
||||
ws.queue.enqueue(a);
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Routing, "with ready=Routing");
|
||||
|
||||
// Assign → Executing
|
||||
ws.queue.dequeue();
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Executing, "with assigned=Executing");
|
||||
|
||||
// Move to review
|
||||
WorkItem updated = *ws.queue.getItem("pt1");
|
||||
transitionWorkItem(updated, WI_IN_PROGRESS);
|
||||
ws.queue.updateItem("pt1", updated);
|
||||
updated = *ws.queue.getItem("pt1");
|
||||
transitionWorkItem(updated, WI_REVIEW);
|
||||
ws.queue.updateItem("pt1", updated);
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Reviewing, "with review=Reviewing");
|
||||
|
||||
// Complete
|
||||
ws.queue.complete("pt1");
|
||||
CHECK(ws.getPhase() == WorkflowPhase::Complete, "all complete=Complete");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// 12. populateFromSkeleton records history entries
|
||||
void test_populate_records_history() {
|
||||
TEST(populate_records_history);
|
||||
auto* mod = makeSkeletonModule("proj2", 3);
|
||||
WorkflowState ws("proj2");
|
||||
ws.populateFromSkeleton(mod, "buf-2");
|
||||
|
||||
// Each item should have a creation history entry
|
||||
int histCount = 0;
|
||||
for (const auto& status : {WI_PENDING, WI_READY}) {
|
||||
for (const auto& item : ws.queue.getByStatus(status)) {
|
||||
auto h = ws.getHistory(item.id);
|
||||
if (!h.empty()) histCount++;
|
||||
}
|
||||
}
|
||||
CHECK(histCount == 3, "3 items have history, got " + std::to_string(histCount));
|
||||
delete mod;
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "=== Step 322: WorkflowState Project-Level Workflow Tracking ===\n";
|
||||
try {
|
||||
test_populate_from_skeleton();
|
||||
test_phase_empty();
|
||||
test_phase_all_pending();
|
||||
test_phase_executing();
|
||||
test_phase_reviewing();
|
||||
test_phase_complete();
|
||||
test_stats_accuracy();
|
||||
test_history_tracking();
|
||||
test_json_roundtrip();
|
||||
test_empty_workflow();
|
||||
test_phase_transitions();
|
||||
test_populate_records_history();
|
||||
} 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;
|
||||
}
|
||||
20
progress.md
20
progress.md
@@ -1245,6 +1245,26 @@ by priority (critical first) then creation time.
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — step321_test target
|
||||
|
||||
### Step 322: WorkflowState — Project-Level Workflow Tracking
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Top-level state managing the full workflow lifecycle for a project: from skeleton
|
||||
creation through routing, execution, review, and completion. Auto-detects phase
|
||||
from item statuses, tracks audit trail, computes stats.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/WorkflowState.h` — WorkflowPhase enum (Modeling/Routing/Executing/
|
||||
Reviewing/Complete), StatusChange audit trail struct, WorkflowStats struct,
|
||||
WorkflowState class with populateFromSkeleton, getStats, getPhase (auto-computed),
|
||||
getHistory, recordChange, toJson/fromJson
|
||||
- `editor/tests/step322_test.cpp` — 12 tests: populate from skeleton, phase
|
||||
auto-detection (empty, all pending, executing, reviewing, complete), stats
|
||||
accuracy, history tracking, JSON roundtrip, empty workflow, phase transitions
|
||||
through lifecycle, populate records history
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — step322_test target
|
||||
|
||||
---
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
Reference in New Issue
Block a user