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        let events_without_completion: Vec<_> = events
173            .iter()
174            .filter(|e| *e != &CliEvent::CliProcessingComplete)
175            .collect();
176        assert!(
177            events_without_completion.is_empty(),
178            "Should have no other events for empty args"
179        );
180    }
181
182    #[test]
183    fn test_args_to_events_quick_preset() {
184        let args = Args::parse_from(["ralph", "-Q"]);
185        let events = args_to_events(&args);
186
187        assert!(
188            events.contains(&CliEvent::QuickPresetApplied),
189            "Should have quick preset event"
190        );
191        assert!(events.contains(&CliEvent::CliProcessingComplete));
192    }
193
194    #[test]
195    fn test_args_to_events_rapid_preset() {
196        let args = Args::parse_from(["ralph", "-U"]);
197        let events = args_to_events(&args);
198
199        assert!(
200            events.contains(&CliEvent::RapidPresetApplied),
201            "Should have rapid preset event"
202        );
203        assert!(events.contains(&CliEvent::CliProcessingComplete));
204    }
205
206    #[test]
207    fn test_args_to_events_long_preset() {
208        let args = Args::parse_from(["ralph", "-L"]);
209        let events = args_to_events(&args);
210
211        assert!(
212            events.contains(&CliEvent::LongPresetApplied),
213            "Should have long preset event"
214        );
215        assert!(events.contains(&CliEvent::CliProcessingComplete));
216    }
217
218    #[test]
219    fn test_args_to_events_standard_preset() {
220        let args = Args::parse_from(["ralph", "-S"]);
221        let events = args_to_events(&args);
222
223        assert!(
224            events.contains(&CliEvent::StandardPresetApplied),
225            "Should have standard preset event"
226        );
227        assert!(events.contains(&CliEvent::CliProcessingComplete));
228    }
229
230    #[test]
231    fn test_args_to_events_thorough_preset() {
232        let args = Args::parse_from(["ralph", "-T"]);
233        let events = args_to_events(&args);
234
235        assert!(
236            events.contains(&CliEvent::ThoroughPresetApplied),
237            "Should have thorough preset event"
238        );
239        assert!(events.contains(&CliEvent::CliProcessingComplete));
240    }
241
242    #[test]
243    fn test_args_to_events_explicit_iters() {
244        let args = Args::parse_from(["ralph", "-D", "7", "-R", "3"]);
245        let events = args_to_events(&args);
246
247        assert!(
248            events.contains(&CliEvent::DeveloperItersSet { value: 7 }),
249            "Should have developer iters event"
250        );
251        assert!(
252            events.contains(&CliEvent::ReviewerReviewsSet { value: 3 }),
253            "Should have reviewer reviews event"
254        );
255    }
256
257    #[test]
258    fn test_args_to_events_preset_plus_explicit_override() {
259        let args = Args::parse_from(["ralph", "-Q", "-D", "10", "-R", "5"]);
260        let events = args_to_events(&args);
261
262        // Should have both preset and explicit values
263        assert!(events.contains(&CliEvent::QuickPresetApplied));
264        assert!(events.contains(&CliEvent::DeveloperItersSet { value: 10 }));
265        assert!(events.contains(&CliEvent::ReviewerReviewsSet { value: 5 }));
266
267        // Verify order: preset comes before explicit override
268        let preset_idx = events
269            .iter()
270            .position(|e| e == &CliEvent::QuickPresetApplied)
271            .expect("Should have quick preset");
272        let iters_idx = events
273            .iter()
274            .position(|e| e == &CliEvent::DeveloperItersSet { value: 10 })
275            .expect("Should have developer iters");
276
277        assert!(
278            preset_idx < iters_idx,
279            "Preset should come before explicit override"
280        );
281    }
282
283    #[test]
284    fn test_args_to_events_agent_selection() {
285        let args = Args::parse_from(["ralph", "-a", "claude", "-r", "gpt"]);
286        let events = args_to_events(&args);
287
288        assert!(
289            events.contains(&CliEvent::DeveloperAgentSet {
290                agent: "claude".to_string()
291            }),
292            "Should have developer agent event"
293        );
294        assert!(
295            events.contains(&CliEvent::ReviewerAgentSet {
296                agent: "gpt".to_string()
297            }),
298            "Should have reviewer agent event"
299        );
300    }
301
302    #[test]
303    fn test_args_to_events_verbose_mode() {
304        let args = Args::parse_from(["ralph", "-v", "3"]);
305        let events = args_to_events(&args);
306
307        assert!(
308            events.contains(&CliEvent::VerbositySet { level: 3 }),
309            "Should have verbosity set event"
310        );
311    }
312
313    #[test]
314    fn test_args_to_events_debug_mode() {
315        let args = Args::parse_from(["ralph", "--debug"]);
316        let events = args_to_events(&args);
317
318        assert!(
319            events.contains(&CliEvent::DebugModeEnabled),
320            "Should have debug mode event"
321        );
322    }
323
324    #[test]
325    fn test_args_to_events_no_isolation() {
326        let args = Args::parse_from(["ralph", "--no-isolation"]);
327        let events = args_to_events(&args);
328
329        assert!(
330            events.contains(&CliEvent::IsolationModeDisabled),
331            "Should have isolation mode disabled event"
332        );
333    }
334
335    #[test]
336    fn test_args_to_events_git_identity() {
337        let args = Args::parse_from([
338            "ralph",
339            "--git-user-name",
340            "John Doe",
341            "--git-user-email",
342            "john@example.com",
343        ]);
344        let events = args_to_events(&args);
345
346        assert!(
347            events.contains(&CliEvent::GitUserNameSet {
348                name: "John Doe".to_string()
349            }),
350            "Should have git user name event"
351        );
352        assert!(
353            events.contains(&CliEvent::GitUserEmailSet {
354                email: "john@example.com".to_string()
355            }),
356            "Should have git user email event"
357        );
358    }
359
360    #[test]
361    fn test_args_to_events_streaming_metrics() {
362        let args = Args::parse_from(["ralph", "--show-streaming-metrics"]);
363        let events = args_to_events(&args);
364
365        assert!(
366            events.contains(&CliEvent::StreamingMetricsEnabled),
367            "Should have streaming metrics event"
368        );
369    }
370}