Skip to main content

autom8/commands/
config.rs

1//! Config command handler.
2//!
3//! Displays, modifies, and resets autom8 configuration values.
4
5use crate::config::{
6    global_config_path, load_global_config, load_project_config, project_config_path,
7    save_global_config, save_project_config, validate_config, Config,
8};
9use crate::error::{Autom8Error, Result};
10use crate::git::is_git_repo;
11use crate::output::{BOLD, CYAN, GRAY, GREEN, RESET, YELLOW};
12use clap::Subcommand;
13use std::fs;
14
15/// Valid configuration keys that can be set via `config set`.
16pub const VALID_CONFIG_KEYS: &[&str] = &[
17    "review",
18    "commit",
19    "pull_request",
20    "pull_request_draft",
21    "worktree",
22    "worktree_path_pattern",
23    "worktree_cleanup",
24];
25
26/// Scope for config operations.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ConfigScope {
29    /// Global configuration (~/.config/autom8/config.toml)
30    Global,
31    /// Project-specific configuration (~/.config/autom8/<project>/config.toml)
32    Project,
33    /// Both global and project configurations
34    Both,
35}
36
37/// Subcommands for the config command.
38#[derive(Subcommand, Debug, Clone)]
39pub enum ConfigSubcommand {
40    /// Set a configuration value
41    #[command(after_help = "EXAMPLES:
42    autom8 config set review false            # Disable review step in project config
43    autom8 config set --global commit true    # Enable auto-commit globally
44    autom8 config set worktree_path_pattern \"{repo}-feature-{branch}\"
45
46VALID KEYS:
47    review              - Enable code review step (true/false)
48    commit              - Enable auto-commit (true/false)
49    pull_request        - Enable auto-PR creation (true/false, requires commit=true)
50    pull_request_draft  - Create PRs as drafts (true/false, requires pull_request=true)
51    worktree            - Enable worktree mode (true/false)
52    worktree_path_pattern - Pattern for worktree directory names (string)
53    worktree_cleanup    - Auto-cleanup worktrees after completion (true/false)
54
55VALUE FORMATS:
56    Boolean: true, false (case-insensitive)
57    String:  Quoted or unquoted text
58
59VALIDATION:
60    - Setting pull_request=true requires commit=true
61    - Invalid keys or values are rejected with an error message")]
62    Set {
63        /// Set in global config instead of project config
64        #[arg(short, long)]
65        global: bool,
66
67        /// The configuration key to set
68        key: String,
69
70        /// The value to set
71        value: String,
72    },
73
74    /// Reset configuration to default values
75    #[command(after_help = "EXAMPLES:
76    autom8 config reset               # Reset project config (with confirmation)
77    autom8 config reset --global      # Reset global config (with confirmation)
78    autom8 config reset -y            # Reset without confirmation prompt
79    autom8 config reset --global -y   # Reset global config without prompting
80
81DEFAULT VALUES:
82    review              = true
83    commit              = true
84    pull_request        = true
85    pull_request_draft  = false
86    worktree            = true
87    worktree_path_pattern = \"{repo}-wt-{branch}\"
88    worktree_cleanup    = false
89
90BEHAVIOR:
91    - Prompts for confirmation before resetting (unless -y/--yes is used)
92    - Overwrites the config file with default values
93    - Displays the new configuration after reset
94    - If config file doesn't exist, informs you defaults are already in use")]
95    Reset {
96        /// Reset global config instead of project config
97        #[arg(short, long)]
98        global: bool,
99
100        /// Skip confirmation prompt
101        #[arg(short, long)]
102        yes: bool,
103    },
104}
105
106/// Display configuration values.
107///
108/// Shows the configuration in TOML format. When scope is `Both`, displays
109/// global config first with a header, then project config with its header.
110///
111/// # Arguments
112///
113/// * `scope` - Which configuration(s) to display
114///
115/// # Returns
116///
117/// * `Ok(())` on success
118/// * `Err(Autom8Error)` if the configuration cannot be read
119pub fn config_display_command(scope: ConfigScope) -> Result<()> {
120    match scope {
121        ConfigScope::Global => display_global_config(),
122        ConfigScope::Project => display_project_config(),
123        ConfigScope::Both => {
124            display_global_config()?;
125            println!();
126            display_project_config()
127        }
128    }
129}
130
131/// Display the global configuration.
132fn display_global_config() -> Result<()> {
133    println!("{BOLD}# Global config{RESET}");
134    println!("{GRAY}# {}{RESET}", global_config_path()?.display());
135    println!();
136
137    let config_path = global_config_path()?;
138
139    if !config_path.exists() {
140        println!("{YELLOW}# (file does not exist, using defaults){RESET}");
141        println!();
142        print_config_as_toml(&Config::default());
143        return Ok(());
144    }
145
146    let config = load_config_from_path(&config_path)?;
147    print_config_as_toml(&config);
148
149    Ok(())
150}
151
152/// Display the project configuration.
153fn display_project_config() -> Result<()> {
154    // Check if we're in a git repo - required for project config
155    if !is_git_repo() {
156        return Err(Autom8Error::Config(
157            "Not in a git repository.\n\n\
158            Project configuration requires being inside a git repository.\n\
159            Run this command from within a git repository, or use --global to view global config."
160                .to_string(),
161        ));
162    }
163
164    println!("{BOLD}# Project config{RESET}");
165    println!("{GRAY}# {}{RESET}", project_config_path()?.display());
166    println!();
167
168    let config_path = project_config_path()?;
169
170    if !config_path.exists() {
171        println!("{YELLOW}# (file does not exist, using global config or defaults){RESET}");
172        println!();
173        // Show what would be effective - either global config or defaults
174        let effective_config = if global_config_path()?.exists() {
175            load_config_from_path(&global_config_path()?)?
176        } else {
177            Config::default()
178        };
179        print_config_as_toml(&effective_config);
180        return Ok(());
181    }
182
183    let config = load_config_from_path(&config_path)?;
184    print_config_as_toml(&config);
185
186    Ok(())
187}
188
189/// Load a config from a specific path without any fallback logic.
190fn load_config_from_path(path: &std::path::Path) -> Result<Config> {
191    let content = fs::read_to_string(path)?;
192    toml::from_str(&content).map_err(|e| {
193        Autom8Error::Config(format!("Failed to parse config file at {:?}: {}", path, e))
194    })
195}
196
197// ============================================================================
198// Config Set Command (US-002)
199// ============================================================================
200
201/// Set a configuration value.
202///
203/// Sets a single configuration key to the specified value, with immediate
204/// validation. Creates the config file if it doesn't exist.
205///
206/// # Arguments
207///
208/// * `key` - The configuration key to set (e.g., "review", "commit")
209/// * `value` - The value to set (booleans: "true"/"false", strings as-is)
210/// * `global` - If true, sets in global config; otherwise in project config
211///
212/// # Returns
213///
214/// * `Ok(())` on success
215/// * `Err(Autom8Error)` if the key is invalid, value is invalid, or validation fails
216pub fn config_set_command(key: &str, value: &str, global: bool) -> Result<()> {
217    // Validate the key
218    if !VALID_CONFIG_KEYS.contains(&key) {
219        return Err(Autom8Error::Config(format!(
220            "Invalid configuration key: '{}'\n\n\
221            Valid keys are:\n  - {}\n\n\
222            Use 'autom8 config set --help' for more information.",
223            key,
224            VALID_CONFIG_KEYS.join("\n  - ")
225        )));
226    }
227
228    // Check if we're in a git repo for project config
229    if !global && !is_git_repo() {
230        return Err(Autom8Error::Config(
231            "Not in a git repository.\n\n\
232            Project configuration requires being inside a git repository.\n\
233            Either:\n  - Run this command from within a git repository, or\n  - Use --global to set the global config."
234                .to_string(),
235        ));
236    }
237
238    // Load the current config (create default if doesn't exist)
239    let mut config = if global {
240        load_global_config()?
241    } else {
242        load_project_config()?
243    };
244
245    // Parse and set the value
246    set_config_value(&mut config, key, value)?;
247
248    // Validate the resulting configuration
249    validate_config(&config).map_err(|e| Autom8Error::Config(e.to_string()))?;
250
251    // Save the config
252    let config_type = if global { "global" } else { "project" };
253    if global {
254        save_global_config(&config)?;
255    } else {
256        save_project_config(&config)?;
257    }
258
259    // Print confirmation
260    let display_value = format_value_for_display(key, &config);
261    println!("{GREEN}Set {CYAN}{key}{RESET} = {display_value} in {config_type} config{RESET}");
262
263    Ok(())
264}
265
266/// Set a configuration value on a Config struct.
267///
268/// Parses the string value and sets the appropriate field.
269fn set_config_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
270    match key {
271        "review" => {
272            config.review = parse_bool_value(value, key)?;
273        }
274        "commit" => {
275            config.commit = parse_bool_value(value, key)?;
276        }
277        "pull_request" => {
278            config.pull_request = parse_bool_value(value, key)?;
279        }
280        "pull_request_draft" => {
281            config.pull_request_draft = parse_bool_value(value, key)?;
282        }
283        "worktree" => {
284            config.worktree = parse_bool_value(value, key)?;
285        }
286        "worktree_path_pattern" => {
287            config.worktree_path_pattern = value.to_string();
288        }
289        "worktree_cleanup" => {
290            config.worktree_cleanup = parse_bool_value(value, key)?;
291        }
292        _ => {
293            // This shouldn't happen if VALID_CONFIG_KEYS is kept in sync
294            return Err(Autom8Error::Config(format!("Unknown key: {}", key)));
295        }
296    }
297    Ok(())
298}
299
300/// Parse a boolean value from a string (case-insensitive).
301fn parse_bool_value(value: &str, key: &str) -> Result<bool> {
302    match value.to_lowercase().as_str() {
303        "true" => Ok(true),
304        "false" => Ok(false),
305        _ => Err(Autom8Error::Config(format!(
306            "Invalid value for '{}': expected boolean (true/false), got '{}'",
307            key, value
308        ))),
309    }
310}
311
312/// Format a config value for display in the confirmation message.
313fn format_value_for_display(key: &str, config: &Config) -> String {
314    match key {
315        "review" => config.review.to_string(),
316        "commit" => config.commit.to_string(),
317        "pull_request" => config.pull_request.to_string(),
318        "pull_request_draft" => config.pull_request_draft.to_string(),
319        "worktree" => config.worktree.to_string(),
320        "worktree_path_pattern" => format!("\"{}\"", config.worktree_path_pattern),
321        "worktree_cleanup" => config.worktree_cleanup.to_string(),
322        _ => "unknown".to_string(),
323    }
324}
325
326// ============================================================================
327// Config Reset Command (US-003)
328// ============================================================================
329
330/// Reset configuration to default values.
331///
332/// Resets the specified config file to default values. Prompts for
333/// confirmation before resetting (unless `--yes` is used).
334///
335/// # Arguments
336///
337/// * `global` - If true, resets global config; otherwise resets project config
338/// * `yes` - If true, skips confirmation prompt
339///
340/// # Returns
341///
342/// * `Ok(())` on success
343/// * `Err(Autom8Error)` if the reset fails or user cancels
344pub fn config_reset_command(global: bool, yes: bool) -> Result<()> {
345    // Check if we're in a git repo for project config
346    if !global && !is_git_repo() {
347        return Err(Autom8Error::Config(
348            "Not in a git repository.\n\n\
349            Project configuration requires being inside a git repository.\n\
350            Either:\n  - Run this command from within a git repository, or\n  - Use --global to reset the global config."
351                .to_string(),
352        ));
353    }
354
355    let config_type = if global { "global" } else { "project" };
356    let config_path = if global {
357        global_config_path()?
358    } else {
359        project_config_path()?
360    };
361
362    // Check if config file exists
363    if !config_path.exists() {
364        println!(
365            "{YELLOW}Config file does not exist: {}{RESET}",
366            config_path.display()
367        );
368        println!();
369        println!("Default values are already in use:");
370        println!();
371        print_config_as_toml(&Config::default());
372        return Ok(());
373    }
374
375    // Prompt for confirmation unless --yes is set
376    if !yes {
377        print!("Reset {} config to defaults? [y/N] ", config_type);
378        std::io::Write::flush(&mut std::io::stdout())?;
379
380        let mut input = String::new();
381        std::io::stdin().read_line(&mut input)?;
382
383        let input = input.trim().to_lowercase();
384        if input != "y" && input != "yes" {
385            println!("{YELLOW}Reset cancelled.{RESET}");
386            return Ok(());
387        }
388    }
389
390    // Create default config and save it
391    let default_config = Config::default();
392
393    if global {
394        save_global_config(&default_config)?;
395    } else {
396        save_project_config(&default_config)?;
397    }
398
399    // Print confirmation and new config
400    println!("{GREEN}Reset {} config to defaults:{RESET}", config_type);
401    println!();
402    print_config_as_toml(&default_config);
403
404    Ok(())
405}
406
407/// Print a Config struct as valid TOML format.
408fn print_config_as_toml(config: &Config) {
409    println!("{CYAN}review{RESET} = {}", config.review);
410    println!("{CYAN}commit{RESET} = {}", config.commit);
411    println!("{CYAN}pull_request{RESET} = {}", config.pull_request);
412    println!(
413        "{CYAN}pull_request_draft{RESET} = {}",
414        config.pull_request_draft
415    );
416    println!("{CYAN}worktree{RESET} = {}", config.worktree);
417    println!(
418        "{CYAN}worktree_path_pattern{RESET} = \"{}\"",
419        config.worktree_path_pattern
420    );
421    println!(
422        "{CYAN}worktree_cleanup{RESET} = {}",
423        config.worktree_cleanup
424    );
425}
426
427/// Convert a Config to a TOML string (for testing).
428#[cfg(test)]
429fn config_to_toml_string(config: &Config) -> String {
430    format!(
431        "review = {}\n\
432         commit = {}\n\
433         pull_request = {}\n\
434         pull_request_draft = {}\n\
435         worktree = {}\n\
436         worktree_path_pattern = \"{}\"\n\
437         worktree_cleanup = {}",
438        config.review,
439        config.commit,
440        config.pull_request,
441        config.pull_request_draft,
442        config.worktree,
443        config.worktree_path_pattern,
444        config.worktree_cleanup
445    )
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    // ========================================================================
453    // US-001: Config display tests
454    // ========================================================================
455
456    #[test]
457    fn test_us001_config_to_toml_produces_valid_toml() {
458        let config = Config::default();
459        let toml_str = config_to_toml_string(&config);
460
461        // Parse the generated TOML to verify it's valid
462        let parsed: std::result::Result<Config, _> = toml::from_str(&toml_str);
463        assert!(
464            parsed.is_ok(),
465            "Generated TOML should be parseable: {:?}",
466            parsed.err()
467        );
468
469        // Verify the parsed config matches the original
470        let parsed_config = parsed.unwrap();
471        assert_eq!(parsed_config, config);
472    }
473
474    #[test]
475    fn test_us001_config_to_toml_includes_all_fields() {
476        let config = Config {
477            review: false,
478            commit: true,
479            pull_request: false,
480            pull_request_draft: true,
481            worktree: false,
482            worktree_path_pattern: "custom-{branch}".to_string(),
483            worktree_cleanup: true,
484        };
485        let toml_str = config_to_toml_string(&config);
486
487        assert!(toml_str.contains("review = false"));
488        assert!(toml_str.contains("commit = true"));
489        assert!(toml_str.contains("pull_request = false"));
490        assert!(toml_str.contains("pull_request_draft = true"));
491        assert!(toml_str.contains("worktree = false"));
492        assert!(toml_str.contains("worktree_path_pattern = \"custom-{branch}\""));
493        assert!(toml_str.contains("worktree_cleanup = true"));
494    }
495
496    #[test]
497    fn test_us001_config_to_toml_default_values() {
498        let config = Config::default();
499        let toml_str = config_to_toml_string(&config);
500
501        // Verify default values are correct
502        assert!(toml_str.contains("review = true"));
503        assert!(toml_str.contains("commit = true"));
504        assert!(toml_str.contains("pull_request = true"));
505        assert!(toml_str.contains("worktree = true"));
506        assert!(toml_str.contains("worktree_path_pattern = \"{repo}-wt-{branch}\""));
507        assert!(toml_str.contains("worktree_cleanup = false"));
508    }
509
510    #[test]
511    fn test_us001_config_scope_variants() {
512        // Verify all scope variants exist and are distinct
513        assert_ne!(ConfigScope::Global, ConfigScope::Project);
514        assert_ne!(ConfigScope::Global, ConfigScope::Both);
515        assert_ne!(ConfigScope::Project, ConfigScope::Both);
516    }
517
518    // ========================================================================
519    // US-002: Config set tests
520    // ========================================================================
521
522    #[test]
523    fn test_us002_valid_config_keys_constant() {
524        // Verify all expected keys are in the VALID_CONFIG_KEYS constant
525        assert!(VALID_CONFIG_KEYS.contains(&"review"));
526        assert!(VALID_CONFIG_KEYS.contains(&"commit"));
527        assert!(VALID_CONFIG_KEYS.contains(&"pull_request"));
528        assert!(VALID_CONFIG_KEYS.contains(&"pull_request_draft"));
529        assert!(VALID_CONFIG_KEYS.contains(&"worktree"));
530        assert!(VALID_CONFIG_KEYS.contains(&"worktree_path_pattern"));
531        assert!(VALID_CONFIG_KEYS.contains(&"worktree_cleanup"));
532        assert_eq!(
533            VALID_CONFIG_KEYS.len(),
534            7,
535            "Should have exactly 7 valid keys"
536        );
537    }
538
539    #[test]
540    fn test_us002_parse_bool_value_true() {
541        // Test various true spellings (case-insensitive)
542        assert!(parse_bool_value("true", "test").unwrap());
543        assert!(parse_bool_value("TRUE", "test").unwrap());
544        assert!(parse_bool_value("True", "test").unwrap());
545        assert!(parse_bool_value("tRuE", "test").unwrap());
546    }
547
548    #[test]
549    fn test_us002_parse_bool_value_false() {
550        // Test various false spellings (case-insensitive)
551        assert!(!parse_bool_value("false", "test").unwrap());
552        assert!(!parse_bool_value("FALSE", "test").unwrap());
553        assert!(!parse_bool_value("False", "test").unwrap());
554        assert!(!parse_bool_value("fAlSe", "test").unwrap());
555    }
556
557    #[test]
558    fn test_us002_parse_bool_value_invalid() {
559        // Test invalid values
560        let result = parse_bool_value("yes", "review");
561        assert!(result.is_err());
562        let err = result.unwrap_err().to_string();
563        assert!(err.contains("Invalid value for 'review'"));
564        assert!(err.contains("expected boolean"));
565        assert!(err.contains("yes"));
566
567        let result = parse_bool_value("1", "commit");
568        assert!(result.is_err());
569
570        let result = parse_bool_value("on", "worktree");
571        assert!(result.is_err());
572
573        let result = parse_bool_value("", "review");
574        assert!(result.is_err());
575    }
576
577    #[test]
578    fn test_us002_set_config_value_review() {
579        let mut config = Config::default();
580        assert!(config.review); // default is true
581
582        set_config_value(&mut config, "review", "false").unwrap();
583        assert!(!config.review);
584
585        set_config_value(&mut config, "review", "true").unwrap();
586        assert!(config.review);
587    }
588
589    #[test]
590    fn test_us002_set_config_value_commit() {
591        let mut config = Config::default();
592        set_config_value(&mut config, "commit", "false").unwrap();
593        assert!(!config.commit);
594    }
595
596    #[test]
597    fn test_us002_set_config_value_pull_request() {
598        let mut config = Config::default();
599        set_config_value(&mut config, "pull_request", "false").unwrap();
600        assert!(!config.pull_request);
601    }
602
603    #[test]
604    fn test_us002_set_config_value_worktree() {
605        let mut config = Config::default();
606        set_config_value(&mut config, "worktree", "false").unwrap();
607        assert!(!config.worktree);
608    }
609
610    #[test]
611    fn test_us002_set_config_value_worktree_cleanup() {
612        let mut config = Config::default();
613        assert!(!config.worktree_cleanup); // default is false
614
615        set_config_value(&mut config, "worktree_cleanup", "true").unwrap();
616        assert!(config.worktree_cleanup);
617    }
618
619    #[test]
620    fn test_us002_set_config_value_worktree_path_pattern() {
621        let mut config = Config::default();
622        let custom_pattern = "{repo}-feature-{branch}";
623
624        set_config_value(&mut config, "worktree_path_pattern", custom_pattern).unwrap();
625        assert_eq!(config.worktree_path_pattern, custom_pattern);
626    }
627
628    #[test]
629    fn test_us002_set_config_value_worktree_path_pattern_with_spaces() {
630        let mut config = Config::default();
631        let pattern_with_spaces = "my-repo wt {branch}";
632
633        set_config_value(&mut config, "worktree_path_pattern", pattern_with_spaces).unwrap();
634        assert_eq!(config.worktree_path_pattern, pattern_with_spaces);
635    }
636
637    #[test]
638    fn test_us002_format_value_for_display_boolean() {
639        let mut config = Config::default();
640        config.review = true;
641        config.commit = false;
642
643        assert_eq!(format_value_for_display("review", &config), "true");
644        assert_eq!(format_value_for_display("commit", &config), "false");
645    }
646
647    #[test]
648    fn test_us002_format_value_for_display_string() {
649        let mut config = Config::default();
650        config.worktree_path_pattern = "custom-{branch}".to_string();
651
652        // String values should be quoted
653        assert_eq!(
654            format_value_for_display("worktree_path_pattern", &config),
655            "\"custom-{branch}\""
656        );
657    }
658
659    #[test]
660    fn test_us002_invalid_key_rejected() {
661        let mut config = Config::default();
662        let result = set_config_value(&mut config, "invalid_key", "value");
663        assert!(result.is_err());
664        assert!(result.unwrap_err().to_string().contains("Unknown key"));
665    }
666
667    #[test]
668    fn test_us002_all_valid_keys_settable() {
669        // Verify each valid key can be set
670        for key in VALID_CONFIG_KEYS.iter() {
671            let mut config = Config::default();
672            let value = match *key {
673                "worktree_path_pattern" => "custom-pattern",
674                _ => "false", // Boolean keys
675            };
676            let result = set_config_value(&mut config, key, value);
677            assert!(result.is_ok(), "Setting key '{}' should succeed", key);
678        }
679    }
680
681    #[test]
682    fn test_us002_validation_enforced_pr_without_commit() {
683        // Test that setting pull_request=true when commit=false would fail validation
684        let mut config = Config {
685            review: true,
686            commit: false,
687            pull_request: false,
688            ..Default::default()
689        };
690
691        // Set pull_request to true
692        set_config_value(&mut config, "pull_request", "true").unwrap();
693
694        // The config should fail validation (this is what config_set_command checks)
695        let validation_result = validate_config(&config);
696        assert!(validation_result.is_err());
697        assert!(validation_result
698            .unwrap_err()
699            .to_string()
700            .contains("commit"));
701    }
702
703    #[test]
704    fn test_us002_validation_enforced_commit_false_with_pr_true() {
705        // Test that setting commit=false when pull_request=true would fail validation
706        let mut config = Config {
707            review: true,
708            commit: true,
709            pull_request: true,
710            ..Default::default()
711        };
712
713        // Set commit to false
714        set_config_value(&mut config, "commit", "false").unwrap();
715
716        // The config should fail validation
717        let validation_result = validate_config(&config);
718        assert!(validation_result.is_err());
719    }
720
721    #[test]
722    fn test_us002_valid_combinations_pass_validation() {
723        // Test valid combinations
724        let valid_combos = [
725            (true, true, true),   // all true
726            (true, true, false),  // commit true, pr false
727            (true, false, false), // commit false, pr false
728            (false, true, true),  // review false, others true
729        ];
730
731        for (review, commit, pull_request) in valid_combos {
732            let config = Config {
733                review,
734                commit,
735                pull_request,
736                ..Default::default()
737            };
738            let result = validate_config(&config);
739            assert!(
740                result.is_ok(),
741                "Config (review={}, commit={}, pull_request={}) should be valid",
742                review,
743                commit,
744                pull_request
745            );
746        }
747    }
748
749    #[test]
750    fn test_us002_case_insensitive_boolean_values() {
751        let mut config = Config::default();
752
753        // Test various case combinations
754        set_config_value(&mut config, "review", "TRUE").unwrap();
755        assert!(config.review);
756
757        set_config_value(&mut config, "review", "False").unwrap();
758        assert!(!config.review);
759
760        set_config_value(&mut config, "worktree", "TrUe").unwrap();
761        assert!(config.worktree);
762    }
763
764    #[test]
765    fn test_us002_invalid_boolean_value_descriptive_error() {
766        let result = parse_bool_value("yes", "review");
767        assert!(result.is_err());
768
769        let error_msg = result.unwrap_err().to_string();
770        assert!(error_msg.contains("review"), "Error should mention the key");
771        assert!(
772            error_msg.contains("boolean"),
773            "Error should mention expected type"
774        );
775        assert!(
776            error_msg.contains("true/false"),
777            "Error should mention valid values"
778        );
779        assert!(
780            error_msg.contains("yes"),
781            "Error should mention the invalid value"
782        );
783    }
784
785    // ========================================================================
786    // US-003: Config reset tests
787    // ========================================================================
788
789    #[test]
790    fn test_us003_config_reset_function_exists() {
791        // Verify the config_reset_command function exists and has correct signature
792        use super::config_reset_command;
793        let _: fn(bool, bool) -> Result<()> = config_reset_command;
794    }
795
796    #[test]
797    fn test_us003_default_config_values() {
798        // Verify the default config values that reset would restore
799        let config = Config::default();
800        assert!(config.review, "default review should be true");
801        assert!(config.commit, "default commit should be true");
802        assert!(config.pull_request, "default pull_request should be true");
803        assert!(config.worktree, "default worktree should be true");
804        assert_eq!(
805            config.worktree_path_pattern, "{repo}-wt-{branch}",
806            "default worktree_path_pattern should be '{{repo}}-wt-{{branch}}'"
807        );
808        assert!(
809            !config.worktree_cleanup,
810            "default worktree_cleanup should be false"
811        );
812    }
813
814    #[test]
815    fn test_us003_reset_produces_defaults() {
816        // Verify that a reset would produce the same values as Config::default()
817        let reset_config = Config::default();
818        let expected = Config::default();
819
820        assert_eq!(reset_config.review, expected.review);
821        assert_eq!(reset_config.commit, expected.commit);
822        assert_eq!(reset_config.pull_request, expected.pull_request);
823        assert_eq!(reset_config.worktree, expected.worktree);
824        assert_eq!(
825            reset_config.worktree_path_pattern,
826            expected.worktree_path_pattern
827        );
828        assert_eq!(reset_config.worktree_cleanup, expected.worktree_cleanup);
829    }
830
831    #[test]
832    fn test_us003_config_toml_from_default() {
833        // Verify the default config serializes correctly to TOML
834        let config = Config::default();
835        let toml_str = config_to_toml_string(&config);
836
837        // Should contain all default values
838        assert!(toml_str.contains("review = true"));
839        assert!(toml_str.contains("commit = true"));
840        assert!(toml_str.contains("pull_request = true"));
841        assert!(toml_str.contains("worktree = true"));
842        assert!(toml_str.contains("worktree_path_pattern = \"{repo}-wt-{branch}\""));
843        assert!(toml_str.contains("worktree_cleanup = false"));
844    }
845
846    #[test]
847    fn test_us003_reset_toml_round_trip() {
848        // Verify the default config can be written and read back correctly
849        let default_config = Config::default();
850        let toml_str = config_to_toml_string(&default_config);
851
852        let parsed: Config = toml::from_str(&toml_str).unwrap();
853        assert_eq!(parsed.review, default_config.review);
854        assert_eq!(parsed.commit, default_config.commit);
855        assert_eq!(parsed.pull_request, default_config.pull_request);
856        assert_eq!(parsed.worktree, default_config.worktree);
857        assert_eq!(
858            parsed.worktree_path_pattern,
859            default_config.worktree_path_pattern
860        );
861        assert_eq!(parsed.worktree_cleanup, default_config.worktree_cleanup);
862    }
863
864    #[test]
865    fn test_us003_modified_config_differs_from_default() {
866        // Verify we can detect when a config differs from default
867        let mut modified = Config::default();
868        modified.review = false;
869        modified.commit = false;
870        modified.worktree_path_pattern = "custom-{branch}".to_string();
871
872        let default = Config::default();
873
874        assert_ne!(modified.review, default.review);
875        assert_ne!(modified.commit, default.commit);
876        assert_ne!(
877            modified.worktree_path_pattern,
878            default.worktree_path_pattern
879        );
880    }
881
882    #[test]
883    fn test_us003_global_vs_project_config_paths_different() {
884        // Verify global and project config paths are different
885        // (reset should only affect the specified config)
886        let global_path = global_config_path();
887        let project_path = project_config_path();
888
889        // Both should succeed or global succeeds and project fails (not in git repo)
890        if let (Ok(global), Ok(project)) = (global_path, project_path) {
891            assert_ne!(
892                global, project,
893                "Global and project config paths should be different"
894            );
895        }
896    }
897
898    #[test]
899    fn test_us003_save_global_config_available() {
900        // Verify save_global_config function is available (used by reset)
901        let config = Config::default();
902        // Just verify the function exists and accepts the right type
903        // We don't call it in tests to avoid side effects
904        let _ = &config; // Suppress unused warning
905                         // The actual function call would be: save_global_config(&config)
906    }
907
908    #[test]
909    fn test_us003_save_project_config_available() {
910        // Verify save_project_config function is available (used by reset)
911        let config = Config::default();
912        let _ = &config;
913        // The actual function call would be: save_project_config(&config)
914    }
915}