Skip to main content

ralph_workflow/cli/reducer/
parser.rs

1//! Parse Args into CLI events.
2//!
3//! This module converts the clap-parsed Args struct into a sequence
4//! of `CliEvents` that can be processed by the reducer.
5
6use super::event::CliEvent;
7
8/// Convert CLI arguments into a sequence of events.
9///
10/// This function maps each relevant field in the Args struct to a
11/// corresponding `CliEvent`. Events are generated in a deterministic order,
12/// with later events taking precedence over earlier ones (last-wins semantics).
13///
14/// # Event Ordering
15///
16/// Events are generated in this order:
17/// 1. Verbosity flags (--quiet, --full, --debug, -v)
18/// 2. Preset flags (--quick, --rapid, --long, --standard, --thorough)
19/// 3. Explicit iteration counts (-D, -R)
20/// 4. Agent selection (-a, -r, model flags)
21/// 5. Configuration flags (--no-isolation, --review-depth, etc.)
22/// 6. Finalization event
23///
24/// This ordering ensures that:
25/// - Explicit overrides (like -D) come after presets and override them
26/// - Last-specified preset wins if multiple are given
27///
28/// # Arguments
29///
30/// * `args` - The parsed CLI arguments from clap
31///
32/// # Returns
33///
34/// A vector of `CliEvents` representing all specified CLI arguments.
35#[must_use]
36pub fn args_to_events(args: &super::super::Args) -> Vec<CliEvent> {
37    let mut events = Vec::new();
38
39    // ===== Verbosity Events =====
40    if args.verbosity_shorthand.quiet {
41        events.push(CliEvent::QuietModeEnabled);
42    }
43    if args.verbosity_shorthand.full {
44        events.push(CliEvent::FullModeEnabled);
45    }
46    if args.debug_verbosity.debug {
47        events.push(CliEvent::DebugModeEnabled);
48    }
49    if let Some(level) = args.verbosity {
50        events.push(CliEvent::VerbositySet { level });
51    }
52
53    // ===== Preset Events =====
54    // Order matters: later presets override earlier ones
55    if args.quick_presets.quick {
56        events.push(CliEvent::QuickPresetApplied);
57    }
58    if args.quick_presets.rapid {
59        events.push(CliEvent::RapidPresetApplied);
60    }
61    // THE FIX: These three preset flags were missing!
62    if args.quick_presets.long {
63        events.push(CliEvent::LongPresetApplied);
64    }
65    if args.standard_presets.standard {
66        events.push(CliEvent::StandardPresetApplied);
67    }
68    if args.standard_presets.thorough {
69        events.push(CliEvent::ThoroughPresetApplied);
70    }
71
72    // ===== Iteration Count Events =====
73    // Explicit iteration counts come after presets so they override preset defaults
74    if let Some(iters) = args.developer_iters {
75        events.push(CliEvent::DeveloperItersSet { value: iters });
76    }
77    if let Some(reviews) = args.reviewer_reviews {
78        events.push(CliEvent::ReviewerReviewsSet { value: reviews });
79    }
80
81    // ===== Agent Selection Events =====
82    if let Some(ref agent) = args.developer_agent {
83        events.push(CliEvent::DeveloperAgentSet {
84            agent: agent.clone(),
85        });
86    }
87    if let Some(ref agent) = args.reviewer_agent {
88        events.push(CliEvent::ReviewerAgentSet {
89            agent: agent.clone(),
90        });
91    }
92    if let Some(ref model) = args.developer_model {
93        events.push(CliEvent::DeveloperModelSet {
94            model: model.clone(),
95        });
96    }
97    if let Some(ref model) = args.reviewer_model {
98        events.push(CliEvent::ReviewerModelSet {
99            model: model.clone(),
100        });
101    }
102    if let Some(ref provider) = args.developer_provider {
103        events.push(CliEvent::DeveloperProviderSet {
104            provider: provider.clone(),
105        });
106    }
107    if let Some(ref provider) = args.reviewer_provider {
108        events.push(CliEvent::ReviewerProviderSet {
109            provider: provider.clone(),
110        });
111    }
112    if let Some(ref parser) = args.reviewer_json_parser {
113        events.push(CliEvent::ReviewerJsonParserSet {
114            parser: parser.clone(),
115        });
116    }
117
118    // ===== Agent Preset Events =====
119    if let Some(ref preset) = args.preset {
120        events.push(CliEvent::AgentPresetSet {
121            preset: format!("{preset:?}"),
122        });
123    }
124
125    // ===== Configuration Events =====
126    if args.no_isolation {
127        events.push(CliEvent::IsolationModeDisabled);
128    }
129    if let Some(ref depth) = args.review_depth {
130        events.push(CliEvent::ReviewDepthSet {
131            depth: depth.clone(),
132        });
133    }
134    if let Some(ref name) = args.git_user_name {
135        events.push(CliEvent::GitUserNameSet {
136            name: name.trim().to_string(),
137        });
138    }
139    if let Some(ref email) = args.git_user_email {
140        events.push(CliEvent::GitUserEmailSet {
141            email: email.trim().to_string(),
142        });
143    }
144    if args.show_streaming_metrics {
145        events.push(CliEvent::StreamingMetricsEnabled);
146    }
147
148    // ===== Finalization =====
149    events.push(CliEvent::CliProcessingComplete);
150
151    events
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::cli::Args;
158    use clap::Parser;
159
160    #[test]
161    fn test_args_to_events_empty() {
162        let args = Args::parse_from(["ralph"]);
163        let events = args_to_events(&args);
164
165        // Should have at least the completion event
166        assert!(
167            events.contains(&CliEvent::CliProcessingComplete),
168            "Should always have completion event"
169        );
170
171        // Should not have any other events
172        assert!(
173            !events.iter().any(|e| e != &CliEvent::CliProcessingComplete),
174            "Should have no other events for empty args"
175        );
176    }
177
178    #[test]
179    fn test_args_to_events_quick_preset() {
180        let args = Args::parse_from(["ralph", "-Q"]);
181        let events = args_to_events(&args);
182
183        assert!(
184            events.contains(&CliEvent::QuickPresetApplied),
185            "Should have quick preset event"
186        );
187        assert!(events.contains(&CliEvent::CliProcessingComplete));
188    }
189
190    #[test]
191    fn test_args_to_events_rapid_preset() {
192        let args = Args::parse_from(["ralph", "-U"]);
193        let events = args_to_events(&args);
194
195        assert!(
196            events.contains(&CliEvent::RapidPresetApplied),
197            "Should have rapid preset event"
198        );
199        assert!(events.contains(&CliEvent::CliProcessingComplete));
200    }
201
202    #[test]
203    fn test_args_to_events_long_preset() {
204        let args = Args::parse_from(["ralph", "-L"]);
205        let events = args_to_events(&args);
206
207        assert!(
208            events.contains(&CliEvent::LongPresetApplied),
209            "Should have long preset event"
210        );
211        assert!(events.contains(&CliEvent::CliProcessingComplete));
212    }
213
214    #[test]
215    fn test_args_to_events_standard_preset() {
216        let args = Args::parse_from(["ralph", "-S"]);
217        let events = args_to_events(&args);
218
219        assert!(
220            events.contains(&CliEvent::StandardPresetApplied),
221            "Should have standard preset event"
222        );
223        assert!(events.contains(&CliEvent::CliProcessingComplete));
224    }
225
226    #[test]
227    fn test_args_to_events_thorough_preset() {
228        let args = Args::parse_from(["ralph", "-T"]);
229        let events = args_to_events(&args);
230
231        assert!(
232            events.contains(&CliEvent::ThoroughPresetApplied),
233            "Should have thorough preset event"
234        );
235        assert!(events.contains(&CliEvent::CliProcessingComplete));
236    }
237
238    #[test]
239    fn test_args_to_events_explicit_iters() {
240        let args = Args::parse_from(["ralph", "-D", "7", "-R", "3"]);
241        let events = args_to_events(&args);
242
243        assert!(
244            events.contains(&CliEvent::DeveloperItersSet { value: 7 }),
245            "Should have developer iters event"
246        );
247        assert!(
248            events.contains(&CliEvent::ReviewerReviewsSet { value: 3 }),
249            "Should have reviewer reviews event"
250        );
251    }
252
253    #[test]
254    fn test_args_to_events_preset_plus_explicit_override() {
255        let args = Args::parse_from(["ralph", "-Q", "-D", "10", "-R", "5"]);
256        let events = args_to_events(&args);
257
258        // Should have both preset and explicit values
259        assert!(events.contains(&CliEvent::QuickPresetApplied));
260        assert!(events.contains(&CliEvent::DeveloperItersSet { value: 10 }));
261        assert!(events.contains(&CliEvent::ReviewerReviewsSet { value: 5 }));
262
263        // Verify order: preset comes before explicit override
264        let preset_idx = events
265            .iter()
266            .position(|e| e == &CliEvent::QuickPresetApplied)
267            .expect("Should have quick preset");
268        let iters_idx = events
269            .iter()
270            .position(|e| e == &CliEvent::DeveloperItersSet { value: 10 })
271            .expect("Should have developer iters");
272
273        assert!(
274            preset_idx < iters_idx,
275            "Preset should come before explicit override"
276        );
277    }
278
279    #[test]
280    fn test_args_to_events_agent_selection() {
281        let args = Args::parse_from(["ralph", "-a", "claude", "-r", "gpt"]);
282        let events = args_to_events(&args);
283
284        assert!(
285            events.contains(&CliEvent::DeveloperAgentSet {
286                agent: "claude".to_string()
287            }),
288            "Should have developer agent event"
289        );
290        assert!(
291            events.contains(&CliEvent::ReviewerAgentSet {
292                agent: "gpt".to_string()
293            }),
294            "Should have reviewer agent event"
295        );
296    }
297
298    #[test]
299    fn test_args_to_events_verbose_mode() {
300        let args = Args::parse_from(["ralph", "-v", "3"]);
301        let events = args_to_events(&args);
302
303        assert!(
304            events.contains(&CliEvent::VerbositySet { level: 3 }),
305            "Should have verbosity set event"
306        );
307    }
308
309    #[test]
310    fn test_args_to_events_debug_mode() {
311        let args = Args::parse_from(["ralph", "--debug"]);
312        let events = args_to_events(&args);
313
314        assert!(
315            events.contains(&CliEvent::DebugModeEnabled),
316            "Should have debug mode event"
317        );
318    }
319
320    #[test]
321    fn test_args_to_events_no_isolation() {
322        let args = Args::parse_from(["ralph", "--no-isolation"]);
323        let events = args_to_events(&args);
324
325        assert!(
326            events.contains(&CliEvent::IsolationModeDisabled),
327            "Should have isolation mode disabled event"
328        );
329    }
330
331    #[test]
332    fn test_args_to_events_git_identity() {
333        let args = Args::parse_from([
334            "ralph",
335            "--git-user-name",
336            "John Doe",
337            "--git-user-email",
338            "john@example.com",
339        ]);
340        let events = args_to_events(&args);
341
342        assert!(
343            events.contains(&CliEvent::GitUserNameSet {
344                name: "John Doe".to_string()
345            }),
346            "Should have git user name event"
347        );
348        assert!(
349            events.contains(&CliEvent::GitUserEmailSet {
350                email: "john@example.com".to_string()
351            }),
352            "Should have git user email event"
353        );
354    }
355
356    #[test]
357    fn test_args_to_events_streaming_metrics() {
358        let args = Args::parse_from(["ralph", "--show-streaming-metrics"]);
359        let events = args_to_events(&args);
360
361        assert!(
362            events.contains(&CliEvent::StreamingMetricsEnabled),
363            "Should have streaming metrics event"
364        );
365    }
366
367    #[test]
368    fn test_args_parses_pause_on_exit_mode() {
369        let args = Args::try_parse_from(["ralph", "--pause-on-exit", "always"])
370            .expect("pause-on-exit should parse");
371
372        assert_eq!(args.pause_on_exit, crate::cli::PauseOnExitMode::Always);
373    }
374}