Step 381: add workflow progress and ETA tracker
This commit is contained in:
196
editor/tests/step381_test.cpp
Normal file
196
editor/tests/step381_test.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
// Step 381: Workflow progress + ETA tracking (12 tests)
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "WorkflowProgress.h"
|
||||
|
||||
static OrchestratorEvent eventOf(const std::string& type,
|
||||
const std::string& itemId,
|
||||
const std::string& timestamp,
|
||||
const json& detail = json::object()) {
|
||||
OrchestratorEvent ev;
|
||||
ev.type = type;
|
||||
ev.itemId = itemId;
|
||||
ev.timestamp = timestamp;
|
||||
ev.detail = detail;
|
||||
return ev;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
|
||||
// Test 1: completion percent tracks completed/total
|
||||
{
|
||||
WorkflowProgress progress(4);
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("completed", "b", "2026-02-16T10:01:00Z"));
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(snap.totalItems == 4);
|
||||
assert(snap.completedItems == 2);
|
||||
assert(std::fabs(snap.completionPercent - 50.0f) < 0.01f);
|
||||
std::cout << "Test 1 PASSED: completion percent accurate\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 2: throughput computed from elapsed time and completions
|
||||
{
|
||||
WorkflowProgress progress(3);
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("completed", "b", "2026-02-16T10:02:00Z"));
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(std::fabs(snap.itemsPerMinute - 1.0f) < 0.01f);
|
||||
std::cout << "Test 2 PASSED: throughput computed\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 3: ETA is close for uniform completion cadence
|
||||
{
|
||||
WorkflowProgress progress(4);
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("completed", "b", "2026-02-16T10:02:00Z"));
|
||||
auto snap = progress.getSnapshot();
|
||||
// 2 done in 2 minutes => 1/min; remaining 2 => ~2 minutes.
|
||||
assert(snap.estimatedRemainingMinutes >= 1.0f);
|
||||
assert(snap.estimatedRemainingMinutes <= 4.0f);
|
||||
std::cout << "Test 3 PASSED: ETA stays within reasonable bound\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 4: worker stats tracked per worker type
|
||||
{
|
||||
WorkflowProgress progress(2);
|
||||
progress.recordEvent(eventOf("routed", "a", "2026-02-16T10:00:00Z",
|
||||
json{{"workerType", "template"}}));
|
||||
progress.recordEvent(eventOf("executed", "a", "2026-02-16T10:00:10Z",
|
||||
json{{"workerType", "template"},
|
||||
{"confidence", 0.9},
|
||||
{"tokensGenerated", 40}}));
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:20Z"));
|
||||
progress.recordEvent(eventOf("routed", "b", "2026-02-16T10:00:30Z",
|
||||
json{{"workerType", "llm"}}));
|
||||
progress.recordEvent(eventOf("executed", "b", "2026-02-16T10:01:00Z",
|
||||
json{{"workerType", "llm"},
|
||||
{"confidence", 0.6},
|
||||
{"tokensGenerated", 200}}));
|
||||
auto byWorker = progress.getWorkerStats();
|
||||
assert(byWorker.count("template") == 1);
|
||||
assert(byWorker.count("llm") == 1);
|
||||
assert(byWorker["template"].completed == 1);
|
||||
assert(byWorker["llm"].completed == 0);
|
||||
std::cout << "Test 4 PASSED: worker stats grouped by worker type\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 5: rejection rate updates when rejected events are recorded
|
||||
{
|
||||
WorkflowProgress progress(1);
|
||||
progress.recordEvent(eventOf("routed", "a", "2026-02-16T10:00:00Z",
|
||||
json{{"workerType", "slm"}}));
|
||||
progress.recordEvent(eventOf("executed", "a", "2026-02-16T10:00:20Z",
|
||||
json{{"workerType", "slm"},
|
||||
{"confidence", 0.7},
|
||||
{"tokensGenerated", 80}}));
|
||||
progress.recordEvent(eventOf("rejected", "a", "2026-02-16T10:00:25Z"));
|
||||
auto stats = progress.getWorkerStats();
|
||||
assert(stats.count("slm") == 1);
|
||||
assert(std::fabs(stats["slm"].rejectionRate - 1.0f) < 0.01f);
|
||||
std::cout << "Test 5 PASSED: rejection rate tracked\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 6: timeline records all events in order
|
||||
{
|
||||
WorkflowProgress progress(1);
|
||||
progress.recordEvent(eventOf("routed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("executed", "a", "2026-02-16T10:00:10Z"));
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:20Z"));
|
||||
auto timeline = progress.getTimeline();
|
||||
assert(timeline.size() == 3);
|
||||
assert(timeline[0].type == "routed");
|
||||
assert(timeline[2].type == "completed");
|
||||
std::cout << "Test 6 PASSED: timeline event log preserved\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 7: empty snapshot shows zero progress and unknown ETA
|
||||
{
|
||||
WorkflowProgress progress;
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(snap.completionPercent == 0.0f);
|
||||
assert(snap.estimatedRemainingMinutes < 0.0f);
|
||||
std::cout << "Test 7 PASSED: empty workflow snapshot sane\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 8: blockers included from blocked event payload
|
||||
{
|
||||
WorkflowProgress progress(2);
|
||||
progress.recordEvent(eventOf("blocked", "", "2026-02-16T10:00:00Z",
|
||||
json{{"blockers", json::array({
|
||||
json{{"type", "needs-human"},
|
||||
{"itemIds", json::array({"x"})},
|
||||
{"description", "manual"}}})}}));
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(snap.blockers.size() == 1);
|
||||
assert(snap.blockers[0].type == "needs-human");
|
||||
std::cout << "Test 8 PASSED: blockers surfaced in snapshot\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 9: snapshot JSON serialization has expected fields
|
||||
{
|
||||
WorkflowProgress progress(1);
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:00Z"));
|
||||
auto j = progress.getSnapshot().toJson();
|
||||
assert(j.contains("totalItems"));
|
||||
assert(j.contains("completionPercent"));
|
||||
assert(j.contains("byWorkerType"));
|
||||
assert(j.contains("blockers"));
|
||||
std::cout << "Test 9 PASSED: snapshot serializes to JSON\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 10: completion percent stays monotonic under rejections
|
||||
{
|
||||
WorkflowProgress progress(3);
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:00:00Z"));
|
||||
float p1 = progress.getSnapshot().completionPercent;
|
||||
progress.recordEvent(eventOf("rejected", "b", "2026-02-16T10:00:10Z"));
|
||||
float p2 = progress.getSnapshot().completionPercent;
|
||||
progress.recordEvent(eventOf("completed", "b", "2026-02-16T10:01:00Z"));
|
||||
float p3 = progress.getSnapshot().completionPercent;
|
||||
assert(p2 >= p1);
|
||||
assert(p3 >= p2);
|
||||
std::cout << "Test 10 PASSED: completion stays monotonic\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 11: startedAt and lastActivityAt are tracked
|
||||
{
|
||||
WorkflowProgress progress(1);
|
||||
progress.recordEvent(eventOf("routed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("completed", "a", "2026-02-16T10:02:00Z"));
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(snap.startedAt == "2026-02-16T10:00:00Z");
|
||||
assert(snap.lastActivityAt == "2026-02-16T10:02:00Z");
|
||||
std::cout << "Test 11 PASSED: activity timestamps tracked\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
// Test 12: total items fallback to observed unique item ids
|
||||
{
|
||||
WorkflowProgress progress;
|
||||
progress.recordEvent(eventOf("routed", "a", "2026-02-16T10:00:00Z"));
|
||||
progress.recordEvent(eventOf("routed", "b", "2026-02-16T10:00:10Z"));
|
||||
auto snap = progress.getSnapshot();
|
||||
assert(snap.totalItems == 2);
|
||||
std::cout << "Test 12 PASSED: observed-item fallback for total count\n";
|
||||
passed++;
|
||||
}
|
||||
|
||||
std::cout << "\nResults: " << passed << "/12\n";
|
||||
assert(passed == 12);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user