integration_tests.rs 8.5 KB

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