Skip to main content

ralph_workflow/cli/reducer/
state_reduction.rs

1//! Pure reducer for CLI argument processing.
2//!
3//! This module contains the pure `reduce` function that transforms CLI state
4//! based on events. It follows the same pattern as the pipeline reducer.
5
6use super::event::CliEvent;
7use super::state::{CliState, PresetType};
8
9/// Pure reducer function for CLI argument processing.
10///
11/// This function takes the current state and an event, returning a new state.
12/// It is a pure function with no side effects, making it easy to test.
13///
14/// # Arguments
15///
16/// * `state` - The current CLI state
17/// * `event` - The event to process
18///
19/// # Returns
20///
21/// A new CLI state with the event applied.
22///
23/// # Event Processing Order
24///
25/// Events are processed in the order they are received. For conflicting options
26/// (like multiple presets), the last one wins. This allows users to combine
27/// flags where the later flag takes precedence.
28#[must_use]
29pub fn reduce(mut state: CliState, event: CliEvent) -> CliState {
30    match event {
31        // ===== Verbosity Events =====
32        CliEvent::VerbositySet { level } => state.verbosity = Some(level),
33        CliEvent::QuietModeEnabled => state.quiet_mode = true,
34        CliEvent::FullModeEnabled => state.full_mode = true,
35        CliEvent::DebugModeEnabled => state.debug_mode = true,
36
37        // ===== Preset Events (last wins) =====
38        CliEvent::QuickPresetApplied => state.preset_applied = Some(PresetType::Quick),
39        CliEvent::RapidPresetApplied => state.preset_applied = Some(PresetType::Rapid),
40        CliEvent::LongPresetApplied => state.preset_applied = Some(PresetType::Long),
41        CliEvent::StandardPresetApplied => state.preset_applied = Some(PresetType::Standard),
42        CliEvent::ThoroughPresetApplied => state.preset_applied = Some(PresetType::Thorough),
43
44        // ===== Iteration Count Events =====
45        CliEvent::DeveloperItersSet { value } => state.developer_iters = Some(value),
46        CliEvent::ReviewerReviewsSet { value } => state.reviewer_reviews = Some(value),
47
48        // ===== Agent Selection Events =====
49        CliEvent::DeveloperAgentSet { agent } => state.developer_agent = Some(agent),
50        CliEvent::ReviewerAgentSet { agent } => state.reviewer_agent = Some(agent),
51        CliEvent::DeveloperModelSet { model } => state.developer_model = Some(model),
52        CliEvent::ReviewerModelSet { model } => state.reviewer_model = Some(model),
53        CliEvent::DeveloperProviderSet { provider } => state.developer_provider = Some(provider),
54        CliEvent::ReviewerProviderSet { provider } => state.reviewer_provider = Some(provider),
55        CliEvent::ReviewerJsonParserSet { parser } => state.reviewer_json_parser = Some(parser),
56
57        // ===== Configuration Events =====
58        CliEvent::IsolationModeDisabled => state.isolation_mode = Some(false),
59        CliEvent::ReviewDepthSet { depth } => state.review_depth = Some(depth),
60        CliEvent::GitUserNameSet { name } => state.git_user_name = Some(name),
61        CliEvent::GitUserEmailSet { email } => state.git_user_email = Some(email),
62        CliEvent::StreamingMetricsEnabled => state.streaming_metrics = true,
63
64        // ===== Agent Preset Events =====
65        CliEvent::AgentPresetSet { preset } => state.agent_preset = Some(preset),
66
67        // ===== Finalization =====
68        CliEvent::CliProcessingComplete => state.complete = true,
69    }
70
71    state
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_reduce_verbosity_set() {
80        let state = CliState::initial();
81        let new_state = reduce(state, CliEvent::VerbositySet { level: 3 });
82        assert_eq!(new_state.verbosity, Some(3));
83    }
84
85    #[test]
86    fn test_reduce_quiet_mode() {
87        let state = CliState::initial();
88        let new_state = reduce(state, CliEvent::QuietModeEnabled);
89        assert!(new_state.quiet_mode);
90    }
91
92    #[test]
93    fn test_reduce_preset_quick() {
94        let state = CliState::initial();
95        let new_state = reduce(state, CliEvent::QuickPresetApplied);
96        assert_eq!(new_state.preset_applied, Some(PresetType::Quick));
97        assert_eq!(new_state.resolved_developer_iters(99), 1);
98        assert_eq!(new_state.resolved_reviewer_reviews(99), 1);
99    }
100
101    #[test]
102    fn test_reduce_preset_long() {
103        let state = CliState::initial();
104        let new_state = reduce(state, CliEvent::LongPresetApplied);
105        assert_eq!(new_state.preset_applied, Some(PresetType::Long));
106        assert_eq!(new_state.resolved_developer_iters(99), 15);
107        assert_eq!(new_state.resolved_reviewer_reviews(99), 10);
108    }
109
110    #[test]
111    fn test_reduce_preset_standard() {
112        let state = CliState::initial();
113        let new_state = reduce(state, CliEvent::StandardPresetApplied);
114        assert_eq!(new_state.preset_applied, Some(PresetType::Standard));
115        assert_eq!(new_state.resolved_developer_iters(99), 5);
116        assert_eq!(new_state.resolved_reviewer_reviews(99), 2);
117    }
118
119    #[test]
120    fn test_reduce_preset_thorough() {
121        let state = CliState::initial();
122        let new_state = reduce(state, CliEvent::ThoroughPresetApplied);
123        assert_eq!(new_state.preset_applied, Some(PresetType::Thorough));
124        assert_eq!(new_state.resolved_developer_iters(99), 10);
125        assert_eq!(new_state.resolved_reviewer_reviews(99), 5);
126    }
127
128    #[test]
129    fn test_reduce_preset_last_wins() {
130        let state = CliState::initial();
131        // Apply quick first, then long
132        let state = reduce(state, CliEvent::QuickPresetApplied);
133        let state = reduce(state, CliEvent::LongPresetApplied);
134
135        // Long should win (last applied)
136        assert_eq!(state.preset_applied, Some(PresetType::Long));
137        assert_eq!(state.resolved_developer_iters(99), 15);
138    }
139
140    #[test]
141    fn test_reduce_explicit_iters_override_preset() {
142        let state = CliState::initial();
143        // Apply preset first
144        let state = reduce(state, CliEvent::QuickPresetApplied);
145        // Then explicit override
146        let state = reduce(state, CliEvent::DeveloperItersSet { value: 7 });
147        let state = reduce(state, CliEvent::ReviewerReviewsSet { value: 3 });
148
149        // Explicit values should be used
150        assert_eq!(state.resolved_developer_iters(99), 7);
151        assert_eq!(state.resolved_reviewer_reviews(99), 3);
152    }
153
154    #[test]
155    fn test_reduce_developer_agent() {
156        let state = CliState::initial();
157        let new_state = reduce(
158            state,
159            CliEvent::DeveloperAgentSet {
160                agent: "claude".to_string(),
161            },
162        );
163        assert_eq!(new_state.developer_agent, Some("claude".to_string()));
164    }
165
166    #[test]
167    fn test_reduce_isolation_mode_disabled() {
168        let state = CliState::initial();
169        let new_state = reduce(state, CliEvent::IsolationModeDisabled);
170        assert_eq!(new_state.isolation_mode, Some(false));
171    }
172
173    #[test]
174    fn test_reduce_streaming_metrics() {
175        let state = CliState::initial();
176        let new_state = reduce(state, CliEvent::StreamingMetricsEnabled);
177        assert!(new_state.streaming_metrics);
178    }
179
180    #[test]
181    fn test_reduce_complete() {
182        let state = CliState::initial();
183        let new_state = reduce(state, CliEvent::CliProcessingComplete);
184        assert!(new_state.complete);
185    }
186
187    #[test]
188    fn test_reduce_preserves_unrelated_fields() {
189        let mut state = CliState::initial();
190        state.developer_agent = Some("existing".to_string());
191
192        // Applying unrelated event should preserve developer_agent
193        let new_state = reduce(state, CliEvent::QuietModeEnabled);
194
195        assert!(new_state.quiet_mode);
196        assert_eq!(new_state.developer_agent, Some("existing".to_string()));
197    }
198
199    #[test]
200    fn test_full_event_sequence() {
201        let events = vec![
202            CliEvent::ThoroughPresetApplied,
203            CliEvent::DeveloperAgentSet {
204                agent: "opencode".to_string(),
205            },
206            CliEvent::ReviewerAgentSet {
207                agent: "claude".to_string(),
208            },
209            CliEvent::DebugModeEnabled,
210            CliEvent::StreamingMetricsEnabled,
211            CliEvent::CliProcessingComplete,
212        ];
213
214        let mut state = CliState::initial();
215        for event in events {
216            state = reduce(state, event);
217        }
218
219        assert!(state.complete);
220        assert!(state.debug_mode);
221        assert!(state.streaming_metrics);
222        assert_eq!(state.preset_applied, Some(PresetType::Thorough));
223        assert_eq!(state.developer_agent, Some("opencode".to_string()));
224        assert_eq!(state.reviewer_agent, Some("claude".to_string()));
225        assert_eq!(state.resolved_developer_iters(5), 10);
226        assert_eq!(state.resolved_reviewer_reviews(2), 5);
227    }
228}