Add step 605 deployment promotion gate
This commit is contained in:
@@ -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)
|
||||
|
||||
76
editor/src/DeploymentPromotionGate.h
Normal file
76
editor/src/DeploymentPromotionGate.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
// Step 605: Deployment Promotion Gate
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::string> blockedEnvironments() const {
|
||||
std::vector<std::string> 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<std::string, EnvironmentGateState> states_;
|
||||
std::vector<std::string> order_;
|
||||
};
|
||||
149
editor/tests/step605_test.cpp
Normal file
149
editor/tests/step605_test.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
// Step 605: Deployment Promotion Gate (12 tests)
|
||||
|
||||
#include "DeploymentPromotionGate.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
30
progress.md
30
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`
|
||||
|
||||
Reference in New Issue
Block a user