197 lines
8.2 KiB
C++
197 lines
8.2 KiB
C++
|
|
// 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;
|
||
|
|
}
|