| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- //! 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::green_contract::{GreenContract, GreenContractOutcome, GreenLevel};
- use runtime::{
- apply_policy, BranchFreshness, DiffScope, LaneBlocker, LaneContext, PolicyAction,
- PolicyCondition, PolicyEngine, PolicyRule, ReconcileReason, ReviewStatus, StaleBranchAction,
- StaleBranchPolicy,
- };
- /// 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]);
- }
- /// worker_boot + recovery_recipes + policy_engine integration:
- /// When a session completes with a provider failure, does the worker
- /// status transition trigger the correct recovery recipe, and does
- /// the resulting recovery state feed into policy decisions?
- #[test]
- fn worker_provider_failure_flows_through_recovery_to_policy() {
- use runtime::recovery_recipes::{
- attempt_recovery, FailureScenario, RecoveryContext, RecoveryResult, RecoveryStep,
- };
- use runtime::worker_boot::{WorkerFailureKind, WorkerRegistry, WorkerStatus};
- // given — a worker that encounters a provider failure during session completion
- let registry = WorkerRegistry::new();
- let worker = registry.create("/tmp/repo-recovery-test", &[], true);
- // Worker reaches ready state
- registry
- .observe(&worker.worker_id, "Ready for your input\n>")
- .expect("ready observe should succeed");
- registry
- .send_prompt(&worker.worker_id, Some("Run analysis"))
- .expect("prompt send should succeed");
- // Session completes with provider failure (finish="unknown", tokens=0)
- let failed_worker = registry
- .observe_completion(&worker.worker_id, "unknown", 0)
- .expect("completion observe should succeed");
- assert_eq!(failed_worker.status, WorkerStatus::Failed);
- let failure = failed_worker
- .last_error
- .expect("worker should have recorded error");
- assert_eq!(failure.kind, WorkerFailureKind::Provider);
- // Bridge: WorkerFailureKind -> FailureScenario
- let scenario = FailureScenario::from_worker_failure_kind(failure.kind);
- assert_eq!(scenario, FailureScenario::ProviderFailure);
- // Recovery recipe lookup and execution
- let mut ctx = RecoveryContext::new();
- let result = attempt_recovery(&scenario, &mut ctx);
- // then — recovery should recommend RestartWorker step
- assert!(
- matches!(result, RecoveryResult::Recovered { steps_taken: 1 }),
- "provider failure should recover via single RestartWorker step, got: {result:?}"
- );
- assert!(
- ctx.events().iter().any(|e| {
- matches!(
- e,
- runtime::recovery_recipes::RecoveryEvent::RecoveryAttempted {
- result: RecoveryResult::Recovered { steps_taken: 1 },
- ..
- }
- )
- }),
- "recovery should emit structured attempt event"
- );
- // Policy integration: recovery success + green status = merge-ready
- // (Simulating the policy check that would happen after successful recovery)
- let recovery_success = matches!(result, RecoveryResult::Recovered { .. });
- let green_level = 3; // Workspace green
- let not_stale = Duration::from_secs(30 * 60); // 30 min — fresh
- let post_recovery_context = LaneContext::new(
- "recovered-lane",
- green_level,
- not_stale,
- LaneBlocker::None,
- ReviewStatus::Approved,
- DiffScope::Scoped,
- false,
- );
- let policy_engine = PolicyEngine::new(vec![
- // Rule: if recovered from failure + green + approved -> merge
- PolicyRule::new(
- "merge-after-successful-recovery",
- PolicyCondition::And(vec![
- PolicyCondition::GreenAt { level: 3 },
- PolicyCondition::ReviewPassed,
- ]),
- PolicyAction::MergeToDev,
- 10,
- ),
- ]);
- // Recovery success is a pre-condition; policy evaluates post-recovery context
- assert!(
- recovery_success,
- "recovery must succeed for lane to proceed"
- );
- let actions = policy_engine.evaluate(&post_recovery_context);
- assert_eq!(
- actions,
- vec![PolicyAction::MergeToDev],
- "post-recovery green+approved lane should be merge-ready"
- );
- }
|