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(state: CliState, event: CliEvent) -> CliState {
30    match event {
31        // ===== Verbosity Events =====
32        CliEvent::VerbositySet { level } => CliState {
33            verbosity: Some(level),
34            ..state
35        },
36        CliEvent::QuietModeEnabled => CliState {
37            quiet_mode: true,
38            ..state
39        },
40        CliEvent::FullModeEnabled => CliState {
41            full_mode: true,
42            ..state
43        },
44        CliEvent::DebugModeEnabled => CliState {
45            debug_mode: true,
46            ..state
47        },
48
49        // ===== Preset Events (last wins) =====
50        CliEvent::QuickPresetApplied => CliState {
51            preset_applied: Some(PresetType::Quick),
52            ..state
53        },
54        CliEvent::RapidPresetApplied => CliState {
55            preset_applied: Some(PresetType::Rapid),
56            ..state
57        },
58        CliEvent::LongPresetApplied => CliState {
59            preset_applied: Some(PresetType::Long),
60            ..state
61        },
62        CliEvent::StandardPresetApplied => CliState {
63            preset_applied: Some(PresetType::Standard),
64            ..state
65        },
66        CliEvent::ThoroughPresetApplied => CliState {
67            preset_applied: Some(PresetType::Thorough),
68            ..state
69        },
70
71        // ===== Iteration Count Events =====
72        CliEvent::DeveloperItersSet { value } => CliState {
73            developer_iters: Some(value),
74            ..state
75        },
76        CliEvent::ReviewerReviewsSet { value } => CliState {
77            reviewer_reviews: Some(value),
78            ..state
79        },
80
81        // ===== Agent Selection Events =====
82        CliEvent::DeveloperAgentSet { agent } => CliState {
83            developer_agent: Some(agent),
84            ..state
85        },
86        CliEvent::ReviewerAgentSet { agent } => CliState {
87            reviewer_agent: Some(agent),
88            ..state
89        },
90        CliEvent::DeveloperModelSet { model } => CliState {
91            developer_model: Some(model),
92            ..state
93        },
94        CliEvent::ReviewerModelSet { model } => CliState {
95            reviewer_model: Some(model),
96            ..state
97        },
98        CliEvent::DeveloperProviderSet { provider } => CliState {
99            developer_provider: Some(provider),
100            ..state
101        },
102        CliEvent::ReviewerProviderSet { provider } => CliState {
103            reviewer_provider: Some(provider),
104            ..state
105        },
106        CliEvent::ReviewerJsonParserSet { parser } => CliState {
107            reviewer_json_parser: Some(parser),
108            ..state
109        },
110
111        // ===== Configuration Events =====
112        CliEvent::IsolationModeDisabled => CliState {
113            isolation_mode: Some(false),
114            ..state
115        },
116        CliEvent::ReviewDepthSet { depth } => CliState {
117            review_depth: Some(depth),
118            ..state
119        },
120        CliEvent::GitUserNameSet { name } => CliState {
121            git_user_name: Some(name),
122            ..state
123        },
124        CliEvent::GitUserEmailSet { email } => CliState {
125            git_user_email: Some(email),
126            ..state
127        },
128        CliEvent::StreamingMetricsEnabled => CliState {
129            streaming_metrics: true,
130            ..state
131        },
132
133        // ===== Agent Preset Events =====
134        CliEvent::AgentPresetSet { preset } => CliState {
135            agent_preset: Some(preset),
136            ..state
137        },
138
139        // ===== Finalization =====
140        CliEvent::CliProcessingComplete => CliState {
141            complete: true,
142            ..state
143        },
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_reduce_verbosity_set() {
153        let state = CliState::initial();
154        let new_state = reduce(state, CliEvent::VerbositySet { level: 3 });
155        assert_eq!(new_state.verbosity, Some(3));
156    }
157
158    #[test]
159    fn test_reduce_quiet_mode() {
160        let state = CliState::initial();
161        let new_state = reduce(state, CliEvent::QuietModeEnabled);
162        assert!(new_state.quiet_mode);
163    }
164
165    #[test]
166    fn test_reduce_preset_quick() {
167        let state = CliState::initial();
168        let new_state = reduce(state, CliEvent::QuickPresetApplied);
169        assert_eq!(new_state.preset_applied, Some(PresetType::Quick));
170        assert_eq!(new_state.resolved_developer_iters(99), 1);
171        assert_eq!(new_state.resolved_reviewer_reviews(99), 1);
172    }
173
174    #[test]
175    fn test_reduce_preset_long() {
176        let state = CliState::initial();
177        let new_state = reduce(state, CliEvent::LongPresetApplied);
178        assert_eq!(new_state.preset_applied, Some(PresetType::Long));
179        assert_eq!(new_state.resolved_developer_iters(99), 15);
180        assert_eq!(new_state.resolved_reviewer_reviews(99), 10);
181    }
182
183    #[test]
184    fn test_reduce_preset_standard() {
185        let state = CliState::initial();
186        let new_state = reduce(state, CliEvent::StandardPresetApplied);
187        assert_eq!(new_state.preset_applied, Some(PresetType::Standard));
188        assert_eq!(new_state.resolved_developer_iters(99), 5);
189        assert_eq!(new_state.resolved_reviewer_reviews(99), 2);
190    }
191
192    #[test]
193    fn test_reduce_preset_thorough() {
194        let state = CliState::initial();
195        let new_state = reduce(state, CliEvent::ThoroughPresetApplied);
196        assert_eq!(new_state.preset_applied, Some(PresetType::Thorough));
197        assert_eq!(new_state.resolved_developer_iters(99), 10);
198        assert_eq!(new_state.resolved_reviewer_reviews(99), 5);
199    }
200
201    #[test]
202    fn test_reduce_preset_last_wins() {
203        let state = CliState::initial();
204        // Apply quick first, then long
205        let state = reduce(state, CliEvent::QuickPresetApplied);
206        let state = reduce(state, CliEvent::LongPresetApplied);
207
208        // Long should win (last applied)
209        assert_eq!(state.preset_applied, Some(PresetType::Long));
210        assert_eq!(state.resolved_developer_iters(99), 15);
211    }
212
213    #[test]
214    fn test_reduce_explicit_iters_override_preset() {
215        let state = CliState::initial();
216        // Apply preset first
217        let state = reduce(state, CliEvent::QuickPresetApplied);
218        // Then explicit override
219        let state = reduce(state, CliEvent::DeveloperItersSet { value: 7 });
220        let state = reduce(state, CliEvent::ReviewerReviewsSet { value: 3 });
221
222        // Explicit values should be used
223        assert_eq!(state.resolved_developer_iters(99), 7);
224        assert_eq!(state.resolved_reviewer_reviews(99), 3);
225    }
226
227    #[test]
228    fn test_reduce_developer_agent() {
229        let state = CliState::initial();
230        let new_state = reduce(
231            state,
232            CliEvent::DeveloperAgentSet {
233                agent: "claude".to_string(),
234            },
235        );
236        assert_eq!(new_state.developer_agent, Some("claude".to_string()));
237    }
238
239    #[test]
240    fn test_reduce_isolation_mode_disabled() {
241        let state = CliState::initial();
242        let new_state = reduce(state, CliEvent::IsolationModeDisabled);
243        assert_eq!(new_state.isolation_mode, Some(false));
244    }
245
246    #[test]
247    fn test_reduce_streaming_metrics() {
248        let state = CliState::initial();
249        let new_state = reduce(state, CliEvent::StreamingMetricsEnabled);
250        assert!(new_state.streaming_metrics);
251    }
252
253    #[test]
254    fn test_reduce_complete() {
255        let state = CliState::initial();
256        let new_state = reduce(state, CliEvent::CliProcessingComplete);
257        assert!(new_state.complete);
258    }
259
260    #[test]
261    fn test_reduce_preserves_unrelated_fields() {
262        let mut state = CliState::initial();
263        state.developer_agent = Some("existing".to_string());
264
265        // Applying unrelated event should preserve developer_agent
266        let new_state = reduce(state, CliEvent::QuietModeEnabled);
267
268        assert!(new_state.quiet_mode);
269        assert_eq!(new_state.developer_agent, Some("existing".to_string()));
270    }
271
272    #[test]
273    fn test_full_event_sequence() {
274        let events = vec![
275            CliEvent::ThoroughPresetApplied,
276            CliEvent::DeveloperAgentSet {
277                agent: "opencode".to_string(),
278            },
279            CliEvent::ReviewerAgentSet {
280                agent: "claude".to_string(),
281            },
282            CliEvent::DebugModeEnabled,
283            CliEvent::StreamingMetricsEnabled,
284            CliEvent::CliProcessingComplete,
285        ];
286
287        let mut state = CliState::initial();
288        for event in events {
289            state = reduce(state, event);
290        }
291
292        assert!(state.complete);
293        assert!(state.debug_mode);
294        assert!(state.streaming_metrics);
295        assert_eq!(state.preset_applied, Some(PresetType::Thorough));
296        assert_eq!(state.developer_agent, Some("opencode".to_string()));
297        assert_eq!(state.reviewer_agent, Some("claude".to_string()));
298        assert_eq!(state.resolved_developer_iters(5), 10);
299        assert_eq!(state.resolved_reviewer_reviews(2), 5);
300    }
301}