diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9fd0306..6561be3 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4270,4 +4270,13 @@ target_link_libraries(step598_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step599_test tests/step599_test.cpp) +target_include_directories(step599_test PRIVATE src) +target_link_libraries(step599_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/IncidentResponseRunbookEngine.h b/editor/src/IncidentResponseRunbookEngine.h new file mode 100644 index 0000000..d9d8f6b --- /dev/null +++ b/editor/src/IncidentResponseRunbookEngine.h @@ -0,0 +1,106 @@ +#pragma once +// Step 599: Incident Response Runbook Engine + +#include +#include +#include + +struct RunbookStep { + std::string stepId; + std::string instruction; + bool required = true; +}; + +struct Runbook { + std::string runbookId; + std::string incidentType; + std::vector steps; +}; + +struct RunbookExecutionState { + std::string runbookId; + std::vector completedStepIds; + bool closed = false; +}; + +class IncidentResponseRunbookEngine { +public: + bool registerRunbook(const Runbook& runbook, std::string* error) { + if (!error) return false; + error->clear(); + if (runbook.runbookId.empty()) return fail(error, "runbook_id_missing"); + if (runbook.incidentType.empty()) return fail(error, "incident_type_missing"); + if (runbook.steps.empty()) return fail(error, "runbook_steps_missing"); + if (runbooks_.count(runbook.runbookId) != 0) return fail(error, "runbook_duplicate"); + runbooks_[runbook.runbookId] = runbook; + return true; + } + + bool start(const std::string& runbookId, RunbookExecutionState* state, std::string* error) const { + if (!state || !error) return false; + error->clear(); + if (runbooks_.count(runbookId) == 0) return fail(error, "runbook_missing"); + state->runbookId = runbookId; + state->completedStepIds.clear(); + state->closed = false; + return true; + } + + bool completeStep(RunbookExecutionState* state, + const std::string& stepId, + std::string* error) const { + if (!state || !error) return false; + error->clear(); + const auto* rb = findRunbook(state->runbookId); + if (!rb) return fail(error, "runbook_missing"); + if (state->closed) return fail(error, "execution_closed"); + if (!hasStep(*rb, stepId)) return fail(error, "step_missing"); + for (const auto& s : state->completedStepIds) if (s == stepId) return fail(error, "step_already_completed"); + state->completedStepIds.push_back(stepId); + return true; + } + + bool closeIfReady(RunbookExecutionState* state, std::string* error) const { + if (!state || !error) return false; + error->clear(); + const auto* rb = findRunbook(state->runbookId); + if (!rb) return fail(error, "runbook_missing"); + for (const auto& step : rb->steps) { + if (!step.required) continue; + if (!isCompleted(*state, step.stepId)) return fail(error, "required_steps_incomplete"); + } + state->closed = true; + return true; + } + + int completionPercent(const RunbookExecutionState& state) const { + const auto* rb = findRunbook(state.runbookId); + if (!rb || rb->steps.empty()) return 0; + const int done = static_cast(state.completedStepIds.size()); + return (done * 100) / static_cast(rb->steps.size()); + } + +private: + std::map runbooks_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } + + const Runbook* findRunbook(const std::string& runbookId) const { + auto it = runbooks_.find(runbookId); + if (it == runbooks_.end()) return nullptr; + return &it->second; + } + + static bool hasStep(const Runbook& runbook, const std::string& stepId) { + for (const auto& s : runbook.steps) if (s.stepId == stepId) return true; + return false; + } + + static bool isCompleted(const RunbookExecutionState& state, const std::string& stepId) { + for (const auto& s : state.completedStepIds) if (s == stepId) return true; + return false; + } +}; diff --git a/editor/tests/step599_test.cpp b/editor/tests/step599_test.cpp new file mode 100644 index 0000000..d72c8c9 --- /dev/null +++ b/editor/tests/step599_test.cpp @@ -0,0 +1,179 @@ +// Step 599: Incident Response Runbook Engine (12 tests) + +#include "IncidentResponseRunbookEngine.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 Runbook runbook() { + Runbook rb; + rb.runbookId = "rb-1"; + rb.incidentType = "crash"; + rb.steps = { + {"s1", "collect logs", true}, + {"s2", "restart services", true}, + {"s3", "postmortem draft", false} + }; + return rb; +} + +void test_register_runbook_success() { + TEST(register_runbook_success); + IncidentResponseRunbookEngine e; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register should succeed"); + PASS(); +} + +void test_register_runbook_rejects_missing_id() { + TEST(register_runbook_rejects_missing_id); + IncidentResponseRunbookEngine e; + std::string error; + auto rb = runbook(); + rb.runbookId.clear(); + CHECK(!e.registerRunbook(rb, &error), "register should fail"); + CHECK(error == "runbook_id_missing", "wrong error"); + PASS(); +} + +void test_register_runbook_rejects_missing_steps() { + TEST(register_runbook_rejects_missing_steps); + IncidentResponseRunbookEngine e; + std::string error; + auto rb = runbook(); + rb.steps.clear(); + CHECK(!e.registerRunbook(rb, &error), "register should fail"); + CHECK(error == "runbook_steps_missing", "wrong error"); + PASS(); +} + +void test_register_runbook_rejects_duplicate() { + TEST(register_runbook_rejects_duplicate); + IncidentResponseRunbookEngine e; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "first register failed"); + CHECK(!e.registerRunbook(runbook(), &error), "duplicate should fail"); + CHECK(error == "runbook_duplicate", "wrong error"); + PASS(); +} + +void test_start_execution_success() { + TEST(start_execution_success); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start should succeed"); + CHECK(s.runbookId == "rb-1", "runbook id mismatch"); + PASS(); +} + +void test_start_execution_fails_for_missing_runbook() { + TEST(start_execution_fails_for_missing_runbook); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(!e.start("missing", &s, &error), "start should fail"); + CHECK(error == "runbook_missing", "wrong error"); + PASS(); +} + +void test_complete_step_success() { + TEST(complete_step_success); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(e.completeStep(&s, "s1", &error), "complete should succeed"); + CHECK(s.completedStepIds.size() == 1, "completed size mismatch"); + PASS(); +} + +void test_complete_step_rejects_unknown_step() { + TEST(complete_step_rejects_unknown_step); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(!e.completeStep(&s, "missing", &error), "complete should fail"); + CHECK(error == "step_missing", "wrong error"); + PASS(); +} + +void test_complete_step_rejects_duplicate_completion() { + TEST(complete_step_rejects_duplicate_completion); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(e.completeStep(&s, "s1", &error), "complete failed"); + CHECK(!e.completeStep(&s, "s1", &error), "duplicate complete should fail"); + CHECK(error == "step_already_completed", "wrong error"); + PASS(); +} + +void test_close_if_ready_requires_required_steps() { + TEST(close_if_ready_requires_required_steps); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(!e.closeIfReady(&s, &error), "close should fail"); + CHECK(error == "required_steps_incomplete", "wrong error"); + PASS(); +} + +void test_close_if_ready_succeeds_after_required_steps() { + TEST(close_if_ready_succeeds_after_required_steps); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(e.completeStep(&s, "s1", &error), "s1 complete failed"); + CHECK(e.completeStep(&s, "s2", &error), "s2 complete failed"); + CHECK(e.closeIfReady(&s, &error), "close should succeed"); + CHECK(s.closed, "state should be closed"); + PASS(); +} + +void test_completion_percent_computed_from_steps() { + TEST(completion_percent_computed_from_steps); + IncidentResponseRunbookEngine e; + RunbookExecutionState s; + std::string error; + CHECK(e.registerRunbook(runbook(), &error), "register failed"); + CHECK(e.start("rb-1", &s, &error), "start failed"); + CHECK(e.completeStep(&s, "s1", &error), "complete failed"); + CHECK(e.completionPercent(s) == 33, "completion percent mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 599: Incident Response Runbook Engine\n"; + + test_register_runbook_success(); // 1 + test_register_runbook_rejects_missing_id(); // 2 + test_register_runbook_rejects_missing_steps(); // 3 + test_register_runbook_rejects_duplicate(); // 4 + test_start_execution_success(); // 5 + test_start_execution_fails_for_missing_runbook();// 6 + test_complete_step_success(); // 7 + test_complete_step_rejects_unknown_step(); // 8 + test_complete_step_rejects_duplicate_completion();// 9 + test_close_if_ready_requires_required_steps(); // 10 + test_close_if_ready_succeeds_after_required_steps();// 11 + test_completion_percent_computed_from_steps(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 4caf480..181816e 100644 --- a/progress.md +++ b/progress.md @@ -11508,3 +11508,37 @@ guardrail evaluation, escalation planning, audit capture, and drift detection. **Phase 34a totals (594-598):** - **Steps completed:** 5 - **New tests in this phase plan:** 56/56 passing + +### Step 599: Incident Response Runbook Engine +**Status:** PASS (12/12 tests) + +Implements incident response runbook execution with required-step gating, +completion tracking, and close-readiness validation. + +**Files added:** +- `editor/src/IncidentResponseRunbookEngine.h` - runbook engine module: + - runbook registration with validation/duplicate guards + - runbook execution start state + - step completion lifecycle with dedupe checks + - required-step close gate + - completion-percent helper +- `editor/tests/step599_test.cpp` - 12 tests covering: + - runbook registration success/failure behavior + - execution start behavior + - step completion success/failure behavior + - close readiness behavior + - completion percentage behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step599_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step599_test step598_test` - PASS +- `./editor/build-native/step599_test` - PASS (12/12) +- `./editor/build-native/step598_test` - PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/IncidentResponseRunbookEngine.h` within header-size limit (`106` <= `600`) +- `editor/tests/step599_test.cpp` within test-file size guidance (`179` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`