Skip to main content

ralph_workflow/cli/reducer/
apply.rs

1//! Apply CLI state to Config.
2//!
3//! This module handles the final step of the CLI processing pipeline:
4//! taking a `CliState` and applying its values to the actual Config struct.
5
6use super::state::CliState;
7use crate::config::{Config, ReviewDepth, Verbosity};
8
9/// Apply CLI state to configuration (functional pattern - returns new Config).
10///
11/// This function takes the accumulated CLI state and applies all values
12/// to a new Config struct, respecting priority rules:
13///
14/// - Verbosity: debug > full > quiet > explicit > base
15/// - Iterations: explicit -D/-R > preset > config default
16/// - Agent settings: CLI > config > defaults
17///
18/// # Arguments
19///
20/// * `cli_state` - The CLI state after processing all events
21/// * `config` - The base configuration to apply state to
22///
23/// # Returns
24///
25/// A new Config with CLI state applied
26#[must_use]
27pub fn apply_cli_state_to_config(cli_state: &CliState, config: &Config) -> Config {
28    // ===== Verbosity =====
29    // Priority: debug > full > quiet > explicit > base
30    let verbosity = if cli_state.debug_mode {
31        Verbosity::Debug
32    } else if cli_state.full_mode {
33        Verbosity::Full
34    } else if cli_state.quiet_mode {
35        Verbosity::Quiet
36    } else {
37        cli_state
38            .verbosity
39            .map(Verbosity::from)
40            .unwrap_or(config.verbosity)
41    };
42
43    // ===== Iteration Counts =====
44    let current_developer_iters = config.developer_iters;
45    let current_reviewer_reviews = config.reviewer_reviews;
46    let developer_iters = cli_state.resolved_developer_iters(current_developer_iters);
47    let reviewer_reviews = cli_state.resolved_reviewer_reviews(current_reviewer_reviews);
48
49    // ===== Agent Selection =====
50    let developer_agent = cli_state
51        .developer_agent
52        .clone()
53        .or_else(|| config.developer_agent.clone());
54    let reviewer_agent = cli_state
55        .reviewer_agent
56        .clone()
57        .or_else(|| config.reviewer_agent.clone());
58
59    // ===== Model and Provider Overrides =====
60    let developer_model = cli_state
61        .developer_model
62        .clone()
63        .or_else(|| config.developer_model.clone());
64    let reviewer_model = cli_state
65        .reviewer_model
66        .clone()
67        .or_else(|| config.reviewer_model.clone());
68    let developer_provider = cli_state
69        .developer_provider
70        .clone()
71        .or_else(|| config.developer_provider.clone());
72    let reviewer_provider = cli_state
73        .reviewer_provider
74        .clone()
75        .or_else(|| config.reviewer_provider.clone());
76    let reviewer_json_parser = cli_state
77        .reviewer_json_parser
78        .clone()
79        .or_else(|| config.reviewer_json_parser.clone());
80
81    // ===== Configuration Flags =====
82    // Isolation mode: explicit CLI flag > config default
83    let isolation_mode = cli_state.isolation_mode.unwrap_or(config.isolation_mode);
84
85    // Review depth
86    let review_depth = cli_state
87        .review_depth
88        .as_ref()
89        .and_then(|d| ReviewDepth::from_str(d))
90        .unwrap_or(config.review_depth);
91
92    // Git identity (highest priority in resolution chain)
93    let git_user_name = cli_state
94        .git_user_name
95        .clone()
96        .or_else(|| config.git_user_name.clone());
97    let git_user_email = cli_state
98        .git_user_email
99        .clone()
100        .or_else(|| config.git_user_email.clone());
101
102    // Streaming metrics
103    let show_streaming_metrics = config.show_streaming_metrics || cli_state.streaming_metrics;
104
105    // ===== Agent Presets =====
106    // Handle named presets (default, opencode)
107    let (developer_agent, reviewer_agent) = if let Some(ref preset) = cli_state.agent_preset {
108        if preset.as_str() == "opencode" {
109            (Some("opencode".to_string()), Some("opencode".to_string()))
110        } else {
111            (developer_agent, reviewer_agent)
112        }
113    } else {
114        (developer_agent, reviewer_agent)
115    };
116
117    // Build new config using struct update syntax (functional pattern)
118    Config {
119        verbosity,
120        developer_iters,
121        reviewer_reviews,
122        developer_agent,
123        reviewer_agent,
124        developer_model,
125        reviewer_model,
126        developer_provider,
127        reviewer_provider,
128        reviewer_json_parser,
129        isolation_mode,
130        review_depth,
131        git_user_name,
132        git_user_email,
133        show_streaming_metrics,
134        ..config.clone()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::config::types::{BehavioralFlags, FeatureFlags};
142
143    fn create_test_config() -> Config {
144        Config {
145            developer_agent: None,
146            reviewer_agent: None,
147            developer_cmd: None,
148            reviewer_cmd: None,
149            commit_cmd: None,
150            developer_model: None,
151            reviewer_model: None,
152            developer_provider: None,
153            reviewer_provider: None,
154            reviewer_json_parser: None,
155            features: FeatureFlags {
156                checkpoint_enabled: true,
157                force_universal_prompt: false,
158            },
159            developer_iters: 5,
160            reviewer_reviews: 2,
161            fast_check_cmd: None,
162            full_check_cmd: None,
163            behavior: BehavioralFlags {
164                interactive: true,
165                auto_detect_stack: true,
166                strict_validation: false,
167            },
168            prompt_path: std::path::PathBuf::from(".agent/last_prompt.txt"),
169            user_templates_dir: None,
170            developer_context: 1,
171            reviewer_context: 0,
172            verbosity: Verbosity::Verbose,
173            review_depth: ReviewDepth::Standard,
174            isolation_mode: true,
175            git_user_name: None,
176            git_user_email: None,
177            show_streaming_metrics: false,
178            review_format_retries: 5,
179            max_dev_continuations: Some(2),
180            max_xsd_retries: Some(10),
181            max_same_agent_retries: Some(2),
182            max_commit_residual_retries: Some(10),
183            execution_history_limit: 1000,
184            cloud: crate::config::types::CloudConfig::disabled(),
185        }
186    }
187
188    #[test]
189    fn test_apply_verbosity_debug() {
190        let cli_state = CliState {
191            debug_mode: true,
192            ..Default::default()
193        };
194
195        let config = create_test_config();
196        let result = apply_cli_state_to_config(&cli_state, &config);
197
198        assert_eq!(result.verbosity, Verbosity::Debug);
199    }
200
201    #[test]
202    fn test_apply_verbosity_full() {
203        let cli_state = CliState {
204            full_mode: true,
205            ..Default::default()
206        };
207
208        let config = create_test_config();
209        let result = apply_cli_state_to_config(&cli_state, &config);
210
211        assert_eq!(result.verbosity, Verbosity::Full);
212    }
213
214    #[test]
215    fn test_apply_verbosity_quiet() {
216        let cli_state = CliState {
217            quiet_mode: true,
218            ..Default::default()
219        };
220
221        let config = create_test_config();
222        let result = apply_cli_state_to_config(&cli_state, &config);
223
224        assert_eq!(result.verbosity, Verbosity::Quiet);
225    }
226
227    #[test]
228    fn test_apply_verbosity_explicit() {
229        let cli_state = CliState {
230            verbosity: Some(3),
231            ..Default::default()
232        };
233
234        let config = create_test_config();
235        let result = apply_cli_state_to_config(&cli_state, &config);
236
237        assert_eq!(result.verbosity, Verbosity::Full); // level 3 = Full
238    }
239
240    #[test]
241    fn test_apply_iters_from_preset() {
242        use super::super::state::PresetType;
243
244        let cli_state = CliState {
245            preset_applied: Some(PresetType::Long),
246            ..Default::default()
247        };
248
249        let config = Config {
250            developer_iters: 5,
251            reviewer_reviews: 2,
252            ..create_test_config()
253        };
254
255        let result = apply_cli_state_to_config(&cli_state, &config);
256
257        assert_eq!(result.developer_iters, 15);
258        assert_eq!(result.reviewer_reviews, 10);
259    }
260
261    #[test]
262    fn test_apply_iters_explicit_override_preset() {
263        use super::super::state::PresetType;
264
265        let cli_state = CliState {
266            preset_applied: Some(PresetType::Quick), // Would give 1, 1
267            developer_iters: Some(7),                // Explicit override
268            reviewer_reviews: Some(3),               // Explicit override
269            ..Default::default()
270        };
271
272        let config = create_test_config();
273        let result = apply_cli_state_to_config(&cli_state, &config);
274
275        // Explicit values should override preset
276        assert_eq!(result.developer_iters, 7);
277        assert_eq!(result.reviewer_reviews, 3);
278    }
279
280    #[test]
281    fn test_apply_developer_agent() {
282        let cli_state = CliState {
283            developer_agent: Some("claude".to_string()),
284            ..Default::default()
285        };
286
287        let config = create_test_config();
288        let result = apply_cli_state_to_config(&cli_state, &config);
289
290        assert_eq!(result.developer_agent, Some("claude".to_string()));
291    }
292
293    #[test]
294    fn test_apply_reviewer_agent() {
295        let cli_state = CliState {
296            reviewer_agent: Some("gpt".to_string()),
297            ..Default::default()
298        };
299
300        let config = create_test_config();
301        let result = apply_cli_state_to_config(&cli_state, &config);
302
303        assert_eq!(result.reviewer_agent, Some("gpt".to_string()));
304    }
305
306    #[test]
307    fn test_apply_isolation_mode_disabled() {
308        let cli_state = CliState {
309            isolation_mode: Some(false),
310            ..Default::default()
311        };
312
313        let config = Config {
314            isolation_mode: true,
315            ..create_test_config()
316        };
317
318        let result = apply_cli_state_to_config(&cli_state, &config);
319
320        assert!(!result.isolation_mode);
321    }
322
323    #[test]
324    fn test_apply_review_depth() {
325        let cli_state = CliState {
326            review_depth: Some("comprehensive".to_string()),
327            ..Default::default()
328        };
329
330        let config = create_test_config();
331        let result = apply_cli_state_to_config(&cli_state, &config);
332
333        assert_eq!(result.review_depth, ReviewDepth::Comprehensive);
334    }
335
336    #[test]
337    fn test_apply_git_identity() {
338        let cli_state = CliState {
339            git_user_name: Some("John Doe".to_string()),
340            git_user_email: Some("john@example.com".to_string()),
341            ..Default::default()
342        };
343
344        let config = create_test_config();
345        let result = apply_cli_state_to_config(&cli_state, &config);
346
347        assert_eq!(result.git_user_name, Some("John Doe".to_string()));
348        assert_eq!(result.git_user_email, Some("john@example.com".to_string()));
349    }
350
351    #[test]
352    fn test_apply_streaming_metrics() {
353        let cli_state = CliState {
354            streaming_metrics: true,
355            ..Default::default()
356        };
357
358        let config = create_test_config();
359        let result = apply_cli_state_to_config(&cli_state, &config);
360
361        assert!(result.show_streaming_metrics);
362    }
363
364    #[test]
365    fn test_apply_agent_preset_opencode() {
366        let cli_state = CliState {
367            agent_preset: Some("opencode".to_string()),
368            ..Default::default()
369        };
370
371        let config = create_test_config();
372        let result = apply_cli_state_to_config(&cli_state, &config);
373
374        assert_eq!(result.developer_agent, Some("opencode".to_string()));
375        assert_eq!(result.reviewer_agent, Some("opencode".to_string()));
376    }
377
378    #[test]
379    fn test_apply_agent_preset_default() {
380        let cli_state = CliState {
381            agent_preset: Some("default".to_string()),
382            ..Default::default()
383        };
384
385        let config = Config {
386            developer_agent: Some("existing-dev".to_string()),
387            reviewer_agent: Some("existing-rev".to_string()),
388            ..create_test_config()
389        };
390
391        let result = apply_cli_state_to_config(&cli_state, &config);
392
393        // Default preset should not change existing agents
394        assert_eq!(result.developer_agent, Some("existing-dev".to_string()));
395        assert_eq!(result.reviewer_agent, Some("existing-rev".to_string()));
396    }
397
398    #[test]
399    fn test_apply_preserves_unrelated_config_fields() {
400        let cli_state = CliState {
401            developer_agent: Some("new-agent".to_string()),
402            ..Default::default()
403        };
404
405        let config = Config {
406            isolation_mode: true,
407            review_depth: ReviewDepth::Comprehensive,
408            ..create_test_config()
409        };
410
411        let result = apply_cli_state_to_config(&cli_state, &config);
412
413        // Should only change developer_agent
414        assert_eq!(result.developer_agent, Some("new-agent".to_string()));
415        assert!(result.isolation_mode);
416        assert_eq!(result.review_depth, ReviewDepth::Comprehensive);
417    }
418}