|
@@ -0,0 +1,286 @@
|
|
|
|
|
+//! Integration tests for cross-module wiring.
|
|
|
|
|
+//!
|
|
|
|
|
+//! These tests verify that adjacent modules in the runtime crate actually
|
|
|
|
|
+//! connect correctly — catching wiring gaps that unit tests miss.
|
|
|
|
|
+
|
|
|
|
|
+use std::time::Duration;
|
|
|
|
|
+
|
|
|
|
|
+use runtime::{
|
|
|
|
|
+ apply_policy, BranchFreshness, DiffScope, LaneBlocker,
|
|
|
|
|
+ LaneContext, PolicyAction, PolicyCondition, PolicyEngine, PolicyRule,
|
|
|
|
|
+ ReconcileReason, ReviewStatus, StaleBranchAction, StaleBranchPolicy,
|
|
|
|
|
+};
|
|
|
|
|
+use runtime::green_contract::{GreenLevel, GreenContract, GreenContractOutcome};
|
|
|
|
|
+
|
|
|
|
|
+/// stale_branch + policy_engine integration:
|
|
|
|
|
+/// When a branch is detected stale, does it correctly flow through
|
|
|
|
|
+/// PolicyCondition::StaleBranch to generate the expected action?
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn stale_branch_detection_flows_into_policy_engine() {
|
|
|
|
|
+ // given — a stale branch context (2 hours behind main, threshold is 1 hour)
|
|
|
|
|
+ let stale_context = LaneContext::new(
|
|
|
|
|
+ "stale-lane",
|
|
|
|
|
+ 0,
|
|
|
|
|
+ Duration::from_secs(2 * 60 * 60), // 2 hours stale
|
|
|
|
|
+ LaneBlocker::None,
|
|
|
|
|
+ ReviewStatus::Pending,
|
|
|
|
|
+ DiffScope::Full,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![PolicyRule::new(
|
|
|
|
|
+ "stale-merge-forward",
|
|
|
|
|
+ PolicyCondition::StaleBranch,
|
|
|
|
|
+ PolicyAction::MergeForward,
|
|
|
|
|
+ 10,
|
|
|
|
|
+ )]);
|
|
|
|
|
+
|
|
|
|
|
+ // when
|
|
|
|
|
+ let actions = engine.evaluate(&stale_context);
|
|
|
|
|
+
|
|
|
|
|
+ // then
|
|
|
|
|
+ assert_eq!(actions, vec![PolicyAction::MergeForward]);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// stale_branch + policy_engine: Fresh branch does NOT trigger stale rules
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn fresh_branch_does_not_trigger_stale_policy() {
|
|
|
|
|
+ let fresh_context = LaneContext::new(
|
|
|
|
|
+ "fresh-lane",
|
|
|
|
|
+ 0,
|
|
|
|
|
+ Duration::from_secs(30 * 60), // 30 min stale — under 1 hour threshold
|
|
|
|
|
+ LaneBlocker::None,
|
|
|
|
|
+ ReviewStatus::Pending,
|
|
|
|
|
+ DiffScope::Full,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![PolicyRule::new(
|
|
|
|
|
+ "stale-merge-forward",
|
|
|
|
|
+ PolicyCondition::StaleBranch,
|
|
|
|
|
+ PolicyAction::MergeForward,
|
|
|
|
|
+ 10,
|
|
|
|
|
+ )]);
|
|
|
|
|
+
|
|
|
|
|
+ let actions = engine.evaluate(&fresh_context);
|
|
|
|
|
+ assert!(actions.is_empty());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// green_contract + policy_engine integration:
|
|
|
|
|
+/// A lane that meets its green contract should be mergeable
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn green_contract_satisfied_allows_merge() {
|
|
|
|
|
+ let contract = GreenContract::new(GreenLevel::Workspace);
|
|
|
|
|
+ let satisfied = contract.is_satisfied_by(GreenLevel::Workspace);
|
|
|
|
|
+ assert!(satisfied);
|
|
|
|
|
+
|
|
|
|
|
+ let exceeded = contract.is_satisfied_by(GreenLevel::MergeReady);
|
|
|
|
|
+ assert!(exceeded);
|
|
|
|
|
+
|
|
|
|
|
+ let insufficient = contract.is_satisfied_by(GreenLevel::Package);
|
|
|
|
|
+ assert!(!insufficient);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// green_contract + policy_engine:
|
|
|
|
|
+/// Lane with green level below contract requirement gets blocked
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn green_contract_unsatisfied_blocks_merge() {
|
|
|
|
|
+ let context = LaneContext::new(
|
|
|
|
|
+ "partial-green-lane",
|
|
|
|
|
+ 1, // GreenLevel::Package as u8
|
|
|
|
|
+ Duration::from_secs(0),
|
|
|
|
|
+ LaneBlocker::None,
|
|
|
|
|
+ ReviewStatus::Pending,
|
|
|
|
|
+ DiffScope::Full,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // This is a conceptual test — we need a way to express "requires workspace green"
|
|
|
|
|
+ // Currently LaneContext has raw green_level: u8, not a contract
|
|
|
|
|
+ // For now we just verify the policy condition works
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![PolicyRule::new(
|
|
|
|
|
+ "workspace-green-required",
|
|
|
|
|
+ PolicyCondition::GreenAt { level: 3 }, // GreenLevel::Workspace
|
|
|
|
|
+ PolicyAction::MergeToDev,
|
|
|
|
|
+ 10,
|
|
|
|
|
+ )]);
|
|
|
|
|
+
|
|
|
|
|
+ let actions = engine.evaluate(&context);
|
|
|
|
|
+ assert!(actions.is_empty()); // level 1 < 3, so no merge
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// reconciliation + policy_engine integration:
|
|
|
|
|
+/// A reconciled lane should be handled by reconcile rules, not generic closeout
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn reconciled_lane_matches_reconcile_condition() {
|
|
|
|
|
+ let context = LaneContext::reconciled("reconciled-lane");
|
|
|
|
|
+
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![
|
|
|
|
|
+ PolicyRule::new(
|
|
|
|
|
+ "reconcile-first",
|
|
|
|
|
+ PolicyCondition::LaneReconciled,
|
|
|
|
|
+ PolicyAction::Reconcile {
|
|
|
|
|
+ reason: ReconcileReason::AlreadyMerged,
|
|
|
|
|
+ },
|
|
|
|
|
+ 5,
|
|
|
|
|
+ ),
|
|
|
|
|
+ PolicyRule::new(
|
|
|
|
|
+ "generic-closeout",
|
|
|
|
|
+ PolicyCondition::LaneCompleted,
|
|
|
|
|
+ PolicyAction::CloseoutLane,
|
|
|
|
|
+ 30,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ let actions = engine.evaluate(&context);
|
|
|
|
|
+
|
|
|
|
|
+ // Both rules fire — reconcile (priority 5) first, then closeout (priority 30)
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ actions,
|
|
|
|
|
+ vec![
|
|
|
|
|
+ PolicyAction::Reconcile {
|
|
|
|
|
+ reason: ReconcileReason::AlreadyMerged,
|
|
|
|
|
+ },
|
|
|
|
|
+ PolicyAction::CloseoutLane,
|
|
|
|
|
+ ]
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// stale_branch module: apply_policy generates correct actions
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn stale_branch_apply_policy_produces_rebase_action() {
|
|
|
|
|
+ let stale = BranchFreshness::Stale {
|
|
|
|
|
+ commits_behind: 5,
|
|
|
|
|
+ missing_fixes: vec!["fix-123".to_string()],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let action = apply_policy(&stale, StaleBranchPolicy::AutoRebase);
|
|
|
|
|
+ assert_eq!(action, StaleBranchAction::Rebase);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn stale_branch_apply_policy_produces_merge_forward_action() {
|
|
|
|
|
+ let stale = BranchFreshness::Stale {
|
|
|
|
|
+ commits_behind: 3,
|
|
|
|
|
+ missing_fixes: vec![],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let action = apply_policy(&stale, StaleBranchPolicy::AutoMergeForward);
|
|
|
|
|
+ assert_eq!(action, StaleBranchAction::MergeForward);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn stale_branch_apply_policy_warn_only() {
|
|
|
|
|
+ let stale = BranchFreshness::Stale {
|
|
|
|
|
+ commits_behind: 2,
|
|
|
|
|
+ missing_fixes: vec!["fix-456".to_string()],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let action = apply_policy(&stale, StaleBranchPolicy::WarnOnly);
|
|
|
|
|
+ match action {
|
|
|
|
|
+ StaleBranchAction::Warn { message } => {
|
|
|
|
|
+ assert!(message.contains("2 commit(s) behind main"));
|
|
|
|
|
+ assert!(message.contains("fix-456"));
|
|
|
|
|
+ }
|
|
|
|
|
+ _ => panic!("expected Warn action, got {:?}", action),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn stale_branch_fresh_produces_noop() {
|
|
|
|
|
+ let fresh = BranchFreshness::Fresh;
|
|
|
|
|
+ let action = apply_policy(&fresh, StaleBranchPolicy::AutoRebase);
|
|
|
|
|
+ assert_eq!(action, StaleBranchAction::Noop);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Combined flow: stale detection + policy + action
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn end_to_end_stale_lane_gets_merge_forward_action() {
|
|
|
|
|
+ // Simulating what a harness would do:
|
|
|
|
|
+ // 1. Detect branch freshness
|
|
|
|
|
+ // 2. Build lane context from freshness + other signals
|
|
|
|
|
+ // 3. Run policy engine
|
|
|
|
|
+ // 4. Return actions
|
|
|
|
|
+
|
|
|
|
|
+ // given: detected stale state
|
|
|
|
|
+ let _freshness = BranchFreshness::Stale {
|
|
|
|
|
+ commits_behind: 5,
|
|
|
|
|
+ missing_fixes: vec!["fix-123".to_string()],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // when: build context and evaluate policy
|
|
|
|
|
+ let context = LaneContext::new(
|
|
|
|
|
+ "lane-9411",
|
|
|
|
|
+ 3, // Workspace green
|
|
|
|
|
+ Duration::from_secs(5 * 60 * 60), // 5 hours stale, definitely over threshold
|
|
|
|
|
+ LaneBlocker::None,
|
|
|
|
|
+ ReviewStatus::Approved,
|
|
|
|
|
+ DiffScope::Scoped,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![
|
|
|
|
|
+ // Priority 5: Check if stale first
|
|
|
|
|
+ PolicyRule::new(
|
|
|
|
|
+ "auto-merge-forward-if-stale-and-approved",
|
|
|
|
|
+ PolicyCondition::And(vec![
|
|
|
|
|
+ PolicyCondition::StaleBranch,
|
|
|
|
|
+ PolicyCondition::ReviewPassed,
|
|
|
|
|
+ ]),
|
|
|
|
|
+ PolicyAction::MergeForward,
|
|
|
|
|
+ 5,
|
|
|
|
|
+ ),
|
|
|
|
|
+ // Priority 10: Normal stale handling
|
|
|
|
|
+ PolicyRule::new(
|
|
|
|
|
+ "stale-warning",
|
|
|
|
|
+ PolicyCondition::StaleBranch,
|
|
|
|
|
+ PolicyAction::Notify {
|
|
|
|
|
+ channel: "#build-status".to_string(),
|
|
|
|
|
+ },
|
|
|
|
|
+ 10,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ let actions = engine.evaluate(&context);
|
|
|
|
|
+
|
|
|
|
|
+ // then: both rules should fire (stale + approved matches both)
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ actions,
|
|
|
|
|
+ vec![
|
|
|
|
|
+ PolicyAction::MergeForward,
|
|
|
|
|
+ PolicyAction::Notify {
|
|
|
|
|
+ channel: "#build-status".to_string(),
|
|
|
|
|
+ },
|
|
|
|
|
+ ]
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Fresh branch with approved review should merge (not stale-blocked)
|
|
|
|
|
+#[test]
|
|
|
|
|
+fn fresh_approved_lane_gets_merge_action() {
|
|
|
|
|
+ let context = LaneContext::new(
|
|
|
|
|
+ "fresh-approved-lane",
|
|
|
|
|
+ 3, // Workspace green
|
|
|
|
|
+ Duration::from_secs(30 * 60), // 30 min — under 1 hour threshold = fresh
|
|
|
|
|
+ LaneBlocker::None,
|
|
|
|
|
+ ReviewStatus::Approved,
|
|
|
|
|
+ DiffScope::Scoped,
|
|
|
|
|
+ false,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let engine = PolicyEngine::new(vec![
|
|
|
|
|
+ PolicyRule::new(
|
|
|
|
|
+ "merge-if-green-approved-not-stale",
|
|
|
|
|
+ PolicyCondition::And(vec![
|
|
|
|
|
+ PolicyCondition::GreenAt { level: 3 },
|
|
|
|
|
+ PolicyCondition::ReviewPassed,
|
|
|
|
|
+ // NOT PolicyCondition::StaleBranch — fresh lanes bypass this
|
|
|
|
|
+ ]),
|
|
|
|
|
+ PolicyAction::MergeToDev,
|
|
|
|
|
+ 5,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ let actions = engine.evaluate(&context);
|
|
|
|
|
+ assert_eq!(actions, vec![PolicyAction::MergeToDev]);
|
|
|
|
|
+}
|