use crate::files::llm_output_extraction::try_extract_xml_commit_document_with_trace;
use crate::prompts::prompt_scope_key::{PromptScopeKey, RetryMode};
use crate::reducer::effect::Effect;
use crate::reducer::event::{PipelineEvent, PipelinePhase};
use crate::reducer::state::{CommitState, PromptMode};
use crate::reducer::ui_event::{UIEvent, XmlOutputType};
use super::super::MockEffectHandler;
impl MockEffectHandler {
pub(super) fn handle_commit_effect(
&self,
effect: Effect,
) -> Option<(PipelineEvent, Vec<UIEvent>)> {
match effect {
Effect::RunRebase {
phase,
target_branch: _,
} => Some((
PipelineEvent::rebase_succeeded(phase, "mock_head_abc123".to_string()),
vec![],
)),
Effect::ResolveRebaseConflicts { strategy: _ } => {
Some((PipelineEvent::rebase_conflict_resolved(vec![]), vec![]))
}
Effect::PrepareCommitPrompt { prompt_mode } => {
let attempt = match self.state.commit {
CommitState::Generating { attempt, .. } => attempt,
_ => 1,
};
let retry_mode = match prompt_mode {
PromptMode::XsdRetry => RetryMode::Xsd {
count: self.state.continuation.xsd_retry_count,
},
_ => RetryMode::Normal,
};
let scope_key = PromptScopeKey::for_commit(
self.state.iteration,
attempt,
retry_mode,
self.state.recovery_epoch,
);
let key = scope_key.to_string();
let was_replayed = self
.replay_prompt_keys
.as_ref()
.is_some_and(|keys| keys.contains(&key));
let ui = vec![
UIEvent::PhaseTransition {
from: Some(self.state.phase),
to: PipelinePhase::CommitMessage,
},
UIEvent::PromptReplayHit { key, was_replayed },
];
Some((PipelineEvent::commit_prompt_prepared(attempt), ui))
}
Effect::InvokeCommitAgent => {
let attempt = match self.state.commit {
CommitState::Generating { attempt, .. } => attempt,
_ => 1,
};
Some((PipelineEvent::commit_agent_invoked(attempt), vec![]))
}
Effect::ExtractCommitXml => {
let attempt = match self.state.commit {
CommitState::Generating { attempt, .. } => attempt,
_ => 1,
};
Some((PipelineEvent::commit_xml_extracted(attempt), vec![]))
}
Effect::ValidateCommitXml => {
let attempt = match self.state.commit {
CommitState::Generating { attempt, .. } => attempt,
_ => 1,
};
let xml = self.simulate_commit_message_xml.clone().unwrap_or_else(|| {
r"<ralph-commit>
<ralph-subject>feat: mock commit message for testing</ralph-subject>
<ralph-body>This is a mock commit body generated for testing purposes.
- Changed some files
- Added new features</ralph-body>
</ralph-commit>"
.to_string()
});
let (message, skip_reason, files, excluded_files, detail) =
try_extract_xml_commit_document_with_trace(&xml);
let event = skip_reason.map_or_else(
|| {
message.map_or_else(
|| PipelineEvent::commit_xml_validation_failed(detail, attempt),
|message| {
PipelineEvent::commit_xml_validated(
message,
files,
excluded_files,
attempt,
)
},
)
},
PipelineEvent::commit_skipped,
);
let ui = vec![UIEvent::XmlOutput {
xml_type: XmlOutputType::CommitMessage,
content: xml,
context: None,
}];
Some((event, ui))
}
Effect::ApplyCommitMessageOutcome => {
let event = self.state.commit_validated_outcome.as_ref().map_or_else(
|| {
PipelineEvent::commit_generation_failed(
"Mock commit outcome missing".to_string(),
)
},
|outcome| {
outcome.message.as_ref().map_or_else(
|| {
outcome.reason.as_ref().map_or_else(
|| {
PipelineEvent::commit_generation_failed(
"Mock commit outcome missing message and reason"
.to_string(),
)
},
|reason| {
PipelineEvent::commit_message_validation_failed(
reason.clone(),
outcome.attempt,
)
},
)
},
|message| {
PipelineEvent::commit_message_generated(
message.clone(),
outcome.attempt,
)
},
)
},
);
Some((event, vec![]))
}
Effect::ArchiveCommitXml => {
let attempt = match self.state.commit {
CommitState::Generating { attempt, .. } => attempt,
_ => 1,
};
Some((PipelineEvent::commit_xml_archived(attempt), vec![]))
}
Effect::CreateCommit {
message,
files: _,
excluded_files: _,
} => Some((
PipelineEvent::commit_created("mock_commit_hash_abc123".to_string(), message),
vec![],
)),
Effect::SkipCommit { reason } => Some((PipelineEvent::commit_skipped(reason), vec![])),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reducer::event::CommitEvent;
use crate::reducer::state::PipelineState;
#[test]
fn test_effect_mapping_does_not_handle_check_commit_diff_to_avoid_inconsistent_content_id() {
let handler = MockEffectHandler::new(crate::reducer::state::PipelineState::initial(1, 0));
let mapped = handler.handle_commit_effect(Effect::CheckCommitDiff);
assert!(
mapped.is_none(),
"CheckCommitDiff should be handled by the workspace-backed execute path"
);
}
#[test]
fn test_validate_commit_xml_propagates_excluded_files_metadata() {
let state = crate::reducer::state::PipelineState::initial(1, 0);
let handler = MockEffectHandler::new(state).with_commit_message_xml(
r#"<ralph-commit>
<ralph-subject>feat: mock</ralph-subject>
<ralph-excluded-files>
<ralph-excluded-file reason="deferred">src/leftover.rs</ralph-excluded-file>
</ralph-excluded-files>
</ralph-commit>"#,
);
let (event, _ui) = handler
.handle_commit_effect(Effect::ValidateCommitXml)
.expect("ValidateCommitXml should be handled");
match event {
PipelineEvent::Commit(CommitEvent::CommitXmlValidated { excluded_files, .. }) => {
assert_eq!(excluded_files.len(), 1);
assert_eq!(excluded_files[0].path, "src/leftover.rs");
}
other => panic!("expected CommitXmlValidated event, got {other:?}"),
}
}
#[test]
fn test_prepare_commit_prompt_xsd_retry_uses_state_xsd_retry_count_in_prompt_key() {
let state = {
let s = crate::reducer::state::PipelineState::initial(1, 0);
let continuation = s
.continuation
.trigger_xsd_retry()
.trigger_xsd_retry()
.trigger_xsd_retry();
PipelineState { continuation, ..s }
};
let handler = MockEffectHandler::new(state);
let (_event, ui) = handler
.handle_commit_effect(Effect::PrepareCommitPrompt {
prompt_mode: PromptMode::XsdRetry,
})
.expect("PrepareCommitPrompt should be handled");
let prompt_key = ui
.iter()
.find_map(|e| match e {
UIEvent::PromptReplayHit { key, .. } => Some(key.clone()),
_ => None,
})
.expect("Expected PromptReplayHit UI event");
let expected_key = PromptScopeKey::for_commit(
handler.state.iteration,
1,
RetryMode::Xsd { count: 3 },
handler.state.recovery_epoch,
)
.to_string();
assert_eq!(prompt_key, expected_key);
}
}