|
| 1 | +//! Integration tests for cross-module wiring. |
| 2 | +//! |
| 3 | +//! These tests verify that adjacent modules in the runtime crate actually |
| 4 | +//! connect correctly — catching wiring gaps that unit tests miss. |
| 5 | +
|
| 6 | +use std::time::Duration; |
| 7 | + |
| 8 | +use runtime::{ |
| 9 | + apply_policy, BranchFreshness, DiffScope, LaneBlocker, |
| 10 | + LaneContext, PolicyAction, PolicyCondition, PolicyEngine, PolicyRule, |
| 11 | + ReconcileReason, ReviewStatus, StaleBranchAction, StaleBranchPolicy, |
| 12 | +}; |
| 13 | +use runtime::green_contract::{GreenLevel, GreenContract, GreenContractOutcome}; |
| 14 | + |
| 15 | +/// stale_branch + policy_engine integration: |
| 16 | +/// When a branch is detected stale, does it correctly flow through |
| 17 | +/// PolicyCondition::StaleBranch to generate the expected action? |
| 18 | +#[test] |
| 19 | +fn stale_branch_detection_flows_into_policy_engine() { |
| 20 | + // given — a stale branch context (2 hours behind main, threshold is 1 hour) |
| 21 | + let stale_context = LaneContext::new( |
| 22 | + "stale-lane", |
| 23 | + 0, |
| 24 | + Duration::from_secs(2 * 60 * 60), // 2 hours stale |
| 25 | + LaneBlocker::None, |
| 26 | + ReviewStatus::Pending, |
| 27 | + DiffScope::Full, |
| 28 | + false, |
| 29 | + ); |
| 30 | + |
| 31 | + let engine = PolicyEngine::new(vec![PolicyRule::new( |
| 32 | + "stale-merge-forward", |
| 33 | + PolicyCondition::StaleBranch, |
| 34 | + PolicyAction::MergeForward, |
| 35 | + 10, |
| 36 | + )]); |
| 37 | + |
| 38 | + // when |
| 39 | + let actions = engine.evaluate(&stale_context); |
| 40 | + |
| 41 | + // then |
| 42 | + assert_eq!(actions, vec![PolicyAction::MergeForward]); |
| 43 | +} |
| 44 | + |
| 45 | +/// stale_branch + policy_engine: Fresh branch does NOT trigger stale rules |
| 46 | +#[test] |
| 47 | +fn fresh_branch_does_not_trigger_stale_policy() { |
| 48 | + let fresh_context = LaneContext::new( |
| 49 | + "fresh-lane", |
| 50 | + 0, |
| 51 | + Duration::from_secs(30 * 60), // 30 min stale — under 1 hour threshold |
| 52 | + LaneBlocker::None, |
| 53 | + ReviewStatus::Pending, |
| 54 | + DiffScope::Full, |
| 55 | + false, |
| 56 | + ); |
| 57 | + |
| 58 | + let engine = PolicyEngine::new(vec![PolicyRule::new( |
| 59 | + "stale-merge-forward", |
| 60 | + PolicyCondition::StaleBranch, |
| 61 | + PolicyAction::MergeForward, |
| 62 | + 10, |
| 63 | + )]); |
| 64 | + |
| 65 | + let actions = engine.evaluate(&fresh_context); |
| 66 | + assert!(actions.is_empty()); |
| 67 | +} |
| 68 | + |
| 69 | +/// green_contract + policy_engine integration: |
| 70 | +/// A lane that meets its green contract should be mergeable |
| 71 | +#[test] |
| 72 | +fn green_contract_satisfied_allows_merge() { |
| 73 | + let contract = GreenContract::new(GreenLevel::Workspace); |
| 74 | + let satisfied = contract.is_satisfied_by(GreenLevel::Workspace); |
| 75 | + assert!(satisfied); |
| 76 | + |
| 77 | + let exceeded = contract.is_satisfied_by(GreenLevel::MergeReady); |
| 78 | + assert!(exceeded); |
| 79 | + |
| 80 | + let insufficient = contract.is_satisfied_by(GreenLevel::Package); |
| 81 | + assert!(!insufficient); |
| 82 | +} |
| 83 | + |
| 84 | +/// green_contract + policy_engine: |
| 85 | +/// Lane with green level below contract requirement gets blocked |
| 86 | +#[test] |
| 87 | +fn green_contract_unsatisfied_blocks_merge() { |
| 88 | + let context = LaneContext::new( |
| 89 | + "partial-green-lane", |
| 90 | + 1, // GreenLevel::Package as u8 |
| 91 | + Duration::from_secs(0), |
| 92 | + LaneBlocker::None, |
| 93 | + ReviewStatus::Pending, |
| 94 | + DiffScope::Full, |
| 95 | + false, |
| 96 | + ); |
| 97 | + |
| 98 | + // This is a conceptual test — we need a way to express "requires workspace green" |
| 99 | + // Currently LaneContext has raw green_level: u8, not a contract |
| 100 | + // For now we just verify the policy condition works |
| 101 | + let engine = PolicyEngine::new(vec![PolicyRule::new( |
| 102 | + "workspace-green-required", |
| 103 | + PolicyCondition::GreenAt { level: 3 }, // GreenLevel::Workspace |
| 104 | + PolicyAction::MergeToDev, |
| 105 | + 10, |
| 106 | + )]); |
| 107 | + |
| 108 | + let actions = engine.evaluate(&context); |
| 109 | + assert!(actions.is_empty()); // level 1 < 3, so no merge |
| 110 | +} |
| 111 | + |
| 112 | +/// reconciliation + policy_engine integration: |
| 113 | +/// A reconciled lane should be handled by reconcile rules, not generic closeout |
| 114 | +#[test] |
| 115 | +fn reconciled_lane_matches_reconcile_condition() { |
| 116 | + let context = LaneContext::reconciled("reconciled-lane"); |
| 117 | + |
| 118 | + let engine = PolicyEngine::new(vec![ |
| 119 | + PolicyRule::new( |
| 120 | + "reconcile-first", |
| 121 | + PolicyCondition::LaneReconciled, |
| 122 | + PolicyAction::Reconcile { |
| 123 | + reason: ReconcileReason::AlreadyMerged, |
| 124 | + }, |
| 125 | + 5, |
| 126 | + ), |
| 127 | + PolicyRule::new( |
| 128 | + "generic-closeout", |
| 129 | + PolicyCondition::LaneCompleted, |
| 130 | + PolicyAction::CloseoutLane, |
| 131 | + 30, |
| 132 | + ), |
| 133 | + ]); |
| 134 | + |
| 135 | + let actions = engine.evaluate(&context); |
| 136 | + |
| 137 | + // Both rules fire — reconcile (priority 5) first, then closeout (priority 30) |
| 138 | + assert_eq!( |
| 139 | + actions, |
| 140 | + vec![ |
| 141 | + PolicyAction::Reconcile { |
| 142 | + reason: ReconcileReason::AlreadyMerged, |
| 143 | + }, |
| 144 | + PolicyAction::CloseoutLane, |
| 145 | + ] |
| 146 | + ); |
| 147 | +} |
| 148 | + |
| 149 | +/// stale_branch module: apply_policy generates correct actions |
| 150 | +#[test] |
| 151 | +fn stale_branch_apply_policy_produces_rebase_action() { |
| 152 | + let stale = BranchFreshness::Stale { |
| 153 | + commits_behind: 5, |
| 154 | + missing_fixes: vec!["fix-123".to_string()], |
| 155 | + }; |
| 156 | + |
| 157 | + let action = apply_policy(&stale, StaleBranchPolicy::AutoRebase); |
| 158 | + assert_eq!(action, StaleBranchAction::Rebase); |
| 159 | +} |
| 160 | + |
| 161 | +#[test] |
| 162 | +fn stale_branch_apply_policy_produces_merge_forward_action() { |
| 163 | + let stale = BranchFreshness::Stale { |
| 164 | + commits_behind: 3, |
| 165 | + missing_fixes: vec![], |
| 166 | + }; |
| 167 | + |
| 168 | + let action = apply_policy(&stale, StaleBranchPolicy::AutoMergeForward); |
| 169 | + assert_eq!(action, StaleBranchAction::MergeForward); |
| 170 | +} |
| 171 | + |
| 172 | +#[test] |
| 173 | +fn stale_branch_apply_policy_warn_only() { |
| 174 | + let stale = BranchFreshness::Stale { |
| 175 | + commits_behind: 2, |
| 176 | + missing_fixes: vec!["fix-456".to_string()], |
| 177 | + }; |
| 178 | + |
| 179 | + let action = apply_policy(&stale, StaleBranchPolicy::WarnOnly); |
| 180 | + match action { |
| 181 | + StaleBranchAction::Warn { message } => { |
| 182 | + assert!(message.contains("2 commit(s) behind main")); |
| 183 | + assert!(message.contains("fix-456")); |
| 184 | + } |
| 185 | + _ => panic!("expected Warn action, got {:?}", action), |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +#[test] |
| 190 | +fn stale_branch_fresh_produces_noop() { |
| 191 | + let fresh = BranchFreshness::Fresh; |
| 192 | + let action = apply_policy(&fresh, StaleBranchPolicy::AutoRebase); |
| 193 | + assert_eq!(action, StaleBranchAction::Noop); |
| 194 | +} |
| 195 | + |
| 196 | +/// Combined flow: stale detection + policy + action |
| 197 | +#[test] |
| 198 | +fn end_to_end_stale_lane_gets_merge_forward_action() { |
| 199 | + // Simulating what a harness would do: |
| 200 | + // 1. Detect branch freshness |
| 201 | + // 2. Build lane context from freshness + other signals |
| 202 | + // 3. Run policy engine |
| 203 | + // 4. Return actions |
| 204 | + |
| 205 | + // given: detected stale state |
| 206 | + let _freshness = BranchFreshness::Stale { |
| 207 | + commits_behind: 5, |
| 208 | + missing_fixes: vec!["fix-123".to_string()], |
| 209 | + }; |
| 210 | + |
| 211 | + // when: build context and evaluate policy |
| 212 | + let context = LaneContext::new( |
| 213 | + "lane-9411", |
| 214 | + 3, // Workspace green |
| 215 | + Duration::from_secs(5 * 60 * 60), // 5 hours stale, definitely over threshold |
| 216 | + LaneBlocker::None, |
| 217 | + ReviewStatus::Approved, |
| 218 | + DiffScope::Scoped, |
| 219 | + false, |
| 220 | + ); |
| 221 | + |
| 222 | + let engine = PolicyEngine::new(vec![ |
| 223 | + // Priority 5: Check if stale first |
| 224 | + PolicyRule::new( |
| 225 | + "auto-merge-forward-if-stale-and-approved", |
| 226 | + PolicyCondition::And(vec![ |
| 227 | + PolicyCondition::StaleBranch, |
| 228 | + PolicyCondition::ReviewPassed, |
| 229 | + ]), |
| 230 | + PolicyAction::MergeForward, |
| 231 | + 5, |
| 232 | + ), |
| 233 | + // Priority 10: Normal stale handling |
| 234 | + PolicyRule::new( |
| 235 | + "stale-warning", |
| 236 | + PolicyCondition::StaleBranch, |
| 237 | + PolicyAction::Notify { |
| 238 | + channel: "#build-status".to_string(), |
| 239 | + }, |
| 240 | + 10, |
| 241 | + ), |
| 242 | + ]); |
| 243 | + |
| 244 | + let actions = engine.evaluate(&context); |
| 245 | + |
| 246 | + // then: both rules should fire (stale + approved matches both) |
| 247 | + assert_eq!( |
| 248 | + actions, |
| 249 | + vec![ |
| 250 | + PolicyAction::MergeForward, |
| 251 | + PolicyAction::Notify { |
| 252 | + channel: "#build-status".to_string(), |
| 253 | + }, |
| 254 | + ] |
| 255 | + ); |
| 256 | +} |
| 257 | + |
| 258 | +/// Fresh branch with approved review should merge (not stale-blocked) |
| 259 | +#[test] |
| 260 | +fn fresh_approved_lane_gets_merge_action() { |
| 261 | + let context = LaneContext::new( |
| 262 | + "fresh-approved-lane", |
| 263 | + 3, // Workspace green |
| 264 | + Duration::from_secs(30 * 60), // 30 min — under 1 hour threshold = fresh |
| 265 | + LaneBlocker::None, |
| 266 | + ReviewStatus::Approved, |
| 267 | + DiffScope::Scoped, |
| 268 | + false, |
| 269 | + ); |
| 270 | + |
| 271 | + let engine = PolicyEngine::new(vec![ |
| 272 | + PolicyRule::new( |
| 273 | + "merge-if-green-approved-not-stale", |
| 274 | + PolicyCondition::And(vec![ |
| 275 | + PolicyCondition::GreenAt { level: 3 }, |
| 276 | + PolicyCondition::ReviewPassed, |
| 277 | + // NOT PolicyCondition::StaleBranch — fresh lanes bypass this |
| 278 | + ]), |
| 279 | + PolicyAction::MergeToDev, |
| 280 | + 5, |
| 281 | + ), |
| 282 | + ]); |
| 283 | + |
| 284 | + let actions = engine.evaluate(&context); |
| 285 | + assert_eq!(actions, vec![PolicyAction::MergeToDev]); |
| 286 | +} |
0 commit comments