use crate::common::domain_types::AgentName;
fn agent_names_from_strings(strings: &[String]) -> Vec<AgentName> {
strings.iter().cloned().map(AgentName::from).collect()
}
#[test]
fn test_commit_created_clears_agent_chain_when_transitioning_to_review() {
use crate::reducer::orchestration::determine_next_effect;
let developer_chain = crate::reducer::state::AgentChainState::initial()
.with_agents(
vec!["dev-agent-1".to_string(), "dev-agent-2".to_string()],
vec![vec![], vec![]],
crate::agents::AgentRole::Developer,
)
.with_max_cycles(3);
let mut state = PipelineState {
phase: PipelinePhase::Development,
previous_phase: Some(PipelinePhase::Development),
iteration: 4, total_iterations: 5,
total_reviewer_passes: 2,
agent_chain: developer_chain,
commit: CommitState::Generated {
message: "test commit".to_string(),
},
..create_test_state()
};
assert!(!state.agent_chain.agents.is_empty());
assert_eq!(
state.agent_chain.current_agent(),
Some(&"dev-agent-1".to_string())
);
assert_eq!(
state.agent_chain.current_role,
crate::agents::AgentRole::Developer
);
state = reduce(
state,
PipelineEvent::commit_created("abc123".to_string(), "test commit".to_string()),
);
assert_eq!(
state.phase,
PipelinePhase::Review,
"Should transition to Review phase after last development iteration commit"
);
assert!(
state.agent_chain.agents.is_empty(),
"Agent chain should be cleared when transitioning from Development to Review"
);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Review,
..
}
),
"Orchestration should emit InitializeAgentChain for Reviewer, got {effect:?}"
);
}
#[test]
fn test_review_uses_agent_from_state_chain_not_context() {
use crate::reducer::orchestration::determine_next_effect;
let reviewer_chain = crate::reducer::state::AgentChainState::initial()
.with_agents(
vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
],
vec![vec![], vec![], vec![]],
crate::agents::AgentRole::Reviewer,
)
.with_max_cycles(3);
let state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 2,
agent_chain: reviewer_chain,
..create_test_state()
};
assert!(!state.agent_chain.agents.is_empty());
assert_eq!(
state.agent_chain.current_agent(),
Some(&"codex".to_string()),
"First agent should be 'codex' (from fallback chain), not 'claude'"
);
assert_eq!(state.agent_chain.current_agent_index, 0);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareReviewContext { pass: 0 }
),
"Orchestration should emit PrepareReviewContext, got {effect:?}"
);
}
#[test]
fn test_fix_attempt_reinitializes_chain_for_reviewer_role() {
use crate::reducer::orchestration::determine_next_effect;
let developer_chain = crate::reducer::state::AgentChainState::initial().with_agents(
vec!["dev-1".to_string(), "dev-2".to_string()],
vec![vec![], vec![]],
crate::agents::AgentRole::Developer,
);
let state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 1,
review_issues_found: true,
agent_chain: developer_chain.with_drain(crate::agents::AgentDrain::Fix),
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareFixPrompt {
pass: 0,
prompt_mode: crate::reducer::state::PromptMode::Normal,
}
),
"Expected fix prompt preparation for explicit Fix drain, got {effect:?}"
);
}
#[test]
fn test_auth_failure_during_review_advances_agent_chain() {
use crate::reducer::event::AgentErrorKind;
let reviewer_chain = crate::reducer::state::AgentChainState::initial()
.with_agents(
vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
],
vec![vec![], vec![], vec![]],
crate::agents::AgentRole::Reviewer,
)
.with_max_cycles(3);
let mut state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 2,
agent_chain: reviewer_chain,
..create_test_state()
};
assert_eq!(
state.agent_chain.current_agent(),
Some(&"codex".to_string())
);
assert_eq!(state.agent_chain.current_agent_index, 0);
state = reduce(
state,
PipelineEvent::agent_invocation_failed(
crate::agents::AgentRole::Reviewer,
AgentName::from("codex"),
1,
AgentErrorKind::Authentication,
false, ),
);
assert_eq!(
state.agent_chain.current_agent(),
Some(&"opencode".to_string()),
"Should advance to next agent after auth failure"
);
assert_eq!(state.agent_chain.current_agent_index, 1);
state = reduce(
state,
PipelineEvent::agent_invocation_failed(
crate::agents::AgentRole::Reviewer,
AgentName::from("opencode"),
1,
AgentErrorKind::Authentication,
false,
),
);
assert_eq!(
state.agent_chain.current_agent(),
Some(&"claude".to_string()),
"Should advance to third agent after second auth failure"
);
assert_eq!(state.agent_chain.current_agent_index, 2);
}
#[test]
fn test_handler_reads_correct_agent_from_state_after_chain_initialized() {
let reviewers = vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
];
let state = reduce(
PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 2,
..create_test_state()
},
PipelineEvent::agent_chain_initialized(
crate::agents::AgentDrain::Review,
agent_names_from_strings(&reviewers),
vec![],
3,
1000,
2.0,
60000,
),
);
let review_agent = state.agent_chain.current_agent().cloned();
assert!(
review_agent.is_some(),
"Handler should get Some(agent) from state.agent_chain.current_agent(), got None"
);
assert_eq!(
review_agent,
Some("codex".to_string()),
"Handler should pass 'codex' to run_review_pass, not '{review_agent:?}'"
);
assert_eq!(state.agent_chain.agents.len(), 3);
assert_eq!(state.agent_chain.current_agent_index, 0);
}
#[test]
fn test_full_pipeline_flow_uses_correct_reviewer_agent() {
use crate::reducer::orchestration::determine_next_effect;
let dev_chain = crate::reducer::state::AgentChainState::initial()
.with_agents(
vec!["claude".to_string()], vec![vec![]],
crate::agents::AgentRole::Developer,
)
.with_max_cycles(3);
let mut state = PipelineState {
phase: PipelinePhase::Development,
previous_phase: Some(PipelinePhase::Development),
iteration: 4, total_iterations: 5,
total_reviewer_passes: 2,
agent_chain: dev_chain,
commit: CommitState::Generated {
message: "test".to_string(),
},
..create_test_state()
};
state = reduce(
state,
PipelineEvent::commit_created("abc123".to_string(), "test".to_string()),
);
assert_eq!(state.phase, PipelinePhase::Review);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Review,
..
}
),
"Should request reviewer chain initialization, got {effect:?}"
);
let review_agents = vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
];
state = reduce(
state,
PipelineEvent::agent_chain_initialized(
crate::agents::AgentDrain::Review,
agent_names_from_strings(&review_agents),
vec![],
3,
1000,
2.0,
60000,
),
);
assert_eq!(
state.agent_chain.current_agent(),
Some(&"codex".to_string()),
"First reviewer agent should be 'codex', not 'claude'"
);
assert_eq!(
state.agent_chain.current_role,
crate::agents::AgentRole::Reviewer
);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareReviewContext { pass: 0 }
),
"Should emit PrepareReviewContext, got {effect:?}"
);
}
#[test]
fn test_event_loop_state_consistency_for_review_agent() {
use crate::reducer::orchestration::determine_next_effect;
let mut state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 2,
agent_chain: crate::reducer::state::AgentChainState::initial(),
..create_test_state()
};
assert!(
state.agent_chain.agents.is_empty(),
"Agent chain should be empty before initialization"
);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Review,
..
}
),
"Expected InitializeAgentChain, got {effect:?}"
);
let review_agents = vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
];
let event = PipelineEvent::agent_chain_initialized(
crate::agents::AgentDrain::Review,
agent_names_from_strings(&review_agents),
vec![],
3,
1000,
2.0,
60000,
);
state = reduce(state, event);
let handler_state = state.clone();
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareReviewContext { pass: 0 }
),
"Expected PrepareReviewContext, got {effect:?}"
);
state = reduce(state, PipelineEvent::review_context_prepared(0));
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::MaterializeReviewInputs { pass: 0 }
),
"Expected MaterializeReviewInputs, got {effect:?}"
);
let sig = state.agent_chain.consumer_signature_sha256();
state = reduce(
state,
PipelineEvent::review_inputs_materialized(
0,
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Plan,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: sig.clone(),
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: None,
inline_budget_bytes: None,
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: sig,
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: None,
inline_budget_bytes: None,
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
),
);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareReviewPrompt { pass: 0, .. }
),
"Expected PrepareReviewPrompt, got {effect:?}"
);
state = reduce(state, PipelineEvent::review_prompt_prepared(0));
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::CleanupRequiredFiles { ref files }
if files.iter().any(|f| f.contains("issues.xml"))
),
"Expected CleanupRequiredFiles for issues.xml, got {effect:?}"
);
state = reduce(state, PipelineEvent::review_issues_xml_cleaned(0));
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InvokeReviewAgent { pass: 0 }
),
"Expected InvokeReviewAgent, got {effect:?}"
);
let review_agent = handler_state.agent_chain.current_agent().cloned();
assert!(
review_agent.is_some(),
"Handler should get Some(agent) from state.agent_chain.current_agent(), got None. \
This means the agent chain was not properly populated before InvokeReviewAgent."
);
assert_eq!(
review_agent,
Some("codex".to_string()),
"Handler should pass 'codex' to InvokeReviewAgent, got {review_agent:?}. \
This means the wrong agent is being used."
);
assert_eq!(handler_state.agent_chain.agents.len(), 3);
assert_eq!(handler_state.agent_chain.current_agent_index, 0);
assert_eq!(
handler_state.agent_chain.current_role,
crate::agents::AgentRole::Reviewer
);
}
#[test]
fn test_complete_flow_dev_commit_review_uses_correct_reviewer_agent() {
use crate::reducer::orchestration::determine_next_effect;
let dev_chain = crate::reducer::state::AgentChainState::initial()
.with_agents(
vec!["claude".to_string()], vec![vec![]],
crate::agents::AgentRole::Developer,
)
.with_max_cycles(3);
let mut state = PipelineState {
phase: PipelinePhase::Development,
iteration: 4, total_iterations: 5,
total_reviewer_passes: 2,
agent_chain: dev_chain,
..create_test_state()
};
state = reduce(
state,
PipelineEvent::development_iteration_completed(4, true),
);
assert_eq!(
state.phase,
PipelinePhase::CommitMessage,
"Should transition to CommitMessage after successful dev iteration"
);
assert_eq!(
state.previous_phase,
Some(PipelinePhase::Development),
"previous_phase should be Development"
);
assert!(!state.agent_chain.agents.is_empty());
state = reduce(
state,
PipelineEvent::commit_message_generated("test commit".to_string(), 0),
);
assert_eq!(state.phase, PipelinePhase::CommitMessage);
assert!(matches!(
state.commit,
crate::reducer::state::CommitState::Generated { .. }
));
state = reduce(
state,
PipelineEvent::commit_created("abc123".to_string(), "test commit".to_string()),
);
assert_eq!(
state.phase,
PipelinePhase::Review,
"Should transition to Review after last dev iteration commit"
);
assert!(
state.agent_chain.agents.is_empty(),
"Agent chain should be empty after dev->review transition, got {:?}",
state.agent_chain.agents
);
let mut effect = determine_next_effect(&state);
if matches!(
effect,
crate::reducer::effect::Effect::CleanupContinuationContext
) {
state = reduce(
state,
PipelineEvent::development_continuation_context_cleaned(),
);
effect = determine_next_effect(&state);
}
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Review,
..
}
),
"Orchestration should request reviewer chain initialization, got {effect:?}"
);
let last_reviewer_agents = vec![
"codex".to_string(),
"opencode".to_string(),
"claude".to_string(),
];
state = reduce(
state,
PipelineEvent::agent_chain_initialized(
crate::agents::AgentDrain::Review,
agent_names_from_strings(&last_reviewer_agents),
vec![],
3,
1000,
2.0,
60000,
),
);
assert!(!state.agent_chain.agents.is_empty());
assert_eq!(
state.agent_chain.current_agent(),
Some(&"codex".to_string()),
"First reviewer agent should be 'codex'"
);
assert_eq!(
state.agent_chain.current_role,
crate::agents::AgentRole::Reviewer
);
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::PrepareReviewContext { pass: 0 }
),
"Should request PrepareReviewContext, got {effect:?}"
);
state = reduce(state, PipelineEvent::review_context_prepared(0));
let sig = state.agent_chain.consumer_signature_sha256();
state = reduce(
state,
PipelineEvent::review_inputs_materialized(
0,
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Plan,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: sig.clone(),
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: None,
inline_budget_bytes: None,
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: sig,
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: None,
inline_budget_bytes: None,
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
),
);
state = reduce(state, PipelineEvent::review_prompt_prepared(0));
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::CleanupRequiredFiles { ref files }
if files.iter().any(|f| f.contains("issues.xml"))
),
"Should request CleanupRequiredFiles for issues.xml, got {effect:?}"
);
state = reduce(state, PipelineEvent::review_issues_xml_cleaned(0));
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
crate::reducer::effect::Effect::InvokeReviewAgent { pass: 0 }
),
"Should request InvokeReviewAgent, got {effect:?}"
);
let review_agent = state.agent_chain.current_agent().cloned();
assert_eq!(
review_agent,
Some("codex".to_string()),
"Handler should pass 'codex' to run_review_pass"
);
}