From 2ce778453d77f95db02f7419c23bdb48abdd06b3 Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 17 Feb 2026 11:57:15 -0700 Subject: [PATCH] Add step 605 deployment promotion gate --- editor/CMakeLists.txt | 9 ++ editor/src/DeploymentPromotionGate.h | 76 ++++++++++++++ editor/tests/step605_test.cpp | 149 +++++++++++++++++++++++++++ progress.md | 30 ++++++ 4 files changed, 264 insertions(+) create mode 100644 editor/src/DeploymentPromotionGate.h create mode 100644 editor/tests/step605_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 0417e5e..ab710f0 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4324,4 +4324,13 @@ target_link_libraries(step604_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step605_test tests/step605_test.cpp) +target_include_directories(step605_test PRIVATE src) +target_link_libraries(step605_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/DeploymentPromotionGate.h b/editor/src/DeploymentPromotionGate.h new file mode 100644 index 0000000..7cace5d --- /dev/null +++ b/editor/src/DeploymentPromotionGate.h @@ -0,0 +1,76 @@ +#pragma once +// Step 605: Deployment Promotion Gate + +#include +#include +#include + +#include "ValidationErrorUtil.h" + +struct EnvironmentGateState { + std::string environmentId; + int requiredChecks = 0; + int passedChecks = 0; + int manualApprovals = 0; + int blockingIssues = 0; +}; + +class DeploymentPromotionGate { +public: + bool upsert(const EnvironmentGateState& state, std::string* error) { + if (!error) return false; + error->clear(); + if (state.environmentId.empty()) return failWith(error, "environment_id_missing"); + if (state.requiredChecks < 0) return failWith(error, "required_checks_invalid"); + if (state.passedChecks < 0) return failWith(error, "passed_checks_invalid"); + if (state.manualApprovals < 0) return failWith(error, "manual_approvals_invalid"); + if (state.blockingIssues < 0) return failWith(error, "blocking_issues_invalid"); + if (state.passedChecks > state.requiredChecks) return failWith(error, "passed_checks_exceed_required"); + if (states_.count(state.environmentId) == 0) order_.push_back(state.environmentId); + states_[state.environmentId] = state; + return true; + } + + bool promotable(const std::string& environmentId, std::string* error) const { + if (!error) return false; + error->clear(); + auto it = states_.find(environmentId); + if (it == states_.end()) return failWith(error, "environment_missing"); + const auto& state = it->second; + return state.requiredChecks > 0 && + state.passedChecks == state.requiredChecks && + state.manualApprovals > 0 && + state.blockingIssues == 0; + } + + int promotionScore(const std::string& environmentId, std::string* error) const { + if (!error) return 0; + error->clear(); + auto it = states_.find(environmentId); + if (it == states_.end()) { + failWith(error, "environment_missing"); + return 0; + } + const auto& state = it->second; + if (state.requiredChecks == 0) return 0; + int score = (state.passedChecks * 100) / state.requiredChecks; + score += state.manualApprovals * 5; + score -= state.blockingIssues * 20; + if (score < 0) return 0; + if (score > 100) return 100; + return score; + } + + std::vector blockedEnvironments() const { + std::vector blocked; + for (const auto& id : order_) { + const auto& state = states_.at(id); + if (state.blockingIssues > 0 || state.passedChecks < state.requiredChecks) blocked.push_back(id); + } + return blocked; + } + +private: + std::map states_; + std::vector order_; +}; diff --git a/editor/tests/step605_test.cpp b/editor/tests/step605_test.cpp new file mode 100644 index 0000000..09e578f --- /dev/null +++ b/editor/tests/step605_test.cpp @@ -0,0 +1,149 @@ +// Step 605: Deployment Promotion Gate (12 tests) + +#include "DeploymentPromotionGate.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 EnvironmentGateState state(const std::string& env, + int required, + int passedChecks, + int approvals, + int blockers) { + return {env, required, passedChecks, approvals, blockers}; +} + +void test_upsert_success() { + TEST(upsert_success); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("staging", 4, 3, 1, 0), &error), "upsert should succeed"); + CHECK(gate.blockedEnvironments().size() == 1, "blocked env count mismatch"); + PASS(); +} + +void test_upsert_rejects_missing_environment_id() { + TEST(upsert_rejects_missing_environment_id); + DeploymentPromotionGate gate; + std::string error; + CHECK(!gate.upsert(state("", 4, 3, 1, 0), &error), "upsert should fail"); + CHECK(error == "environment_id_missing", "wrong error"); + PASS(); +} + +void test_upsert_rejects_negative_required_checks() { + TEST(upsert_rejects_negative_required_checks); + DeploymentPromotionGate gate; + std::string error; + CHECK(!gate.upsert(state("staging", -1, 0, 0, 0), &error), "upsert should fail"); + CHECK(error == "required_checks_invalid", "wrong error"); + PASS(); +} + +void test_upsert_rejects_passed_checks_exceed_required() { + TEST(upsert_rejects_passed_checks_exceed_required); + DeploymentPromotionGate gate; + std::string error; + CHECK(!gate.upsert(state("staging", 2, 3, 1, 0), &error), "upsert should fail"); + CHECK(error == "passed_checks_exceed_required", "wrong error"); + PASS(); +} + +void test_promotable_true_when_all_conditions_met() { + TEST(promotable_true_when_all_conditions_met); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 5, 5, 2, 0), &error), "upsert failed"); + CHECK(gate.promotable("prod", &error), "prod should be promotable"); + PASS(); +} + +void test_promotable_false_when_checks_incomplete() { + TEST(promotable_false_when_checks_incomplete); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 5, 4, 2, 0), &error), "upsert failed"); + CHECK(!gate.promotable("prod", &error), "prod should not be promotable"); + PASS(); +} + +void test_promotable_false_when_no_manual_approval() { + TEST(promotable_false_when_no_manual_approval); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 5, 5, 0, 0), &error), "upsert failed"); + CHECK(!gate.promotable("prod", &error), "prod should not be promotable"); + PASS(); +} + +void test_promotable_rejects_missing_environment() { + TEST(promotable_rejects_missing_environment); + DeploymentPromotionGate gate; + std::string error; + CHECK(!gate.promotable("missing", &error), "missing env should fail"); + CHECK(error == "environment_missing", "wrong error"); + PASS(); +} + +void test_promotion_score_base_calculation() { + TEST(promotion_score_base_calculation); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 10, 8, 1, 0), &error), "upsert failed"); + CHECK(gate.promotionScore("prod", &error) == 85, "score mismatch"); + PASS(); +} + +void test_promotion_score_penalizes_blockers() { + TEST(promotion_score_penalizes_blockers); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 10, 10, 2, 2), &error), "upsert failed"); + CHECK(gate.promotionScore("prod", &error) == 70, "score mismatch"); + PASS(); +} + +void test_promotion_score_clamps_to_hundred() { + TEST(promotion_score_clamps_to_hundred); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("prod", 1, 1, 8, 0), &error), "upsert failed"); + CHECK(gate.promotionScore("prod", &error) == 100, "score should clamp to 100"); + PASS(); +} + +void test_blocked_environments_lists_only_blocked() { + TEST(blocked_environments_lists_only_blocked); + DeploymentPromotionGate gate; + std::string error; + CHECK(gate.upsert(state("staging", 3, 3, 1, 0), &error), "upsert staging failed"); + CHECK(gate.upsert(state("prod", 3, 2, 1, 0), &error), "upsert prod failed"); + CHECK(gate.upsert(state("canary", 2, 2, 1, 1), &error), "upsert canary failed"); + CHECK(gate.blockedEnvironments().size() == 2, "blocked list size mismatch"); + PASS(); +} + +int main() { + std::cout << "Step 605: Deployment Promotion Gate\n"; + + test_upsert_success(); // 1 + test_upsert_rejects_missing_environment_id(); // 2 + test_upsert_rejects_negative_required_checks(); // 3 + test_upsert_rejects_passed_checks_exceed_required();// 4 + test_promotable_true_when_all_conditions_met(); // 5 + test_promotable_false_when_checks_incomplete(); // 6 + test_promotable_false_when_no_manual_approval(); // 7 + test_promotable_rejects_missing_environment(); // 8 + test_promotion_score_base_calculation(); // 9 + test_promotion_score_penalizes_blockers(); // 10 + test_promotion_score_clamps_to_hundred(); // 11 + test_blocked_environments_lists_only_blocked(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index a5cb2a0..610b903 100644 --- a/progress.md +++ b/progress.md @@ -11740,3 +11740,33 @@ category filtering. - `editor/src/ReleaseCertificationPacket.h` within header-size limit (`68` <= `600`) - `editor/tests/step604_test.cpp` within test-file size guidance (`154` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 605: Deployment Promotion Gate +**Status:** PASS (12/12 tests) + +Implements environment promotion gating with validation, readiness scoring, and +blocked-environment reporting. + +**Files added:** +- `editor/src/DeploymentPromotionGate.h` - promotion gate module: + - environment gate state upsert with constraint validation + - promotable decision logic over checks/approvals/blockers + - promotion scoring with clamp and blocked-environment listing +- `editor/tests/step605_test.cpp` - 12 tests covering: + - upsert success/failure behavior + - promotable decision behavior and missing-environment handling + - score behavior and blocked environment aggregation + +**Files modified:** +- `editor/CMakeLists.txt` - `step605_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step605_test step604_test` - PASS +- `./editor/build-native/step605_test` - PASS (12/12) +- `./editor/build-native/step604_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `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`