use crate::agents::AgentRole;
use crate::common::domain_types::ModelName;
use crate::reducer::event::AgentErrorKind;
use crate::reducer::io_tests::{
create_test_state, reduce, AgentName, PipelineEvent, PipelinePhase, PipelineState,
};
#[test]
fn test_agent_invocation_started_preserves_agent_chain_indices() {
let base_state = create_test_state();
let mut agent_chain = base_state.agent_chain.with_agents(
vec!["agent1".to_string(), "agent2".to_string()],
vec![
vec!["model1".to_string()],
vec!["model2".to_string(), "model3".to_string()],
],
AgentRole::Developer,
);
agent_chain.retry_cycle = 2;
agent_chain.rate_limit_continuation_prompt =
Some(crate::reducer::state::RateLimitContinuationPrompt {
drain: crate::agents::AgentDrain::Development,
role: AgentRole::Developer,
prompt: "saved prompt".to_string(),
});
let state = PipelineState {
agent_chain: agent_chain.switch_to_next_agent().advance_to_next_model(),
..base_state
};
assert_eq!(state.agent_chain.current_agent_index, 1);
assert_eq!(state.agent_chain.current_model_index, 1);
assert_eq!(state.agent_chain.retry_cycle, 2);
let new_state = reduce(
state,
PipelineEvent::agent_invocation_started(
AgentRole::Developer,
AgentName::from("agent1"),
Some(ModelName::from("model1")),
),
);
assert_eq!(new_state.agent_chain.current_agent_index, 1);
assert_eq!(new_state.agent_chain.current_model_index, 1);
assert_eq!(new_state.agent_chain.retry_cycle, 2);
assert_eq!(
new_state.agent_chain.rate_limit_continuation_prompt,
Some(crate::reducer::state::RateLimitContinuationPrompt {
drain: crate::agents::AgentDrain::Development,
role: AgentRole::Developer,
prompt: "saved prompt".to_string(),
})
);
}
#[test]
fn test_agent_invocation_succeeded_preserves_indices() {
let state = create_test_state();
let new_state = reduce(
state.clone(),
PipelineEvent::agent_invocation_succeeded(AgentRole::Developer, AgentName::from("agent1")),
);
assert_eq!(
new_state.agent_chain.current_agent_index,
state.agent_chain.current_agent_index
);
assert_eq!(
new_state.agent_chain.current_model_index,
state.agent_chain.current_model_index
);
}
#[test]
fn test_agent_invocation_failed_with_retriable_network_sets_check_pending() {
let base_state = create_test_state();
let state = PipelineState {
agent_chain: base_state.agent_chain.with_agents(
vec!["agent1".to_string()],
vec![vec!["model1".to_string(), "model2".to_string()]],
AgentRole::Developer,
),
..base_state
};
let new_state = reduce(
state,
PipelineEvent::agent_invocation_failed(
AgentRole::Developer,
AgentName::from("agent1"),
1,
AgentErrorKind::Network,
true,
),
);
assert_eq!(new_state.agent_chain.current_agent_index, 0);
assert_eq!(
new_state.agent_chain.current_model_index, 0,
"Network error should NOT advance model (check_pending takes priority)"
);
assert!(
new_state.connectivity.check_pending,
"Network error should set check_pending for connectivity verification"
);
}
#[test]
fn test_agent_fallback_triggered_switches_agent() {
let base_state = create_test_state();
let state = PipelineState {
agent_chain: base_state.agent_chain.with_agents(
vec!["agent1".to_string(), "agent2".to_string()],
vec![vec!["model1".to_string()], vec!["model2".to_string()]],
AgentRole::Developer,
),
..base_state
};
let new_state = reduce(
state,
PipelineEvent::agent_fallback_triggered(
AgentRole::Developer,
AgentName::from("agent1"),
AgentName::from("agent2"),
),
);
assert_eq!(new_state.agent_chain.current_agent_index, 1);
assert_eq!(new_state.agent_chain.current_model_index, 0);
}
#[test]
fn test_agent_chain_exhausted_increments_retry_cycle() {
let state = create_test_state();
let initial_retry_cycle = state.agent_chain.retry_cycle;
let new_state = reduce(
state,
PipelineEvent::agent_chain_exhausted(AgentRole::Developer),
);
assert_eq!(new_state.agent_chain.retry_cycle, initial_retry_cycle + 1);
}
#[test]
fn test_agent_chain_exhausted_resets_indices() {
let base_state = create_test_state();
let state = PipelineState {
agent_chain: base_state.agent_chain.with_agents(
vec!["agent1".to_string(), "agent2".to_string()],
vec![
vec!["model1".to_string(), "model2".to_string()],
vec!["model3".to_string()],
],
AgentRole::Developer,
),
..base_state
};
let state = PipelineState {
agent_chain: state
.agent_chain
.advance_to_next_model()
.switch_to_next_agent(),
..state
};
let new_state = reduce(
state,
PipelineEvent::agent_chain_exhausted(AgentRole::Developer),
);
assert_eq!(new_state.agent_chain.current_agent_index, 0);
assert_eq!(new_state.agent_chain.current_model_index, 0);
}
#[test]
fn test_agent_model_fallback_triggered_advances_to_next_model() {
let base_state = create_test_state();
let state = PipelineState {
agent_chain: base_state.agent_chain.with_agents(
vec!["agent1".to_string()],
vec![vec![
"model1".to_string(),
"model2".to_string(),
"model3".to_string(),
]],
AgentRole::Developer,
),
..base_state
};
assert_eq!(state.agent_chain.current_model_index, 0);
let new_state = reduce(
state,
PipelineEvent::agent_model_fallback_triggered(
AgentRole::Developer,
AgentName::from("agent1"),
ModelName::from("model1"),
ModelName::from("model2"),
),
);
assert_eq!(new_state.agent_chain.current_agent_index, 0);
assert_eq!(new_state.agent_chain.current_model_index, 1);
}
#[test]
fn test_agent_retry_cycle_started_clears_backoff_pending() {
let mut base_state = PipelineState {
phase: PipelinePhase::Development,
iteration: 3,
reviewer_pass: 1,
..create_test_state()
};
base_state.agent_chain.backoff_pending_ms = Some(1000);
let new_state = reduce(
base_state.clone(),
PipelineEvent::agent_retry_cycle_started(AgentRole::Developer, 2),
);
assert_eq!(new_state.phase, base_state.phase);
assert_eq!(new_state.iteration, base_state.iteration);
assert_eq!(new_state.reviewer_pass, base_state.reviewer_pass);
assert_eq!(
new_state.agent_chain.current_agent_index,
base_state.agent_chain.current_agent_index
);
assert_eq!(
new_state.agent_chain.current_model_index,
base_state.agent_chain.current_model_index
);
assert!(
new_state.agent_chain.backoff_pending_ms.is_none(),
"backoff_pending_ms should be cleared after RetryCycleStarted"
);
}
#[test]
fn test_agent_invocation_failed_non_retriable_retries_same_agent_until_budget_exhausted() {
use crate::reducer::state::ContinuationState;
let base_state = create_test_state();
let mut continuation = ContinuationState::with_limits(2, 3, 2);
continuation.xsd_retry_count = 7;
continuation.xsd_retry_pending = true;
let state = PipelineState {
agent_chain: base_state.agent_chain.with_agents(
vec!["agent1".to_string(), "agent2".to_string()],
vec![vec!["model1".to_string()], vec!["model2".to_string()]],
AgentRole::Developer,
),
continuation,
..base_state
};
assert_eq!(state.agent_chain.current_agent_index, 0);
let after_first_failure = reduce(
state,
PipelineEvent::agent_invocation_failed(
AgentRole::Developer,
AgentName::from("agent1"),
1,
AgentErrorKind::ParsingError,
false,
),
);
assert_eq!(after_first_failure.agent_chain.current_agent_index, 0);
assert!(after_first_failure.continuation.same_agent_retry_pending);
let after_second_failure = reduce(
after_first_failure,
PipelineEvent::agent_invocation_failed(
AgentRole::Developer,
AgentName::from("agent1"),
1,
AgentErrorKind::ParsingError,
false,
),
);
assert_eq!(after_second_failure.agent_chain.current_agent_index, 1);
assert_eq!(after_second_failure.agent_chain.current_model_index, 0);
assert_eq!(
after_second_failure.continuation.xsd_retry_count, 0,
"XSD retry budget must not carry across agents"
);
assert!(
!after_second_failure.continuation.xsd_retry_pending,
"XSD retry pending must be cleared when switching agents"
);
}