use super::*;
use crate::agents::AgentRole;
use crate::reducer::effect::Effect;
use crate::reducer::orchestration::determine_next_effect;
use crate::reducer::state::{AgentChainState, ContinuationState};
#[test]
fn test_context_prepared_clears_continue_pending_to_prevent_infinite_loop() {
let state = PipelineState {
phase: PipelinePhase::Development,
iteration: 1,
total_iterations: 5,
agent_chain: AgentChainState::initial()
.with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
)
.with_drain(crate::agents::AgentDrain::Development),
continuation: ContinuationState {
continue_pending: true,
context_write_pending: false,
continuation_attempt: 1,
..ContinuationState::default()
},
prompt_permissions: crate::reducer::state::PromptPermissionsState {
locked: true,
restore_needed: true,
restored: false,
last_warning: None,
},
..PipelineState::initial(5, 2)
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::PrepareDevelopmentContext { .. }),
"Expected PrepareDevelopmentContext when continue_pending=true, got {effect:?}"
);
let new_state = reduce(state, PipelineEvent::development_context_prepared(1));
assert!(
!new_state.continuation.continue_pending,
"continue_pending should be false after ContextPrepared to prevent infinite loop"
);
let next_effect = determine_next_effect(&new_state);
assert!(
matches!(next_effect, Effect::MaterializeDevelopmentInputs { .. }),
"Expected MaterializeDevelopmentInputs after ContextPrepared, got {next_effect:?}"
);
}
#[test]
fn test_context_prepared_still_sets_iteration() {
let state = PipelineState {
phase: PipelinePhase::Development,
iteration: 3,
development_context_prepared_iteration: None,
..PipelineState::initial(5, 2)
};
let new_state = reduce(state, PipelineEvent::development_context_prepared(3));
assert_eq!(new_state.development_context_prepared_iteration, Some(3));
}
#[test]
fn test_context_prepared_is_idempotent_on_continue_pending() {
let state = PipelineState {
phase: PipelinePhase::Development,
iteration: 2,
continuation: ContinuationState {
continue_pending: false, ..ContinuationState::default()
},
..PipelineState::initial(5, 2)
};
let new_state = reduce(state, PipelineEvent::development_context_prepared(2));
assert!(!new_state.continuation.continue_pending);
}
#[test]
fn test_fix_prompt_prepared_clears_fix_continue_pending_to_prevent_infinite_loop() {
let mut state = PipelineState::initial(5, 2);
state.phase = PipelinePhase::Review;
state.reviewer_pass = 0;
state.total_reviewer_passes = 2;
state.review_issues_found = true;
state.agent_chain = AgentChainState::initial()
.with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Reviewer,
)
.with_drain(crate::agents::AgentDrain::Fix);
state.continuation = ContinuationState {
fix_continue_pending: true,
fix_continuation_attempt: 1,
..ContinuationState::default()
};
state.prompt_permissions.locked = true;
state.prompt_permissions.restore_needed = true;
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::PrepareFixPrompt { .. }),
"Expected PrepareFixPrompt when fix_continue_pending=true, got {effect:?}"
);
let new_state = reduce(state, PipelineEvent::fix_prompt_prepared(0));
assert!(
!new_state.continuation.fix_continue_pending,
"fix_continue_pending should be false after FixPromptPrepared to prevent infinite loop"
);
let next_effect = determine_next_effect(&new_state);
assert!(
matches!(next_effect, Effect::CleanupRequiredFiles { ref files } if files.iter().any(|f| f.contains("fix_result.xml"))),
"Expected CleanupRequiredFiles for fix_result.xml after FixPromptPrepared, got {next_effect:?}"
);
}
#[test]
fn test_fix_prompt_prepared_still_sets_pass() {
let state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 1,
fix_prompt_prepared_pass: None,
..PipelineState::initial(5, 2)
};
let new_state = reduce(state, PipelineEvent::fix_prompt_prepared(1));
assert_eq!(new_state.fix_prompt_prepared_pass, Some(1));
}
#[test]
fn test_fix_prompt_prepared_is_idempotent_on_fix_continue_pending() {
let state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
continuation: ContinuationState {
fix_continue_pending: false, ..ContinuationState::default()
},
..PipelineState::initial(5, 2)
};
let new_state = reduce(state, PipelineEvent::fix_prompt_prepared(0));
assert!(!new_state.continuation.fix_continue_pending);
}
use crate::reducer::state::DevelopmentStatus;
#[test]
fn test_continuation_does_not_cause_infinite_loop_in_event_loop_simulation() {
let mut state = PipelineState {
phase: PipelinePhase::Development,
iteration: 0,
total_iterations: 1,
agent_chain: AgentChainState::initial()
.with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
)
.with_drain(crate::agents::AgentDrain::Development),
continuation: ContinuationState {
continue_pending: true,
context_write_pending: false,
continuation_attempt: 1,
..ContinuationState::default()
},
development_context_prepared_iteration: None,
..PipelineState::initial(1, 0)
};
let max_iterations = 100;
let mut last_effect_discriminant = None;
let mut repeat_count = 0;
for i in 0..max_iterations {
let effect = determine_next_effect(&state);
let current_discriminant = std::mem::discriminant(&effect);
if Some(current_discriminant) == last_effect_discriminant {
repeat_count += 1;
assert!(repeat_count <= 5,
"Potential infinite loop detected at iteration {i}: effect {effect:?} repeated {repeat_count} times"
);
} else {
repeat_count = 1;
last_effect_discriminant = Some(current_discriminant);
}
state = match effect {
Effect::LockPromptPermissions => {
reduce(state, PipelineEvent::prompt_permissions_locked(None))
}
Effect::RestorePromptPermissions => {
reduce(state, PipelineEvent::prompt_permissions_restored())
}
Effect::PrepareDevelopmentContext { iteration } => reduce(
state,
PipelineEvent::development_context_prepared(iteration),
),
Effect::MaterializeDevelopmentInputs { iteration } => {
let sig = state.agent_chain.consumer_signature_sha256();
let prompt = crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Prompt,
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,
};
let plan = crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Plan,
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,
};
reduce(
state,
PipelineEvent::development_inputs_materialized(iteration, prompt, plan),
)
}
Effect::PrepareDevelopmentPrompt { iteration, .. } => {
reduce(state, PipelineEvent::development_prompt_prepared(iteration))
}
Effect::CleanupRequiredFiles { files }
if files.iter().any(|f| f.contains("development_result.xml")) =>
{
let iteration = state.iteration;
reduce(state, PipelineEvent::development_xml_cleaned(iteration))
}
Effect::InvokeDevelopmentAgent { iteration } => {
reduce(state, PipelineEvent::development_agent_invoked(iteration))
}
Effect::ExtractDevelopmentXml { iteration } => {
reduce(state, PipelineEvent::development_xml_extracted(iteration))
}
Effect::ValidateDevelopmentXml { iteration } => reduce(
state,
PipelineEvent::development_xml_validated(
iteration,
DevelopmentStatus::Completed,
"done".to_string(),
None,
None,
),
),
Effect::ArchiveDevelopmentXml { iteration } => {
reduce(state, PipelineEvent::development_xml_archived(iteration))
}
Effect::ApplyDevelopmentOutcome { iteration } => reduce(
state,
PipelineEvent::development_iteration_completed(iteration, true),
),
Effect::SaveCheckpoint { .. } => {
break;
}
_ => {
break;
}
};
}
}
use crate::reducer::state::FixStatus;
#[test]
fn test_fix_continuation_does_not_cause_infinite_loop_in_event_loop_simulation() {
let mut state = PipelineState {
phase: PipelinePhase::Review,
reviewer_pass: 0,
total_reviewer_passes: 2,
review_issues_found: true,
agent_chain: AgentChainState::initial()
.with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Reviewer,
)
.with_drain(crate::agents::AgentDrain::Fix),
continuation: ContinuationState {
fix_continue_pending: true,
fix_continuation_attempt: 1,
..ContinuationState::default()
},
fix_prompt_prepared_pass: None,
..PipelineState::initial(5, 2)
};
let max_iterations = 100;
let mut last_effect_discriminant = None;
let mut repeat_count = 0;
for i in 0..max_iterations {
let effect = determine_next_effect(&state);
let current_discriminant = std::mem::discriminant(&effect);
if Some(current_discriminant) == last_effect_discriminant {
repeat_count += 1;
assert!(repeat_count <= 5,
"Potential infinite loop at iteration {i}: effect {effect:?} repeated {repeat_count} times"
);
} else {
repeat_count = 1;
last_effect_discriminant = Some(current_discriminant);
}
state = match effect {
Effect::LockPromptPermissions => {
reduce(state, PipelineEvent::prompt_permissions_locked(None))
}
Effect::RestorePromptPermissions => {
reduce(state, PipelineEvent::prompt_permissions_restored())
}
Effect::PrepareFixPrompt { pass, .. } => {
reduce(state, PipelineEvent::fix_prompt_prepared(pass))
}
Effect::CleanupRequiredFiles { files }
if files.iter().any(|f| f.contains("fix_result.xml")) =>
{
let pass = state.reviewer_pass;
reduce(state, PipelineEvent::fix_result_xml_cleaned(pass))
}
Effect::InvokeFixAgent { pass } => {
reduce(state, PipelineEvent::fix_agent_invoked(pass))
}
Effect::ExtractFixResultXml { pass } => {
reduce(state, PipelineEvent::fix_result_xml_extracted(pass))
}
Effect::ValidateFixResultXml { pass } => reduce(
state,
PipelineEvent::fix_result_xml_validated(
pass,
FixStatus::AllIssuesAddressed,
Some("All issues resolved".to_string()),
),
),
Effect::ArchiveFixResultXml { pass } => {
reduce(state, PipelineEvent::fix_result_xml_archived(pass))
}
Effect::ApplyFixOutcome { pass } => {
reduce(state, PipelineEvent::fix_outcome_applied(pass))
}
Effect::SaveCheckpoint { .. } => {
break;
}
_ => {
break;
}
};
}
assert!(
!state.continuation.fix_continue_pending,
"fix_continue_pending should be false after FixPromptPrepared"
);
}