diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ab710f0..9f9dad7 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4333,4 +4333,13 @@ target_link_libraries(step605_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step606_test tests/step606_test.cpp) +target_include_directories(step606_test PRIVATE src) +target_link_libraries(step606_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) diff --git a/editor/src/RollbackDrillTracker.h b/editor/src/RollbackDrillTracker.h new file mode 100644 index 0000000..aa371e1 --- /dev/null +++ b/editor/src/RollbackDrillTracker.h @@ -0,0 +1,93 @@ +#pragma once +// Step 606: Rollback Drill Tracker + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +enum class DrillOutcome { + Scheduled, + Passed, + Failed +}; + +struct RollbackDrillRecord { + std::string drillId; + std::string environmentId; + int recoveryMinutes = 0; + DrillOutcome outcome = DrillOutcome::Scheduled; + std::string notes; +}; + +class RollbackDrillTracker { +public: + bool schedule(const RollbackDrillRecord& drill, std::string* error) { + if (!error) return false; + error->clear(); + if (drill.drillId.empty()) return failWith(error, "drill_id_missing"); + if (drill.environmentId.empty()) return failWith(error, "environment_id_missing"); + if (drill.recoveryMinutes < 0) return failWith(error, "recovery_minutes_invalid"); + if (drill.outcome != DrillOutcome::Scheduled) return failWith(error, "outcome_invalid"); + if (records_.count(drill.drillId) != 0) return failWith(error, "drill_duplicate"); + records_[drill.drillId] = drill; + order_.push_back(drill.drillId); + return true; + } + + bool complete(const std::string& drillId, + DrillOutcome outcome, + int recoveryMinutes, + const std::string& notes, + std::string* error) { + if (!error) return false; + error->clear(); + auto it = records_.find(drillId); + if (it == records_.end()) return failWith(error, "drill_missing"); + if (outcome == DrillOutcome::Scheduled) return failWith(error, "outcome_invalid"); + if (recoveryMinutes < 0) return failWith(error, "recovery_minutes_invalid"); + it->second.outcome = outcome; + it->second.recoveryMinutes = recoveryMinutes; + it->second.notes = notes; + return true; + } + + int passCount() const { + int count = 0; + for (const auto& id : order_) if (records_.at(id).outcome == DrillOutcome::Passed) ++count; + return count; + } + + int failCount() const { + int count = 0; + for (const auto& id : order_) if (records_.at(id).outcome == DrillOutcome::Failed) ++count; + return count; + } + + int averageRecoveryMinutes() const { + int total = 0; + int count = 0; + for (const auto& id : order_) { + const auto& record = records_.at(id); + if (record.outcome == DrillOutcome::Passed || record.outcome == DrillOutcome::Failed) { + total += record.recoveryMinutes; + ++count; + } + } + return count == 0 ? 0 : total / count; + } + + std::vector byEnvironment(const std::string& environmentId) const { + std::vector out; + for (const auto& id : order_) { + const auto& record = records_.at(id); + if (environmentId.empty() || record.environmentId == environmentId) out.push_back(record); + } + return out; + } + +private: + std::map records_; + std::vector order_; +}; diff --git a/editor/tests/step606_test.cpp b/editor/tests/step606_test.cpp new file mode 100644 index 0000000..c1e69b4 --- /dev/null +++ b/editor/tests/step606_test.cpp @@ -0,0 +1,153 @@ +// Step 606: Rollback Drill Tracker (12 tests) + +#include "RollbackDrillTracker.h" + +#include + +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 RollbackDrillRecord drill(const std::string& id, + const std::string& env) { + return {id, env, 0, DrillOutcome::Scheduled, ""}; +} + +void test_schedule_success() { + TEST(schedule_success); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule should succeed"); + CHECK(tracker.byEnvironment("prod").size() == 1, "env count mismatch"); + PASS(); +} + +void test_schedule_rejects_missing_drill_id() { + TEST(schedule_rejects_missing_drill_id); + RollbackDrillTracker tracker; + std::string error; + CHECK(!tracker.schedule(drill("", "prod"), &error), "schedule should fail"); + CHECK(error == "drill_id_missing", "wrong error"); + PASS(); +} + +void test_schedule_rejects_missing_environment_id() { + TEST(schedule_rejects_missing_environment_id); + RollbackDrillTracker tracker; + std::string error; + CHECK(!tracker.schedule(drill("d1", ""), &error), "schedule should fail"); + CHECK(error == "environment_id_missing", "wrong error"); + PASS(); +} + +void test_schedule_rejects_duplicate() { + TEST(schedule_rejects_duplicate); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "first schedule failed"); + CHECK(!tracker.schedule(drill("d1", "staging"), &error), "duplicate should fail"); + CHECK(error == "drill_duplicate", "wrong error"); + PASS(); +} + +void test_complete_pass_updates_counts() { + TEST(complete_pass_updates_counts); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule failed"); + CHECK(tracker.complete("d1", DrillOutcome::Passed, 7, "ok", &error), "complete failed"); + CHECK(tracker.passCount() == 1, "pass count mismatch"); + CHECK(tracker.failCount() == 0, "fail count mismatch"); + PASS(); +} + +void test_complete_fail_updates_counts() { + TEST(complete_fail_updates_counts); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule failed"); + CHECK(tracker.complete("d1", DrillOutcome::Failed, 15, "issue", &error), "complete failed"); + CHECK(tracker.failCount() == 1, "fail count mismatch"); + PASS(); +} + +void test_complete_rejects_missing_drill() { + TEST(complete_rejects_missing_drill); + RollbackDrillTracker tracker; + std::string error; + CHECK(!tracker.complete("missing", DrillOutcome::Passed, 5, "", &error), "complete should fail"); + CHECK(error == "drill_missing", "wrong error"); + PASS(); +} + +void test_complete_rejects_scheduled_outcome() { + TEST(complete_rejects_scheduled_outcome); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule failed"); + CHECK(!tracker.complete("d1", DrillOutcome::Scheduled, 5, "", &error), "complete should fail"); + CHECK(error == "outcome_invalid", "wrong error"); + PASS(); +} + +void test_complete_rejects_negative_recovery_minutes() { + TEST(complete_rejects_negative_recovery_minutes); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule failed"); + CHECK(!tracker.complete("d1", DrillOutcome::Passed, -1, "", &error), "complete should fail"); + CHECK(error == "recovery_minutes_invalid", "wrong error"); + PASS(); +} + +void test_average_recovery_minutes_zero_when_no_completed_drills() { + TEST(average_recovery_minutes_zero_when_no_completed_drills); + RollbackDrillTracker tracker; + CHECK(tracker.averageRecoveryMinutes() == 0, "average should be zero"); + PASS(); +} + +void test_average_recovery_minutes_computes_completed_only() { + TEST(average_recovery_minutes_computes_completed_only); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule d1 failed"); + CHECK(tracker.schedule(drill("d2", "prod"), &error), "schedule d2 failed"); + CHECK(tracker.schedule(drill("d3", "staging"), &error), "schedule d3 failed"); + CHECK(tracker.complete("d1", DrillOutcome::Passed, 10, "", &error), "complete d1 failed"); + CHECK(tracker.complete("d2", DrillOutcome::Failed, 20, "", &error), "complete d2 failed"); + CHECK(tracker.averageRecoveryMinutes() == 15, "average mismatch"); + PASS(); +} + +void test_by_environment_empty_returns_all() { + TEST(by_environment_empty_returns_all); + RollbackDrillTracker tracker; + std::string error; + CHECK(tracker.schedule(drill("d1", "prod"), &error), "schedule d1 failed"); + CHECK(tracker.schedule(drill("d2", "staging"), &error), "schedule d2 failed"); + CHECK(tracker.byEnvironment("").size() == 2, "empty env should return all"); + PASS(); +} + +int main() { + std::cout << "Step 606: Rollback Drill Tracker\n"; + + test_schedule_success(); // 1 + test_schedule_rejects_missing_drill_id(); // 2 + test_schedule_rejects_missing_environment_id(); // 3 + test_schedule_rejects_duplicate(); // 4 + test_complete_pass_updates_counts(); // 5 + test_complete_fail_updates_counts(); // 6 + test_complete_rejects_missing_drill(); // 7 + test_complete_rejects_scheduled_outcome(); // 8 + test_complete_rejects_negative_recovery_minutes(); // 9 + test_average_recovery_minutes_zero_when_no_completed_drills(); // 10 + test_average_recovery_minutes_computes_completed_only(); // 11 + test_by_environment_empty_returns_all(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 610b903..73a4cb6 100644 --- a/progress.md +++ b/progress.md @@ -11770,3 +11770,33 @@ blocked-environment reporting. - `editor/src/DeploymentPromotionGate.h` within header-size limit (`76` <= `600`) - `editor/tests/step605_test.cpp` within test-file size guidance (`149` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 606: Rollback Drill Tracker +**Status:** PASS (12/12 tests) + +Implements rollback drill lifecycle tracking with completion outcomes and +recovery-time metrics. + +**Files added:** +- `editor/src/RollbackDrillTracker.h` - rollback drill module: + - drill scheduling with validation/duplicate guards + - completion lifecycle with pass/fail outcomes and recovery time + - pass/fail counters, average recovery-time, and environment filtering +- `editor/tests/step606_test.cpp` - 12 tests covering: + - schedule success/failure behavior + - completion success/failure behavior and invalid transitions + - aggregate counters and average/filter behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step606_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step606_test step605_test` - PASS +- `./editor/build-native/step606_test` - PASS (12/12) +- `./editor/build-native/step605_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/RollbackDrillTracker.h` within header-size limit (`93` <= `600`) +- `editor/tests/step606_test.cpp` within test-file size guidance (`153` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`