diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 9f9dad7..b01f21e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4342,4 +4342,13 @@ target_link_libraries(step606_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step607_test tests/step607_test.cpp) +target_include_directories(step607_test PRIVATE src) +target_link_libraries(step607_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/OperationalSLOWatch.h b/editor/src/OperationalSLOWatch.h new file mode 100644 index 0000000..99f332a --- /dev/null +++ b/editor/src/OperationalSLOWatch.h @@ -0,0 +1,64 @@ +#pragma once +// Step 607: Operational SLO Watch + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +struct SLOIndicator { + std::string indicatorId; + std::string serviceId; + int targetPercent = 0; + int observedPercent = 0; + int errorBudgetBurn = 0; +}; + +class OperationalSLOWatch { +public: + bool upsert(const SLOIndicator& indicator, std::string* error) { + if (!error) return false; + error->clear(); + if (indicator.indicatorId.empty()) return failWith(error, "indicator_id_missing"); + if (indicator.serviceId.empty()) return failWith(error, "service_id_missing"); + if (indicator.targetPercent < 0 || indicator.targetPercent > 100) return failWith(error, "target_percent_invalid"); + if (indicator.observedPercent < 0 || indicator.observedPercent > 100) return failWith(error, "observed_percent_invalid"); + if (indicator.errorBudgetBurn < 0) return failWith(error, "error_budget_burn_invalid"); + if (items_.count(indicator.indicatorId) == 0) order_.push_back(indicator.indicatorId); + items_[indicator.indicatorId] = indicator; + return true; + } + + bool breaching(const std::string& indicatorId, std::string* error) const { + if (!error) return false; + error->clear(); + auto it = items_.find(indicatorId); + if (it == items_.end()) return failWith(error, "indicator_missing"); + const auto& i = it->second; + return i.observedPercent < i.targetPercent || i.errorBudgetBurn > 100; + } + + int serviceBreachCount(const std::string& serviceId) const { + int count = 0; + for (const auto& id : order_) { + const auto& i = items_.at(id); + if (i.serviceId == serviceId && + (i.observedPercent < i.targetPercent || i.errorBudgetBurn > 100)) ++count; + } + return count; + } + + std::vector byService(const std::string& serviceId) const { + std::vector out; + for (const auto& id : order_) { + const auto& i = items_.at(id); + if (serviceId.empty() || i.serviceId == serviceId) out.push_back(i); + } + return out; + } + +private: + std::map items_; + std::vector order_; +}; diff --git a/editor/tests/step607_test.cpp b/editor/tests/step607_test.cpp new file mode 100644 index 0000000..dd5c38b --- /dev/null +++ b/editor/tests/step607_test.cpp @@ -0,0 +1,150 @@ +// Step 607: Operational SLO Watch (12 tests) + +#include "OperationalSLOWatch.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 SLOIndicator indicator(const std::string& id, + const std::string& service, + int target, + int observed, + int burn) { + return {id, service, target, observed, burn}; +} + +void test_upsert_success() { + TEST(upsert_success); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 99, 50), &error), "upsert should succeed"); + CHECK(watch.byService("svc-a").size() == 1, "service count mismatch"); + PASS(); +} + +void test_upsert_rejects_missing_indicator_id() { + TEST(upsert_rejects_missing_indicator_id); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.upsert(indicator("", "svc-a", 99, 99, 50), &error), "upsert should fail"); + CHECK(error == "indicator_id_missing", "wrong error"); + PASS(); +} + +void test_upsert_rejects_missing_service_id() { + TEST(upsert_rejects_missing_service_id); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.upsert(indicator("i1", "", 99, 99, 50), &error), "upsert should fail"); + CHECK(error == "service_id_missing", "wrong error"); + PASS(); +} + +void test_upsert_rejects_invalid_target_percent() { + TEST(upsert_rejects_invalid_target_percent); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.upsert(indicator("i1", "svc-a", 120, 99, 50), &error), "upsert should fail"); + CHECK(error == "target_percent_invalid", "wrong error"); + PASS(); +} + +void test_upsert_rejects_invalid_observed_percent() { + TEST(upsert_rejects_invalid_observed_percent); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.upsert(indicator("i1", "svc-a", 99, -1, 50), &error), "upsert should fail"); + CHECK(error == "observed_percent_invalid", "wrong error"); + PASS(); +} + +void test_upsert_rejects_negative_error_budget_burn() { + TEST(upsert_rejects_negative_error_budget_burn); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.upsert(indicator("i1", "svc-a", 99, 99, -1), &error), "upsert should fail"); + CHECK(error == "error_budget_burn_invalid", "wrong error"); + PASS(); +} + +void test_breaching_true_when_observed_below_target() { + TEST(breaching_true_when_observed_below_target); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 98, 20), &error), "upsert failed"); + CHECK(watch.breaching("i1", &error), "indicator should breach"); + PASS(); +} + +void test_breaching_true_when_burn_exceeds_hundred() { + TEST(breaching_true_when_burn_exceeds_hundred); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 100, 101), &error), "upsert failed"); + CHECK(watch.breaching("i1", &error), "indicator should breach"); + PASS(); +} + +void test_breaching_false_when_healthy() { + TEST(breaching_false_when_healthy); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 100, 40), &error), "upsert failed"); + CHECK(!watch.breaching("i1", &error), "indicator should be healthy"); + PASS(); +} + +void test_breaching_rejects_missing_indicator() { + TEST(breaching_rejects_missing_indicator); + OperationalSLOWatch watch; + std::string error; + CHECK(!watch.breaching("missing", &error), "breach should fail"); + CHECK(error == "indicator_missing", "wrong error"); + PASS(); +} + +void test_service_breach_count_counts_only_breaches() { + TEST(service_breach_count_counts_only_breaches); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 98, 10), &error), "upsert i1 failed"); + CHECK(watch.upsert(indicator("i2", "svc-a", 99, 100, 10), &error), "upsert i2 failed"); + CHECK(watch.upsert(indicator("i3", "svc-a", 99, 100, 110), &error), "upsert i3 failed"); + CHECK(watch.serviceBreachCount("svc-a") == 2, "breach count mismatch"); + PASS(); +} + +void test_by_service_empty_returns_all() { + TEST(by_service_empty_returns_all); + OperationalSLOWatch watch; + std::string error; + CHECK(watch.upsert(indicator("i1", "svc-a", 99, 99, 10), &error), "upsert i1 failed"); + CHECK(watch.upsert(indicator("i2", "svc-b", 99, 99, 10), &error), "upsert i2 failed"); + CHECK(watch.byService("").size() == 2, "empty service should return all"); + PASS(); +} + +int main() { + std::cout << "Step 607: Operational SLO Watch\n"; + + test_upsert_success(); // 1 + test_upsert_rejects_missing_indicator_id(); // 2 + test_upsert_rejects_missing_service_id(); // 3 + test_upsert_rejects_invalid_target_percent(); // 4 + test_upsert_rejects_invalid_observed_percent(); // 5 + test_upsert_rejects_negative_error_budget_burn(); // 6 + test_breaching_true_when_observed_below_target(); // 7 + test_breaching_true_when_burn_exceeds_hundred(); // 8 + test_breaching_false_when_healthy(); // 9 + test_breaching_rejects_missing_indicator(); // 10 + test_service_breach_count_counts_only_breaches(); // 11 + test_by_service_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 73a4cb6..1c392dc 100644 --- a/progress.md +++ b/progress.md @@ -11800,3 +11800,33 @@ recovery-time metrics. - `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` + +### Step 607: Operational SLO Watch +**Status:** PASS (12/12 tests) + +Implements operational SLO indicator tracking with breach detection and +service-level breach aggregation. + +**Files added:** +- `editor/src/OperationalSLOWatch.h` - SLO watch module: + - indicator upsert with identifier/range validation + - breach detection for target miss or budget over-burn + - service breach counts and service filtering helpers +- `editor/tests/step607_test.cpp` - 12 tests covering: + - upsert success/failure validation behavior + - breach decision behavior and missing indicator handling + - service breach aggregation and filter behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step607_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step607_test step606_test` - PASS +- `./editor/build-native/step607_test` - PASS (12/12) +- `./editor/build-native/step606_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/OperationalSLOWatch.h` within header-size limit (`64` <= `600`) +- `editor/tests/step607_test.cpp` within test-file size guidance (`150` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`