lane_completion.rs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. //! Lane completion detector — automatically marks lanes as completed when
  2. //! session finishes successfully with green tests and pushed code.
  3. //!
  4. //! This bridges the gap where `LaneContext::completed` was a passive bool
  5. //! that nothing automatically set. Now completion is detected from:
  6. //! - Agent output shows Finished status
  7. //! - No errors/blockers present
  8. //! - Tests passed (green status)
  9. //! - Code pushed (has output file)
  10. use runtime::{
  11. evaluate, LaneBlocker, LaneContext, PolicyAction, PolicyCondition, PolicyEngine, PolicyRule,
  12. ReviewStatus,
  13. };
  14. use crate::AgentOutput;
  15. /// Detects if a lane should be automatically marked as completed.
  16. ///
  17. /// Returns `Some(LaneContext)` with `completed = true` if all conditions met,
  18. /// `None` if lane should remain active.
  19. #[allow(dead_code)]
  20. pub(crate) fn detect_lane_completion(
  21. output: &AgentOutput,
  22. test_green: bool,
  23. has_pushed: bool,
  24. ) -> Option<LaneContext> {
  25. // Must be finished without errors
  26. if output.error.is_some() {
  27. return None;
  28. }
  29. // Must have finished status
  30. if !output.status.eq_ignore_ascii_case("completed")
  31. && !output.status.eq_ignore_ascii_case("finished")
  32. {
  33. return None;
  34. }
  35. // Must have no current blocker
  36. if output.current_blocker.is_some() {
  37. return None;
  38. }
  39. // Must have green tests
  40. if !test_green {
  41. return None;
  42. }
  43. // Must have pushed code
  44. if !has_pushed {
  45. return None;
  46. }
  47. // All conditions met — create completed context
  48. Some(LaneContext {
  49. lane_id: output.agent_id.clone(),
  50. green_level: 3, // Workspace green
  51. branch_freshness: std::time::Duration::from_secs(0),
  52. blocker: LaneBlocker::None,
  53. review_status: ReviewStatus::Approved,
  54. diff_scope: runtime::DiffScope::Scoped,
  55. completed: true,
  56. reconciled: false,
  57. })
  58. }
  59. /// Evaluates policy actions for a completed lane.
  60. #[allow(dead_code)]
  61. pub(crate) fn evaluate_completed_lane(
  62. context: &LaneContext,
  63. ) -> Vec<PolicyAction> {
  64. let engine = PolicyEngine::new(vec![
  65. PolicyRule::new(
  66. "closeout-completed-lane",
  67. PolicyCondition::And(vec![
  68. PolicyCondition::LaneCompleted,
  69. PolicyCondition::GreenAt { level: 3 },
  70. ]),
  71. PolicyAction::CloseoutLane,
  72. 10,
  73. ),
  74. PolicyRule::new(
  75. "cleanup-completed-session",
  76. PolicyCondition::LaneCompleted,
  77. PolicyAction::CleanupSession,
  78. 5,
  79. ),
  80. ]);
  81. evaluate(&engine, context)
  82. }
  83. #[cfg(test)]
  84. mod tests {
  85. use super::*;
  86. use runtime::{DiffScope, LaneBlocker};
  87. fn test_output() -> AgentOutput {
  88. AgentOutput {
  89. agent_id: "test-lane-1".to_string(),
  90. name: "Test Agent".to_string(),
  91. description: "Test".to_string(),
  92. subagent_type: None,
  93. model: None,
  94. status: "Finished".to_string(),
  95. output_file: "/tmp/test.output".to_string(),
  96. manifest_file: "/tmp/test.manifest".to_string(),
  97. created_at: "2024-01-01T00:00:00Z".to_string(),
  98. started_at: Some("2024-01-01T00:00:00Z".to_string()),
  99. completed_at: Some("2024-01-01T00:00:00Z".to_string()),
  100. lane_events: vec![],
  101. current_blocker: None,
  102. error: None,
  103. }
  104. }
  105. #[test]
  106. fn detects_completion_when_all_conditions_met() {
  107. let output = test_output();
  108. let result = detect_lane_completion(&output, true, true);
  109. assert!(result.is_some());
  110. let context = result.unwrap();
  111. assert!(context.completed);
  112. assert_eq!(context.green_level, 3);
  113. assert_eq!(context.blocker, LaneBlocker::None);
  114. }
  115. #[test]
  116. fn no_completion_when_error_present() {
  117. let mut output = test_output();
  118. output.error = Some("Build failed".to_string());
  119. let result = detect_lane_completion(&output, true, true);
  120. assert!(result.is_none());
  121. }
  122. #[test]
  123. fn no_completion_when_not_finished() {
  124. let mut output = test_output();
  125. output.status = "Running".to_string();
  126. let result = detect_lane_completion(&output, true, true);
  127. assert!(result.is_none());
  128. }
  129. #[test]
  130. fn no_completion_when_tests_not_green() {
  131. let output = test_output();
  132. let result = detect_lane_completion(&output, false, true);
  133. assert!(result.is_none());
  134. }
  135. #[test]
  136. fn no_completion_when_not_pushed() {
  137. let output = test_output();
  138. let result = detect_lane_completion(&output, true, false);
  139. assert!(result.is_none());
  140. }
  141. #[test]
  142. fn evaluate_triggers_closeout_for_completed_lane() {
  143. let context = LaneContext {
  144. lane_id: "completed-lane".to_string(),
  145. green_level: 3,
  146. branch_freshness: std::time::Duration::from_secs(0),
  147. blocker: LaneBlocker::None,
  148. review_status: ReviewStatus::Approved,
  149. diff_scope: DiffScope::Scoped,
  150. completed: true,
  151. reconciled: false,
  152. };
  153. let actions = evaluate_completed_lane(&context);
  154. assert!(actions.contains(&PolicyAction::CloseoutLane));
  155. assert!(actions.contains(&PolicyAction::CleanupSession));
  156. }
  157. }