use super::*;
#[test]
fn test_determine_effect_commit_message_empty_chain() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::NotStarted,
agent_chain: AgentChainState::initial(),
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(matches!(
effect,
Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Commit,
..
}
));
}
#[test]
fn test_determine_effect_commit_message_role_mismatch_reinitializes_chain() {
let chain = AgentChainState::initial().with_agents(
vec!["dev-agent".to_string()],
vec![vec![]],
AgentRole::Developer,
);
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::NotStarted,
agent_chain: chain,
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(matches!(
effect,
Effect::InitializeAgentChain {
drain: crate::agents::AgentDrain::Commit,
..
}
));
}
#[test]
fn test_determine_effect_commit_message_not_started() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::NotStarted,
commit_diff_prepared: true, commit_diff_content_id_sha256: Some("id".to_string()),
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(matches!(effect, Effect::MaterializeCommitInputs { .. }));
}
#[test]
fn test_commit_phase_uses_xsd_retry_prompt_when_pending() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generating {
attempt: 1,
max_attempts: 3,
},
commit_diff_prepared: true,
commit_diff_empty: false,
commit_diff_content_id_sha256: Some("id".to_string()),
commit_prompt_prepared: false,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
prompt_inputs: crate::reducer::state::PromptInputsState {
commit: Some(crate::reducer::state::MaterializedCommitInputs {
attempt: 1,
diff: crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: PipelineState::initial(5, 2)
.agent_chain
.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
)
.consumer_signature_sha256(),
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: Some(200_000),
inline_budget_bytes: Some(100_000),
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
}),
..Default::default()
},
continuation: crate::reducer::state::ContinuationState {
xsd_retry_pending: true,
..crate::reducer::state::ContinuationState::default()
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(
effect,
Effect::PrepareCommitPrompt {
prompt_mode: PromptMode::XsdRetry
}
),
"Expected XSD retry prompt when xsd_retry_pending=true, got {effect:?}"
);
}
#[test]
fn test_commit_phase_effects_uses_xsd_retry_prompt_mode_when_pending() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generating {
attempt: 1,
max_attempts: 3,
},
commit_diff_prepared: true,
commit_diff_empty: false,
commit_diff_content_id_sha256: Some("id".to_string()),
commit_prompt_prepared: false,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
prompt_inputs: crate::reducer::state::PromptInputsState {
commit: Some(crate::reducer::state::MaterializedCommitInputs {
attempt: 1,
diff: crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: PipelineState::initial(5, 2)
.agent_chain
.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
)
.consumer_signature_sha256(),
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: Some(200_000),
inline_budget_bytes: Some(100_000),
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
}),
..Default::default()
},
continuation: crate::reducer::state::ContinuationState {
xsd_retry_pending: true,
xsd_retry_count: 1,
max_xsd_retry_count: 10,
..crate::reducer::state::ContinuationState::default()
},
..create_test_state()
};
let effect = determine_next_effect_for_phase(&state);
assert!(
matches!(
effect,
Effect::PrepareCommitPrompt {
prompt_mode: PromptMode::XsdRetry
}
),
"Expected phase effect to use XSD retry prompt when xsd_retry_pending=true, got {effect:?}"
);
}
#[test]
fn test_determine_effect_commit_message_ignores_stale_validated_outcome() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generating {
attempt: 2,
max_attempts: 5,
},
commit_diff_prepared: true, commit_diff_content_id_sha256: Some("id".to_string()),
commit_prompt_prepared: false,
commit_agent_invoked: false,
commit_xml_extracted: false,
commit_validated_outcome: Some(crate::reducer::state::CommitValidatedOutcome {
attempt: 1, message: Some("stale message".to_string()),
reason: None,
}),
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(matches!(effect, Effect::MaterializeCommitInputs { .. }));
}
#[test]
fn test_determine_effect_commit_message_generated() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generated {
message: "test commit message".to_string(),
},
commit_xml_archived: true,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
..create_test_state()
};
let effect = determine_next_effect(&state);
match effect {
Effect::CreateCommit { message, .. } => {
assert_eq!(message, "test commit message");
}
_ => panic!("Expected CreateCommit effect, got {effect:?}"),
}
}
#[test]
fn test_determine_effect_commit_message_rematerializes_when_consumer_signature_changes() {
let mut state = PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generating {
attempt: 1,
max_attempts: 3,
},
commit_diff_prepared: true,
commit_diff_empty: false,
commit_diff_content_id_sha256: Some("id".to_string()),
commit_prompt_prepared: false,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string(), "fallback-agent".to_string()],
vec![vec!["model-a".to_string()], vec!["model-b".to_string()]],
AgentRole::Commit,
),
prompt_inputs: crate::reducer::state::PromptInputsState {
commit: Some(crate::reducer::state::MaterializedCommitInputs {
attempt: 1,
diff: crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "id".to_string(),
consumer_signature_sha256: "stale_sig".to_string(),
original_bytes: 1,
final_bytes: 1,
model_budget_bytes: Some(200_000),
inline_budget_bytes: Some(100_000),
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
}),
..Default::default()
},
..create_test_state()
};
let expected_sig = state.agent_chain.consumer_signature_sha256();
assert_ne!(
expected_sig, "stale_sig",
"test setup error: consumer signature should differ"
);
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::MaterializeCommitInputs { attempt: 1 }),
"Expected re-materialization when consumer signature changes, got {effect:?}"
);
state
.prompt_inputs
.commit
.as_mut()
.unwrap()
.diff
.consumer_signature_sha256 = expected_sig;
state.agent_chain.current_agent_index = 1;
let effect = determine_next_effect(&state);
assert!(
!matches!(effect, Effect::MaterializeCommitInputs { .. }),
"Expected no re-materialization when only current agent index changes, got {effect:?}"
);
}
#[test]
fn test_recovery_does_not_emit_success_before_create_commit() {
let state = PipelineState {
phase: PipelinePhase::CommitMessage,
previous_phase: Some(PipelinePhase::AwaitingDevFix),
failed_phase_for_recovery: Some(PipelinePhase::CommitMessage),
dev_fix_attempt_count: 2,
recovery_escalation_level: 1,
commit: CommitState::Generated {
message: "msg".to_string(),
},
commit_xml_archived: true,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CreateCommit { .. }),
"expected CreateCommit (do not clear recovery state yet), got: {effect:?}"
);
}
#[test]
fn test_recovery_emits_success_after_commit_created() {
let state = PipelineState {
phase: PipelinePhase::FinalValidation,
failed_phase_for_recovery: Some(PipelinePhase::CommitMessage),
dev_fix_attempt_count: 3,
recovery_escalation_level: 2,
commit: CommitState::Committed {
hash: "abc123".to_string(),
},
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::EmitRecoverySuccess { .. }),
"expected EmitRecoverySuccess after commit created, got: {effect:?}"
);
}
#[test]
fn test_committed_retry_pass_emits_matching_residual_check() {
let mut state = create_test_state();
state.phase = PipelinePhase::CommitMessage;
state.commit = CommitState::Committed {
hash: "abc123".to_string(),
};
state.agent_chain = AgentChainState::initial().with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
);
state.commit_residual_retry_pass = 3;
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CheckResidualFiles { pass: 3 }),
"Committed retry pass must emit CheckResidualFiles for the same retry pass"
);
}
#[test]
fn test_determine_effect_final_validation() {
let mut state = PipelineState {
phase: PipelinePhase::FinalValidation,
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::CheckUncommittedChangesBeforeTermination),
"FinalValidation should first check for uncommitted changes"
);
state.pre_termination_commit_checked = true;
let effect = determine_next_effect(&state);
assert!(
matches!(effect, Effect::ValidateFinalState),
"After safety check, FinalValidation should derive ValidateFinalState"
);
}
#[test]
fn test_check_commit_diff_emitted_on_each_commit_phase_entry() {
let mut state = PipelineState::initial(2, 0); state.agent_chain = state.agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
);
let mut check_diff_count = 0usize;
let max_steps = 200;
for step in 0..max_steps {
let effect = determine_next_effect(&state);
match effect {
Effect::CheckCommitDiff => {
check_diff_count += 1;
state = reduce(
state,
PipelineEvent::commit_diff_prepared(false, format!("hash-{check_diff_count}")),
);
}
Effect::LockPromptPermissions => {
state = reduce(state, PipelineEvent::prompt_permissions_locked(None));
}
Effect::RestorePromptPermissions => {
state = reduce(state, PipelineEvent::prompt_permissions_restored());
}
Effect::EnsureGitignoreEntries => {
state = reduce(
state,
PipelineEvent::gitignore_entries_ensured(
vec!["/PROMPT*".to_string(), ".agent/".to_string()],
vec![],
false,
),
);
}
Effect::CleanupContext => {
state = reduce(state, PipelineEvent::ContextCleaned);
}
Effect::CleanupContinuationContext => {
state = reduce(
state,
PipelineEvent::development_continuation_context_cleaned(),
);
}
Effect::InitializeAgentChain { drain, .. } => {
state = reduce(
state,
PipelineEvent::agent_chain_initialized(
drain,
vec![AgentName::from("commit-agent")],
vec![],
3,
1000,
2.0,
60000,
),
);
}
Effect::MaterializePlanningInputs { iteration } => {
let sig = state.agent_chain.consumer_signature_sha256();
state = reduce(
state,
PipelineEvent::planning_inputs_materialized(
iteration,
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Prompt,
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,
},
),
);
}
Effect::CleanupRequiredFiles { ref files }
if files.iter().any(|f| f.contains("plan.xml")) =>
{
let iteration = state.iteration;
state = reduce(state, PipelineEvent::planning_xml_cleaned(iteration));
}
Effect::PreparePlanningPrompt { iteration, .. } => {
state = reduce(state, PipelineEvent::planning_prompt_prepared(iteration));
}
Effect::InvokePlanningAgent { iteration } => {
state = reduce(state, PipelineEvent::planning_agent_invoked(iteration));
}
Effect::ExtractPlanningXml { iteration } => {
state = reduce(state, PipelineEvent::planning_xml_extracted(iteration));
}
Effect::ValidatePlanningXml { iteration } => {
state = reduce(
state,
PipelineEvent::planning_xml_validated(
iteration,
true,
Some("# Plan\n\n- step\n".to_string()),
),
);
}
Effect::WritePlanningMarkdown { iteration } => {
state = reduce(state, PipelineEvent::planning_markdown_written(iteration));
}
Effect::ArchivePlanningXml { iteration } => {
state = reduce(state, PipelineEvent::planning_xml_archived(iteration));
}
Effect::ApplyPlanningOutcome { iteration, valid } => {
state = reduce(
state,
PipelineEvent::plan_generation_completed(iteration, valid),
);
}
Effect::PrepareDevelopmentContext { iteration } => {
state = 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,
};
state = reduce(
state,
PipelineEvent::development_inputs_materialized(iteration, prompt, plan),
);
}
Effect::CleanupRequiredFiles { ref files }
if files.iter().any(|f| f.contains("development_result.xml")) =>
{
let iteration = state.iteration;
state = reduce(state, PipelineEvent::development_xml_cleaned(iteration));
}
Effect::PrepareDevelopmentPrompt { iteration, .. } => {
state = reduce(state, PipelineEvent::development_prompt_prepared(iteration));
}
Effect::InvokeDevelopmentAgent { iteration } => {
state = reduce(state, PipelineEvent::development_agent_invoked(iteration));
}
Effect::InvokeAnalysisAgent { iteration } => {
state = reduce(
state,
PipelineEvent::Development(
crate::reducer::event::DevelopmentEvent::AnalysisAgentInvoked { iteration },
),
);
}
Effect::ExtractDevelopmentXml { iteration } => {
state = reduce(state, PipelineEvent::development_xml_extracted(iteration));
}
Effect::ValidateDevelopmentXml { iteration } => {
state = reduce(
state,
PipelineEvent::development_xml_validated(
iteration,
crate::reducer::state::DevelopmentStatus::Completed,
"done".to_string(),
None,
None,
),
);
}
Effect::ArchiveDevelopmentXml { iteration } => {
state = reduce(state, PipelineEvent::development_xml_archived(iteration));
}
Effect::ApplyDevelopmentOutcome { iteration } => {
state = reduce(
state,
PipelineEvent::development_iteration_completed(iteration, true),
);
}
Effect::MaterializeCommitInputs { attempt } => {
let sig = state.agent_chain.consumer_signature_sha256();
state = reduce(
state,
PipelineEvent::commit_inputs_materialized(
attempt,
crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: format!("hash-{check_diff_count}"),
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,
},
),
);
}
Effect::PrepareCommitPrompt { .. } => {
state = reduce(state, PipelineEvent::commit_generation_started());
state = reduce(state, PipelineEvent::commit_prompt_prepared(1));
}
Effect::CleanupRequiredFiles { ref files }
if files.iter().any(|f| f.contains("commit_message.xml")) =>
{
state = reduce(state, PipelineEvent::commit_required_files_cleaned(1));
}
Effect::InvokeCommitAgent => {
state = reduce(state, PipelineEvent::commit_agent_invoked(1));
}
Effect::ExtractCommitXml => {
state = reduce(state, PipelineEvent::commit_xml_extracted(1));
}
Effect::ValidateCommitXml => {
state = reduce(
state,
PipelineEvent::commit_xml_validated(
"test commit".to_string(),
vec![],
vec![],
1,
),
);
}
Effect::ApplyCommitMessageOutcome => {
state = reduce(
state,
PipelineEvent::commit_message_generated("test commit".to_string(), 1),
);
}
Effect::ArchiveCommitXml => {
state = reduce(state, PipelineEvent::commit_xml_archived(1));
}
Effect::CreateCommit { .. } => {
state = reduce(
state,
PipelineEvent::commit_created(
format!("sha-{check_diff_count}"),
"test commit".to_string(),
),
);
}
Effect::CheckUncommittedChangesBeforeTermination => {
state = reduce(state, PipelineEvent::pre_termination_safety_check_passed());
}
Effect::ValidateFinalState => {
state = reduce(state, PipelineEvent::finalizing_started());
}
Effect::SaveCheckpoint { .. } => {
if state.phase == PipelinePhase::Complete {
break;
}
}
_ => panic!("Unexpected effect at step {step}: {effect:?}"),
}
if state.phase == PipelinePhase::Complete {
break;
}
}
assert_eq!(
state.phase,
PipelinePhase::Complete,
"Pipeline should complete"
);
assert_eq!(
check_diff_count, 2,
"CheckCommitDiff must be emitted exactly once per commit phase entry \
(2 dev iterations = 2 commit phases)"
);
}
#[test]
fn test_determine_effect_complete() {
let mut state = PipelineState {
phase: PipelinePhase::Complete,
..create_test_state()
};
let effect = determine_next_effect(&state);
assert!(matches!(
effect,
Effect::CheckUncommittedChangesBeforeTermination
));
state.pre_termination_commit_checked = true;
let effect = determine_next_effect(&state);
assert!(matches!(effect, Effect::SaveCheckpoint { .. }));
}
#[test]
fn test_commit_orchestrator_never_derives_continuation_mode() {
use crate::reducer::state::PromptMode;
let base_state = || PipelineState {
phase: PipelinePhase::CommitMessage,
commit: CommitState::Generating {
attempt: 1,
max_attempts: 3,
},
commit_diff_prepared: true,
commit_diff_empty: false,
commit_diff_content_id_sha256: Some("test_diff_id".to_string()),
commit_prompt_prepared: false,
agent_chain: PipelineState::initial(5, 2).agent_chain.with_agents(
vec!["commit-agent".to_string()],
vec![vec![]],
AgentRole::Commit,
),
prompt_inputs: crate::reducer::state::PromptInputsState {
commit: Some(crate::reducer::state::MaterializedCommitInputs {
attempt: 1,
diff: crate::reducer::state::MaterializedPromptInput {
kind: crate::reducer::state::PromptInputKind::Diff,
content_id_sha256: "test_diff_id".to_string(),
consumer_signature_sha256: "test_consumer".to_string(),
original_bytes: 100,
final_bytes: 100,
model_budget_bytes: Some(1000),
inline_budget_bytes: Some(500),
representation: crate::reducer::state::PromptInputRepresentation::Inline,
reason: crate::reducer::state::PromptMaterializationReason::WithinBudgets,
},
}),
..Default::default()
},
..create_test_state()
};
let state = base_state();
let effect = determine_next_effect(&state);
if let Effect::PrepareCommitPrompt { prompt_mode } = effect {
assert_eq!(
prompt_mode,
PromptMode::Normal,
"Normal path should derive Normal mode"
);
}
let mut state = base_state();
state.continuation.same_agent_retry_pending = true;
state.continuation.same_agent_retry_count = 0;
let effect = determine_next_effect(&state);
if let Effect::PrepareCommitPrompt { prompt_mode } = effect {
assert_eq!(
prompt_mode,
PromptMode::SameAgentRetry,
"Same-agent retry should derive SameAgentRetry mode"
);
}
let mut state = base_state();
state.continuation.xsd_retry_pending = true;
state.continuation.xsd_retry_count = 0;
let effect = determine_next_effect(&state);
if let Effect::PrepareCommitPrompt { prompt_mode } = effect {
assert_eq!(
prompt_mode,
PromptMode::XsdRetry,
"XSD retry should derive XsdRetry mode"
);
}
let mut state = base_state();
state.continuation.same_agent_retry_pending = true;
state.continuation.same_agent_retry_count = 0;
state.continuation.xsd_retry_pending = true;
state.continuation.xsd_retry_count = 0;
let effect = determine_next_effect(&state);
if let Effect::PrepareCommitPrompt { prompt_mode } = effect {
assert_ne!(
prompt_mode,
PromptMode::Continuation,
"Commit phase must never derive Continuation mode (even with both retries)"
);
}
let mut state = base_state();
state.continuation.same_agent_retry_pending = false;
state.continuation.xsd_retry_pending = false;
let effect = determine_next_effect(&state);
if let Effect::PrepareCommitPrompt { prompt_mode } = effect {
assert_eq!(
prompt_mode,
PromptMode::Normal,
"Clean continuation state should derive Normal mode, not Continuation"
);
}
}