use super::*;
use crate::agents::AgentRole;
use crate::reducer::event::PipelineEvent;
use crate::reducer::state::{DevelopmentStatus, PipelineState};
use crate::reducer::{determine_next_effect, effect::Effect};
#[test]
fn test_continuation_triggered_updates_state() {
use crate::reducer::state::DevelopmentStatus;
let state = create_test_state();
let new_state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
1,
DevelopmentStatus::Partial,
"Did work".to_string(),
Some(vec!["src/main.rs".to_string()]),
Some("Continue".to_string()),
),
);
assert!(new_state.continuation.is_continuation());
assert_eq!(
new_state.continuation.previous_status,
Some(DevelopmentStatus::Partial)
);
assert_eq!(
new_state.continuation.previous_summary,
Some("Did work".to_string())
);
assert_eq!(
new_state.continuation.previous_files_changed,
Some(vec!["src/main.rs".to_string()].into_boxed_slice())
);
assert_eq!(
new_state.continuation.previous_next_steps,
Some("Continue".to_string())
);
assert_eq!(new_state.continuation.continuation_attempt, 1);
assert_eq!(
new_state.agent_chain.current_mode,
crate::agents::DrainMode::Continuation
);
}
#[test]
fn test_continuation_triggered_sets_iteration_from_event() {
use crate::reducer::state::DevelopmentStatus;
let state = PipelineState {
iteration: 99,
..create_test_state()
};
let new_state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
2,
DevelopmentStatus::Partial,
"Did work".to_string(),
None,
None,
),
);
assert_eq!(new_state.iteration, 2);
}
#[test]
fn test_continuation_triggered_with_failed_status() {
use crate::reducer::state::DevelopmentStatus;
let state = create_test_state();
let new_state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
1,
DevelopmentStatus::Failed,
"Build failed".to_string(),
None,
Some("Fix errors".to_string()),
),
);
assert!(new_state.continuation.is_continuation());
assert_eq!(
new_state.continuation.previous_status,
Some(DevelopmentStatus::Failed)
);
assert_eq!(
new_state.continuation.previous_summary,
Some("Build failed".to_string())
);
assert!(new_state.continuation.previous_files_changed.is_none());
}
#[test]
fn test_continuation_succeeded_resets_state() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
assert!(state.continuation.is_continuation());
let new_state = reduce(
state,
PipelineEvent::development_iteration_continuation_succeeded(1, 2),
);
assert!(!new_state.continuation.is_continuation());
assert_eq!(new_state.continuation.continuation_attempt, 0);
assert!(new_state.continuation.previous_status.is_none());
assert_eq!(
new_state.agent_chain.current_mode,
crate::agents::DrainMode::Normal
);
}
#[test]
fn test_continuation_succeeded_sets_iteration_from_event() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
phase: PipelinePhase::Development,
iteration: 99,
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
let new_state = reduce(
state,
PipelineEvent::development_iteration_continuation_succeeded(1, 1),
);
assert_eq!(new_state.iteration, 1);
}
#[test]
fn test_iteration_started_resets_continuation() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
assert!(state.continuation.is_continuation());
let new_state = reduce(state, PipelineEvent::development_iteration_started(2));
assert!(!new_state.continuation.is_continuation());
assert_eq!(new_state.iteration, 2);
}
#[test]
fn test_iteration_completed_resets_continuation() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
phase: PipelinePhase::Development,
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
let new_state = reduce(
state,
PipelineEvent::development_iteration_completed(1, true),
);
assert!(!new_state.continuation.is_continuation());
assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
}
#[test]
fn test_development_phase_completed_resets_continuation() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
phase: PipelinePhase::Development,
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
let new_state = reduce(state, PipelineEvent::development_phase_completed());
assert!(!new_state.continuation.is_continuation());
assert_eq!(new_state.phase, PipelinePhase::Review);
assert_eq!(
new_state.agent_chain.current_mode,
crate::agents::DrainMode::Normal
);
}
#[test]
fn test_same_agent_retry_uses_same_agent_retry_mode_until_success() {
let state = PipelineState {
phase: PipelinePhase::Development,
..create_test_state()
};
let retrying_state = reduce(
state,
PipelineEvent::agent_invocation_failed(
AgentRole::Developer,
AgentName::from("mock"),
1,
crate::reducer::event::AgentErrorKind::InternalError,
false,
),
);
assert!(retrying_state.continuation.same_agent_retry_pending);
assert_eq!(
retrying_state.agent_chain.current_mode,
crate::agents::DrainMode::SameAgentRetry
);
let recovered_state = reduce(
retrying_state,
PipelineEvent::agent_invocation_succeeded(AgentRole::Developer, AgentName::from("mock")),
);
assert_eq!(
recovered_state.agent_chain.current_mode,
crate::agents::DrainMode::Normal
);
}
#[test]
fn test_xsd_retry_uses_xsd_retry_mode_until_success() {
let base = create_test_state();
let state = PipelineState {
phase: PipelinePhase::Development,
agent_chain: base
.agent_chain
.with_drain(crate::agents::AgentDrain::Analysis),
..base
};
let retrying_state = reduce(
state,
PipelineEvent::agent_xsd_validation_failed(
AgentRole::Analysis,
crate::reducer::state::ArtifactType::DevelopmentResult,
"invalid xml".to_string(),
0,
),
);
assert!(retrying_state.continuation.xsd_retry_pending);
assert_eq!(
retrying_state.agent_chain.current_mode,
crate::agents::DrainMode::XsdRetry
);
let recovered_state = reduce(
retrying_state,
PipelineEvent::agent_invocation_succeeded(AgentRole::Analysis, AgentName::from("mock")),
);
assert_eq!(
recovered_state.agent_chain.current_mode,
crate::agents::DrainMode::Normal
);
}
#[test]
fn test_multiple_continuation_triggers_accumulate() {
use crate::reducer::state::DevelopmentStatus;
let state = create_test_state();
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
1,
DevelopmentStatus::Partial,
"First attempt".to_string(),
None,
None,
),
);
assert_eq!(state.continuation.continuation_attempt, 1);
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
1,
DevelopmentStatus::Partial,
"Second attempt".to_string(),
None,
None,
),
);
assert_eq!(state.continuation.continuation_attempt, 2);
assert_eq!(
state.continuation.previous_summary,
Some("Second attempt".to_string())
);
}
#[test]
fn test_continuation_budget_exhausted_switches_to_next_agent() {
use crate::reducer::state::DevelopmentStatus;
let state = create_test_state();
assert_eq!(state.agent_chain.current_agent_index, 0);
let new_state = reduce(
state,
PipelineEvent::development_continuation_budget_exhausted(0, 3, DevelopmentStatus::Partial),
);
assert_eq!(
new_state.phase,
PipelinePhase::CommitMessage,
"Should complete iteration and transition to CommitMessage after budget exhaustion"
);
assert_eq!(
new_state.agent_chain.current_agent_index, 0,
"Agent chain should be reset after iteration completion"
);
assert_eq!(
new_state.continuation.continuation_attempt, 0,
"Should reset continuation attempt after iteration completion"
);
}
#[test]
fn test_continuation_budget_exhausted_resets_continuation_state() {
use crate::reducer::state::ContinuationState;
let state = PipelineState {
continuation: ContinuationState::new().trigger_continuation(
DevelopmentStatus::Partial,
"Work".to_string(),
None,
None,
),
..create_test_state()
};
assert!(state.continuation.is_continuation());
let new_state = reduce(
state,
PipelineEvent::development_continuation_budget_exhausted(0, 3, DevelopmentStatus::Partial),
);
assert!(
!new_state.continuation.is_continuation(),
"Continuation state should be reset when switching to next agent"
);
assert_eq!(
new_state.continuation.continuation_attempt, 0,
"Continuation attempt should be reset for new agent"
);
}
#[test]
fn test_continuation_budget_exhausted_preserves_iteration() {
use crate::reducer::state::DevelopmentStatus;
let mut state = PipelineState {
iteration: 5,
..create_test_state()
};
state.iteration = 5;
let new_state = reduce(
state,
PipelineEvent::development_continuation_budget_exhausted(5, 3, DevelopmentStatus::Failed),
);
assert_eq!(
new_state.iteration, 5,
"Should preserve the iteration number"
);
}
#[test]
fn test_orchestration_detects_exhaustion_after_all_agents_tried() {
use crate::agents::AgentRole;
use crate::reducer::state::{AgentChainState, DevelopmentStatus, PromptPermissionsState};
let agent_chain = AgentChainState::initial()
.with_agents(
vec!["agent-a".to_string(), "agent-b".to_string()],
vec![vec!["model-1".to_string()], vec!["model-2".to_string()]],
AgentRole::Developer,
)
.with_max_cycles(1);
let state = {
let base = PipelineState::initial(5, 3);
PipelineState {
agent_chain,
phase: PipelinePhase::Development,
prompt_permissions: PromptPermissionsState {
locked: true,
restore_needed: true,
..base.prompt_permissions
},
..base
}
};
let state = reduce(
state,
PipelineEvent::development_continuation_budget_exhausted(0, 3, DevelopmentStatus::Failed),
);
assert_eq!(
state.phase,
PipelinePhase::CommitMessage,
"Should complete iteration and transition to CommitMessage after continuation budget exhaustion"
);
assert_eq!(
state.agent_chain.current_agent_index, 0,
"Agent chain should be reset to first agent after iteration completion"
);
assert_eq!(
state.continuation.continuation_attempt, 0,
"Continuation attempt should be reset after iteration completion"
);
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CheckCommitDiff)
|| matches!(effect, Effect::CleanupContinuationContext),
"Should proceed with commit flow after iteration completion; got {effect:?}"
);
}
#[test]
fn test_continuation_budget_with_missing_config_key() {
use crate::reducer::state::DevelopmentStatus;
let continuation = ContinuationState::with_limits(
10, 3, 2, );
let state = PipelineState::initial_with_continuation(1, 0, &continuation);
assert_eq!(
state.continuation.max_continue_count, 3,
"Missing config key should default to 3 total attempts (1 initial + 2 continuations)"
);
assert!(
!state.continuation.continuations_exhausted(),
"Initial attempt (0) should not be exhausted"
);
let state = state.continuation.trigger_continuation(
DevelopmentStatus::Partial,
"partial work".to_string(),
None,
None,
);
assert_eq!(state.continuation_attempt, 1);
assert!(
!state.continuations_exhausted(),
"Attempt 1 should not be exhausted"
);
let state = state.trigger_continuation(
DevelopmentStatus::Partial,
"partial work 2".to_string(),
None,
None,
);
assert_eq!(state.continuation_attempt, 2);
assert!(
!state.continuations_exhausted(),
"Attempt 2 should not be exhausted"
);
let state = state.trigger_continuation(
DevelopmentStatus::Partial,
"partial work 3 attempt".to_string(),
None,
None,
);
assert_eq!(
state.continuation_attempt, 2,
"defensive check should prevent increment to 3"
);
assert!(
!state.continuations_exhausted(),
"counter at 2, so 2 < 3 is true (not exhausted by counter check)"
);
assert!(
!state.continue_pending,
"defensive check should clear continue_pending"
);
}
#[test]
fn test_orchestration_fires_budget_exhausted_at_cap() {
use crate::reducer::state::DevelopmentStatus;
let continuation = ContinuationState::with_limits(10, 3, 2);
let state = PipelineState::initial_with_continuation(1, 0, &continuation);
let state = reduce(
PipelineState {
phase: PipelinePhase::Development,
..state.clone()
},
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial".to_string(),
None,
None,
),
);
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial".to_string(),
None,
None,
),
);
assert_eq!(state.continuation.continuation_attempt, 2);
assert!(!state.continuation.continuations_exhausted());
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial attempt 3".to_string(),
None,
None,
),
);
assert_eq!(
state.continuation.continuation_attempt, 2,
"defensive check should prevent increment to 3"
);
assert!(
!state.continuation.continuations_exhausted(),
"counter at 2, so 2 < 3 is true"
);
assert!(
!state.continuation.continue_pending,
"defensive check should clear continue_pending"
);
}
#[test]
fn test_trigger_continuation_at_boundary_does_not_increment() {
use crate::reducer::state::DevelopmentStatus;
let continuation = ContinuationState::with_limits(10, 3, 2);
let state = PipelineState::initial_with_continuation(1, 0, &continuation);
let state = reduce(
PipelineState {
phase: PipelinePhase::Development,
..state.clone()
},
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial".to_string(),
None,
None,
),
);
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial".to_string(),
None,
None,
),
);
assert_eq!(state.continuation.continuation_attempt, 2);
assert!(!state.continuation.continuations_exhausted());
let old_counter = state.continuation.continuation_attempt;
let state = reduce(
state,
PipelineEvent::development_iteration_continuation_triggered(
0,
DevelopmentStatus::Partial,
"partial attempt 3".to_string(),
None,
None,
),
);
assert_eq!(
state.continuation.continuation_attempt, old_counter,
"trigger_continuation defensive check must prevent counter increment at boundary"
);
assert_eq!(state.continuation.continuation_attempt, 2);
assert!(!state.continuation.continue_pending);
assert!(!state.continuation.context_write_pending);
}
#[test]
fn test_outcome_applied_exhausts_before_triggering_third_continuation() {
use crate::reducer::event::DevelopmentEvent;
use crate::reducer::state::{DevelopmentStatus, DevelopmentValidatedOutcome};
let continuation = ContinuationState::with_limits(10, 3, 2);
let initial_state = PipelineState::initial_with_continuation(5, 0, &continuation);
let state = reduce(
PipelineState {
phase: PipelinePhase::Development,
..initial_state.clone()
},
PipelineEvent::development_iteration_started(0),
);
let state = reduce(
PipelineState {
development_validated_outcome: Some(DevelopmentValidatedOutcome {
iteration: 0,
status: DevelopmentStatus::Partial,
summary: "partial work".to_string(),
files_changed: None,
next_steps: None,
}),
..state.clone()
},
PipelineEvent::Development(DevelopmentEvent::OutcomeApplied { iteration: 0 }),
);
assert_eq!(state.continuation.continuation_attempt, 1);
let state = reduce(
PipelineState {
development_validated_outcome: Some(DevelopmentValidatedOutcome {
iteration: 0,
status: DevelopmentStatus::Partial,
summary: "more partial work".to_string(),
files_changed: None,
next_steps: None,
}),
..state.clone()
},
PipelineEvent::Development(DevelopmentEvent::OutcomeApplied { iteration: 0 }),
);
assert_eq!(state.continuation.continuation_attempt, 2);
let state = reduce(
PipelineState {
development_validated_outcome: Some(DevelopmentValidatedOutcome {
iteration: 0,
status: DevelopmentStatus::Partial,
summary: "still more partial work".to_string(),
files_changed: None,
next_steps: None,
}),
..state.clone()
},
PipelineEvent::Development(DevelopmentEvent::OutcomeApplied { iteration: 0 }),
);
assert_eq!(
state.continuation.continuation_attempt, 0,
"OutcomeApplied at attempt 2 should exhaust budget (2+1>=3) and reset counter via agent switch"
);
assert!(!state.continuation.is_continuation());
}