Skip to main content

autom8/
completion.rs

1//! Shell completion infrastructure for autom8.
2//!
3//! This module provides:
4//! - Shell detection from the `$SHELL` environment variable
5//! - Completion script generation for bash, zsh, and fish
6//! - Installation path resolution for each shell type
7//!
8//! # Usage
9//!
10//! ```ignore
11//! use autom8::completion::{detect_shell, get_completion_path, generate_completion_script};
12//!
13//! // Detect user's shell
14//! let shell = detect_shell()?;
15//!
16//! // Get the installation path
17//! let path = get_completion_path(shell)?;
18//!
19//! // Generate the completion script
20//! let script = generate_completion_script(shell);
21//! ```
22
23use crate::error::{Autom8Error, Result};
24use clap::Command;
25use clap_complete::{generate, Shell};
26use std::io::Write;
27use std::path::PathBuf;
28
29/// List of supported shell names for error messages.
30pub const SUPPORTED_SHELLS: &[&str] = &["bash", "zsh", "fish"];
31
32/// Supported shell types for completion scripts.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum ShellType {
35    Bash,
36    Zsh,
37    Fish,
38}
39
40impl ShellType {
41    /// Convert to the `clap_complete::Shell` type.
42    pub fn to_clap_shell(self) -> Shell {
43        match self {
44            ShellType::Bash => Shell::Bash,
45            ShellType::Zsh => Shell::Zsh,
46            ShellType::Fish => Shell::Fish,
47        }
48    }
49
50    /// Get the display name of the shell.
51    pub fn name(&self) -> &'static str {
52        match self {
53            ShellType::Bash => "bash",
54            ShellType::Zsh => "zsh",
55            ShellType::Fish => "fish",
56        }
57    }
58
59    /// Parse a shell type from a string name.
60    ///
61    /// # Arguments
62    ///
63    /// * `name` - The shell name (e.g., "bash", "zsh", "fish")
64    ///
65    /// # Returns
66    ///
67    /// * `Ok(ShellType)` - The parsed shell type
68    /// * `Err(Autom8Error)` - If the shell name is not supported
69    pub fn from_name(name: &str) -> Result<ShellType> {
70        match name.to_lowercase().as_str() {
71            "bash" => Ok(ShellType::Bash),
72            "zsh" => Ok(ShellType::Zsh),
73            "fish" => Ok(ShellType::Fish),
74            _ => Err(Autom8Error::ShellCompletion(format!(
75                "Unsupported shell: '{}'. Supported shells are: {}.",
76                name,
77                SUPPORTED_SHELLS.join(", ")
78            ))),
79        }
80    }
81}
82
83impl std::fmt::Display for ShellType {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", self.name())
86    }
87}
88
89/// Detect the user's shell from the `$SHELL` environment variable.
90///
91/// Parses the shell path (e.g., `/bin/zsh`, `/usr/bin/bash`) and returns
92/// the corresponding `ShellType`.
93///
94/// # Returns
95///
96/// * `Ok(ShellType)` - The detected shell type
97/// * `Err(Autom8Error)` - If `$SHELL` is not set or the shell is unsupported
98///
99/// # Examples
100///
101/// ```ignore
102/// // With $SHELL=/bin/zsh
103/// let shell = detect_shell()?;
104/// assert_eq!(shell, ShellType::Zsh);
105/// ```
106pub fn detect_shell() -> Result<ShellType> {
107    let shell_path = std::env::var("SHELL").map_err(|_| {
108        Autom8Error::ShellCompletion(
109            "$SHELL environment variable is not set. \
110             Please specify your shell manually or set the $SHELL variable."
111                .to_string(),
112        )
113    })?;
114
115    parse_shell_from_path(&shell_path)
116}
117
118/// Parse a shell type from a shell path.
119///
120/// Extracts the basename from the path and matches against supported shells.
121///
122/// # Arguments
123///
124/// * `shell_path` - Full path to the shell (e.g., `/bin/zsh`, `/usr/local/bin/fish`)
125///
126/// # Returns
127///
128/// * `Ok(ShellType)` - The detected shell type
129/// * `Err(Autom8Error)` - If the shell is not supported
130pub fn parse_shell_from_path(shell_path: &str) -> Result<ShellType> {
131    // Extract the basename from the path
132    let shell_name = std::path::Path::new(shell_path)
133        .file_name()
134        .and_then(|name| name.to_str())
135        .unwrap_or(shell_path);
136
137    match shell_name {
138        "bash" => Ok(ShellType::Bash),
139        "zsh" => Ok(ShellType::Zsh),
140        "fish" => Ok(ShellType::Fish),
141        _ => Err(Autom8Error::ShellCompletion(format!(
142            "Unsupported shell: '{}'. \
143             Supported shells are: bash, zsh, fish.",
144            shell_name
145        ))),
146    }
147}
148
149/// Get the installation path for completion scripts.
150///
151/// Returns the appropriate path for each shell:
152/// - **Bash**: `~/.local/share/bash-completion/completions/autom8` (XDG standard)
153///   Falls back to `~/.bash_completion.d/autom8` if XDG path doesn't exist
154/// - **Zsh**: `~/.zfunc/_autom8`
155/// - **Fish**: `~/.config/fish/completions/autom8.fish`
156///
157/// # Arguments
158///
159/// * `shell` - The target shell type
160///
161/// # Returns
162///
163/// * `Ok(PathBuf)` - The path where the completion script should be installed
164/// * `Err(Autom8Error)` - If the home directory cannot be determined
165pub fn get_completion_path(shell: ShellType) -> Result<PathBuf> {
166    let home = dirs::home_dir().ok_or_else(|| {
167        Autom8Error::ShellCompletion("Could not determine home directory".to_string())
168    })?;
169
170    let path = match shell {
171        ShellType::Bash => {
172            // Prefer XDG path, check if the directory exists
173            let xdg_path = home.join(".local/share/bash-completion/completions");
174            if xdg_path.exists() {
175                xdg_path.join("autom8")
176            } else {
177                // Fall back to traditional path
178                home.join(".bash_completion.d/autom8")
179            }
180        }
181        ShellType::Zsh => home.join(".zfunc/_autom8"),
182        ShellType::Fish => home.join(".config/fish/completions/autom8.fish"),
183    };
184
185    Ok(path)
186}
187
188/// Ensure the parent directory for a completion script exists.
189///
190/// Creates the parent directory (and all ancestors) if it doesn't exist.
191///
192/// # Arguments
193///
194/// * `path` - The path to the completion script
195///
196/// # Returns
197///
198/// * `Ok(())` - Directory exists or was created successfully
199/// * `Err(Autom8Error)` - If directory creation fails
200pub fn ensure_completion_dir(path: &std::path::Path) -> Result<()> {
201    if let Some(parent) = path.parent() {
202        if !parent.exists() {
203            std::fs::create_dir_all(parent).map_err(|e| {
204                Autom8Error::ShellCompletion(format!(
205                    "Failed to create completion directory '{}': {}",
206                    parent.display(),
207                    e
208                ))
209            })?;
210        }
211    }
212    Ok(())
213}
214
215/// Build the clap Command structure for completion generation.
216///
217/// This creates a command hierarchy that mirrors the CLI defined in `main.rs`,
218/// allowing clap_complete to generate accurate completion scripts.
219fn build_cli() -> Command {
220    Command::new("autom8")
221        .version(env!("CARGO_PKG_VERSION"))
222        .about("CLI automation tool for orchestrating Claude-powered development")
223        .arg(
224            clap::Arg::new("file")
225                .help("Path to a spec.md or spec.json file (shorthand for `run --spec <file>`)")
226                .value_hint(clap::ValueHint::FilePath),
227        )
228        .arg(
229            clap::Arg::new("verbose")
230                .short('v')
231                .long("verbose")
232                .help("Show full Claude output instead of spinner (useful for debugging)")
233                .global(true)
234                .action(clap::ArgAction::SetTrue),
235        )
236        .subcommand(
237            Command::new("run")
238                .about("Run the agent loop to implement spec stories")
239                .arg(
240                    clap::Arg::new("spec")
241                        .long("spec")
242                        .help("Path to the spec JSON or markdown file")
243                        .default_value("./spec.json")
244                        .value_hint(clap::ValueHint::FilePath),
245                )
246                .arg(
247                    clap::Arg::new("skip-review")
248                        .long("skip-review")
249                        .help("Skip the review loop and go directly to committing")
250                        .action(clap::ArgAction::SetTrue),
251                ),
252        )
253        .subcommand(
254            Command::new("status")
255                .about("Check the current run status")
256                .arg(
257                    clap::Arg::new("all")
258                        .short('a')
259                        .long("all")
260                        .help("Show status across all projects")
261                        .action(clap::ArgAction::SetTrue),
262                )
263                .arg(
264                    clap::Arg::new("global")
265                        .short('g')
266                        .long("global")
267                        .help("Show status across all projects (alias for --all)")
268                        .action(clap::ArgAction::SetTrue),
269                ),
270        )
271        .subcommand(Command::new("resume").about("Resume a failed or interrupted run"))
272        .subcommand(Command::new("clean").about("Clean up spec files from config directory"))
273        .subcommand(
274            Command::new("init")
275                .about("Initialize autom8 config directory structure for current project"),
276        )
277        .subcommand(
278            Command::new("projects").about("List all known projects in the config directory"),
279        )
280        .subcommand(Command::new("list").about("Show a tree view of all projects with status"))
281        .subcommand(
282            Command::new("describe")
283                .about("Show detailed information about a specific project")
284                .arg(
285                    clap::Arg::new("project_name")
286                        .help("The project name to describe (defaults to current directory)"),
287                ),
288        )
289        .subcommand(
290            Command::new("pr-review").about("Analyze PR review comments and fix real issues"),
291        )
292        .subcommand(
293            Command::new("monitor")
294                .about("Monitor autom8 activity across all projects (dashboard view)")
295                .arg(
296                    clap::Arg::new("project")
297                        .short('p')
298                        .long("project")
299                        .help("Filter to a specific project"),
300                )
301                .arg(
302                    clap::Arg::new("interval")
303                        .short('i')
304                        .long("interval")
305                        .help("Polling interval in seconds (default: 1)")
306                        .default_value("1"),
307                ),
308        )
309        .subcommand(Command::new("gui").about("Launch the native GUI to monitor autom8 activity"))
310        .subcommand(
311            Command::new("improve").about(
312                "Continue iterating on a feature with Claude using context from previous runs",
313            ),
314        )
315        .subcommand(
316            Command::new("config")
317                .about("View, modify, or reset configuration")
318                .arg(
319                    clap::Arg::new("global")
320                        .short('g')
321                        .long("global")
322                        .help("Show only the global configuration")
323                        .action(clap::ArgAction::SetTrue)
324                        .conflicts_with("project"),
325                )
326                .arg(
327                    clap::Arg::new("project")
328                        .short('p')
329                        .long("project")
330                        .help("Show only the project configuration")
331                        .action(clap::ArgAction::SetTrue)
332                        .conflicts_with("global"),
333                )
334                .subcommand(
335                    Command::new("set")
336                        .about("Set a configuration value")
337                        .arg(
338                            clap::Arg::new("global")
339                                .short('g')
340                                .long("global")
341                                .help("Set in global config instead of project config")
342                                .action(clap::ArgAction::SetTrue),
343                        )
344                        .arg(
345                            clap::Arg::new("key")
346                                .help("The configuration key to set")
347                                .required(true)
348                                .value_parser([
349                                    "review",
350                                    "commit",
351                                    "pull_request",
352                                    "worktree",
353                                    "worktree_path_pattern",
354                                    "worktree_cleanup",
355                                ]),
356                        )
357                        .arg(
358                            clap::Arg::new("value")
359                                .help("The value to set (true/false for boolean keys)")
360                                .required(true),
361                        ),
362                )
363                .subcommand(
364                    Command::new("reset")
365                        .about("Reset configuration to default values")
366                        .arg(
367                            clap::Arg::new("global")
368                                .short('g')
369                                .long("global")
370                                .help("Reset global config instead of project config")
371                                .action(clap::ArgAction::SetTrue),
372                        )
373                        .arg(
374                            clap::Arg::new("yes")
375                                .short('y')
376                                .long("yes")
377                                .help("Skip confirmation prompt")
378                                .action(clap::ArgAction::SetTrue),
379                        ),
380                ),
381        )
382}
383
384/// Generate a completion script for the specified shell.
385///
386/// Creates a completion script that includes all autom8 commands, subcommands,
387/// flags, and options. The script includes dynamic spec file completion that
388/// queries `~/.config/autom8/*/spec/` at completion time.
389///
390/// # Arguments
391///
392/// * `shell` - The target shell type
393///
394/// # Returns
395///
396/// The completion script as a String.
397pub fn generate_completion_script(shell: ShellType) -> String {
398    let mut cmd = build_cli();
399    let mut buf = Vec::new();
400    generate(shell.to_clap_shell(), &mut cmd, "autom8", &mut buf);
401    let base_script = String::from_utf8(buf).unwrap_or_default();
402
403    // Append dynamic spec completion functions
404    match shell {
405        ShellType::Bash => format!("{}\n{}", base_script, generate_bash_spec_completion()),
406        ShellType::Zsh => format!("{}\n{}", base_script, generate_zsh_spec_completion()),
407        ShellType::Fish => format!("{}\n{}", base_script, generate_fish_spec_completion()),
408    }
409}
410
411/// Generate bash-specific dynamic spec completion function.
412fn generate_bash_spec_completion() -> &'static str {
413    r#"
414# Dynamic spec file completion for autom8
415_autom8_spec_files() {
416    local config_dir="$HOME/.config/autom8"
417    local project_name=""
418    local specs=()
419
420    # Try to detect current project from git repo
421    if git rev-parse --git-dir &>/dev/null; then
422        project_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)
423    fi
424
425    # If in a project and that project has specs, show only those
426    if [[ -n "$project_name" && -d "$config_dir/$project_name/spec" ]]; then
427        local spec_dir="$config_dir/$project_name/spec"
428        if compgen -G "$spec_dir/*.json" &>/dev/null || compgen -G "$spec_dir/*.md" &>/dev/null; then
429            for f in "$spec_dir"/*.json "$spec_dir"/*.md; do
430                [[ -f "$f" ]] && specs+=("$(basename "$f")")
431            done
432        fi
433    fi
434
435    # If no project specs found, show specs from all projects
436    if [[ ${#specs[@]} -eq 0 && -d "$config_dir" ]]; then
437        for project_dir in "$config_dir"/*/spec; do
438            if [[ -d "$project_dir" ]]; then
439                for f in "$project_dir"/*.json "$project_dir"/*.md; do
440                    [[ -f "$f" ]] && specs+=("$(basename "$f")")
441                done
442            fi
443        done
444    fi
445
446    # Remove duplicates and sort
447    printf '%s\n' "${specs[@]}" | sort -u
448}
449
450# Override completion for --spec flag and positional arguments
451_autom8_complete() {
452    local cur prev words cword
453    _init_completion || return
454
455    # Check if we're completing the --spec flag value
456    if [[ "$prev" == "--spec" ]]; then
457        COMPREPLY=($(compgen -W "$(_autom8_spec_files)" -- "$cur"))
458        return
459    fi
460
461    # Check if completing first positional arg (not a subcommand)
462    if [[ $cword -eq 1 && "$cur" != -* ]]; then
463        # Get subcommands
464        local subcommands="run status resume clean config init projects list describe pr-review monitor gui improve"
465        # Get spec files
466        local specs=$(_autom8_spec_files)
467        COMPREPLY=($(compgen -W "$subcommands $specs" -- "$cur"))
468        return
469    fi
470
471    # Config key completion
472    if [[ "${words[1]}" == "config" && "${words[2]}" == "set" ]]; then
473        if [[ $cword -eq 3 ]]; then
474            # Complete config keys
475            COMPREPLY=($(compgen -W "review commit pull_request worktree worktree_path_pattern worktree_cleanup" -- "$cur"))
476            return
477        elif [[ $cword -eq 4 && "${words[3]}" != "worktree_path_pattern" ]]; then
478            # Complete boolean values for non-string keys
479            COMPREPLY=($(compgen -W "true false" -- "$cur"))
480            return
481        fi
482    fi
483
484    # Fall back to default autom8 completion
485    _autom8 "$@"
486}
487
488complete -F _autom8_complete autom8
489"#
490}
491
492/// Generate zsh-specific dynamic spec completion function.
493fn generate_zsh_spec_completion() -> &'static str {
494    r#"
495# Dynamic spec file completion for autom8
496_autom8_spec_files() {
497    local config_dir="$HOME/.config/autom8"
498    local project_name=""
499    local -a specs
500
501    # Try to detect current project from git repo
502    if git rev-parse --git-dir &>/dev/null; then
503        project_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null)
504    fi
505
506    # If in a project and that project has specs, show only those
507    if [[ -n "$project_name" && -d "$config_dir/$project_name/spec" ]]; then
508        local spec_dir="$config_dir/$project_name/spec"
509        specs=(${(f)"$(ls "$spec_dir"/*.json "$spec_dir"/*.md 2>/dev/null | xargs -n1 basename 2>/dev/null)"})
510    fi
511
512    # If no project specs found, show specs from all projects
513    if [[ ${#specs[@]} -eq 0 && -d "$config_dir" ]]; then
514        specs=(${(f)"$(ls "$config_dir"/*/spec/*.json "$config_dir"/*/spec/*.md 2>/dev/null | xargs -n1 basename 2>/dev/null)"})
515    fi
516
517    # Remove duplicates and print
518    printf '%s\n' "${(u)specs[@]}"
519}
520
521# Override _autom8 to add spec file completion
522if (( $+functions[_autom8_original] )); then
523    : # Already patched
524else
525    # Save original function if it exists
526    if (( $+functions[_autom8] )); then
527        functions[_autom8_original]=$functions[_autom8]
528    fi
529
530    _autom8() {
531        local curcontext="$curcontext" state line
532        typeset -A opt_args
533
534        # Check if completing --spec value
535        if [[ "${words[$CURRENT-1]}" == "--spec" ]]; then
536            local -a spec_files
537            spec_files=(${(f)"$(_autom8_spec_files)"})
538            _describe 'spec file' spec_files
539            return
540        fi
541
542        # Check if completing first positional argument
543        if [[ $CURRENT -eq 2 && "${words[2]}" != -* ]]; then
544            local -a completions
545            local -a spec_files
546            spec_files=(${(f)"$(_autom8_spec_files)"})
547            completions=(
548                'run:Run the agent loop to implement spec stories'
549                'status:Check the current run status'
550                'resume:Resume a failed or interrupted run'
551                'clean:Clean up spec files from config directory'
552                'config:View, modify, or reset configuration'
553                'init:Initialize autom8 config directory structure'
554                'projects:List all known projects'
555                'list:Show a tree view of all projects with status'
556                'describe:Show detailed information about a specific project'
557                'pr-review:Analyze PR review comments and fix real issues'
558                'monitor:Monitor autom8 activity across all projects'
559                'gui:Launch the native GUI to monitor autom8 activity'
560                'improve:Continue iterating on a feature with Claude using context from previous runs'
561            )
562            for spec in "${spec_files[@]}"; do
563                [[ -n "$spec" ]] && completions+=("$spec:Spec file")
564            done
565            _describe 'command or spec' completions
566            return
567        fi
568
569        # Config set key/value completion
570        if [[ "${words[2]}" == "config" && "${words[3]}" == "set" ]]; then
571            if [[ $CURRENT -eq 4 ]]; then
572                local -a config_keys
573                config_keys=(
574                    'review:Enable code review step'
575                    'commit:Enable auto-commit'
576                    'pull_request:Enable auto-PR creation'
577                    'worktree:Enable worktree mode'
578                    'worktree_path_pattern:Pattern for worktree names'
579                    'worktree_cleanup:Auto-cleanup worktrees'
580                )
581                _describe 'config key' config_keys
582                return
583            elif [[ $CURRENT -eq 5 && "${words[4]}" != "worktree_path_pattern" ]]; then
584                local -a bool_values
585                bool_values=('true' 'false')
586                _describe 'value' bool_values
587                return
588            fi
589        fi
590
591        # Fall back to original completion if it exists
592        if (( $+functions[_autom8_original] )); then
593            _autom8_original "$@"
594        fi
595    }
596
597    compdef _autom8 autom8
598fi
599"#
600}
601
602/// Generate fish-specific dynamic spec completion function.
603fn generate_fish_spec_completion() -> &'static str {
604    r#"
605# Dynamic spec file completion for autom8
606function __autom8_spec_files
607    set -l config_dir "$HOME/.config/autom8"
608    set -l project_name ""
609
610    # Try to detect current project from git repo
611    if git rev-parse --git-dir &>/dev/null
612        set project_name (basename (git rev-parse --show-toplevel 2>/dev/null) 2>/dev/null)
613    end
614
615    # If in a project and that project has specs, show only those
616    if test -n "$project_name"; and test -d "$config_dir/$project_name/spec"
617        set -l spec_dir "$config_dir/$project_name/spec"
618        for f in $spec_dir/*.json $spec_dir/*.md
619            if test -f "$f"
620                basename "$f"
621            end
622        end
623        return
624    end
625
626    # If no project specs found, show specs from all projects
627    if test -d "$config_dir"
628        for spec_dir in $config_dir/*/spec
629            if test -d "$spec_dir"
630                for f in $spec_dir/*.json $spec_dir/*.md
631                    if test -f "$f"
632                        basename "$f"
633                    end
634                end
635            end
636        end | sort -u
637    end
638end
639
640# Add spec file completions for --spec flag
641complete -c autom8 -l spec -xa '(__autom8_spec_files)'
642
643# Add spec file completions for positional argument (first arg that's not a flag)
644complete -c autom8 -n '__fish_is_first_arg; and not __fish_seen_subcommand_from run status resume clean config init projects list describe pr-review monitor gui improve' -xa '(__autom8_spec_files)'
645
646# Config set key completion
647complete -c autom8 -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from set; and test (count (commandline -opc)) -eq 3' -xa 'review commit pull_request worktree worktree_path_pattern worktree_cleanup'
648
649# Config set value completion (true/false for boolean keys)
650complete -c autom8 -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from set; and test (count (commandline -opc)) -eq 4; and not string match -q worktree_path_pattern (commandline -opc)[-1]' -xa 'true false'
651"#
652}
653
654/// Output the completion script for a shell to stdout.
655///
656/// This is used by the hidden `completions` subcommand to let power users
657/// manually manage their completion scripts.
658///
659/// # Arguments
660///
661/// * `shell` - The target shell type
662pub fn print_completion_script(shell: ShellType) {
663    print!("{}", generate_completion_script(shell));
664}
665
666/// Write a completion script to the specified path.
667///
668/// Creates parent directories if needed, then writes the completion script.
669///
670/// # Arguments
671///
672/// * `shell` - The target shell type
673/// * `path` - The destination path for the script
674///
675/// # Returns
676///
677/// * `Ok(())` - Script written successfully
678/// * `Err(Autom8Error)` - If directory creation or file write fails
679pub fn write_completion_script(shell: ShellType, path: &std::path::Path) -> Result<()> {
680    // Ensure parent directory exists
681    ensure_completion_dir(path)?;
682
683    // Generate and write the script
684    let script = generate_completion_script(shell);
685    let mut file = std::fs::File::create(path).map_err(|e| {
686        Autom8Error::ShellCompletion(format!(
687            "Failed to create completion file '{}': {}",
688            path.display(),
689            e
690        ))
691    })?;
692
693    file.write_all(script.as_bytes()).map_err(|e| {
694        Autom8Error::ShellCompletion(format!(
695            "Failed to write completion script to '{}': {}",
696            path.display(),
697            e
698        ))
699    })?;
700
701    Ok(())
702}
703
704/// Result of completion installation.
705#[derive(Debug)]
706pub struct CompletionInstallResult {
707    /// The shell that completions were installed for.
708    pub shell: ShellType,
709    /// The path where the completion script was written.
710    pub path: PathBuf,
711    /// Additional setup instructions for the user, if any.
712    pub setup_instructions: Option<String>,
713}
714
715/// Check if zsh fpath includes ~/.zfunc.
716///
717/// Returns true if ~/.zfunc is already configured in fpath.
718fn is_zfunc_in_fpath() -> bool {
719    // Check if FPATH environment variable includes .zfunc
720    if let Ok(fpath) = std::env::var("FPATH") {
721        let home = dirs::home_dir().unwrap_or_default();
722        let zfunc = home.join(".zfunc");
723        let zfunc_str = zfunc.to_string_lossy();
724
725        for path in fpath.split(':') {
726            if path == zfunc_str || path == "~/.zfunc" {
727                return true;
728            }
729        }
730    }
731    false
732}
733
734/// Get setup instructions for zsh if ~/.zfunc is not in fpath.
735fn get_zsh_setup_instructions() -> Option<String> {
736    if is_zfunc_in_fpath() {
737        None
738    } else {
739        Some(
740            "To enable completions, add the following to your ~/.zshrc:\n\n\
741             fpath=(~/.zfunc $fpath)\n\
742             autoload -Uz compinit && compinit\n\n\
743             Then restart your shell or run: source ~/.zshrc"
744                .to_string(),
745        )
746    }
747}
748
749/// Get setup instructions for bash.
750fn get_bash_setup_instructions(path: &std::path::Path) -> Option<String> {
751    // Check if bash-completion is likely set up (XDG location)
752    if path
753        .to_string_lossy()
754        .contains("bash-completion/completions")
755    {
756        // XDG location should be auto-loaded
757        Some("Restart your shell to enable completions.".to_string())
758    } else {
759        // Non-XDG location needs manual sourcing
760        Some(format!(
761            "To enable completions, add to your ~/.bashrc:\n\n\
762             source {}\n\n\
763             Then restart your shell or run: source ~/.bashrc",
764            path.display()
765        ))
766    }
767}
768
769/// Install shell completions for the current user.
770///
771/// Detects the user's shell from `$SHELL`, generates the appropriate
772/// completion script, and writes it to the correct location.
773///
774/// # Returns
775///
776/// * `Ok(CompletionInstallResult)` - Installation succeeded
777/// * `Err(Autom8Error)` - If shell detection or file writing fails
778///
779/// # Example
780///
781/// ```ignore
782/// match install_completions() {
783///     Ok(result) => {
784///         println!("Installed {} completions to {}", result.shell, result.path.display());
785///         if let Some(instructions) = result.setup_instructions {
786///             println!("{}", instructions);
787///         }
788///     }
789///     Err(e) => eprintln!("Failed: {}", e),
790/// }
791/// ```
792pub fn install_completions() -> Result<CompletionInstallResult> {
793    let shell = detect_shell()?;
794    let path = get_completion_path(shell)?;
795
796    write_completion_script(shell, &path)?;
797
798    let setup_instructions = match shell {
799        ShellType::Zsh => get_zsh_setup_instructions(),
800        ShellType::Bash => get_bash_setup_instructions(&path),
801        ShellType::Fish => {
802            // Fish auto-loads from ~/.config/fish/completions/
803            Some("Restart your shell to enable completions.".to_string())
804        }
805    };
806
807    Ok(CompletionInstallResult {
808        shell,
809        path,
810        setup_instructions,
811    })
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    // ======================================================================
819    // Tests for US-001: Shell detection
820    // ======================================================================
821
822    #[test]
823    fn test_parse_shell_bash() {
824        assert_eq!(parse_shell_from_path("/bin/bash").unwrap(), ShellType::Bash);
825        assert_eq!(
826            parse_shell_from_path("/usr/bin/bash").unwrap(),
827            ShellType::Bash
828        );
829        assert_eq!(
830            parse_shell_from_path("/usr/local/bin/bash").unwrap(),
831            ShellType::Bash
832        );
833    }
834
835    #[test]
836    fn test_parse_shell_zsh() {
837        assert_eq!(parse_shell_from_path("/bin/zsh").unwrap(), ShellType::Zsh);
838        assert_eq!(
839            parse_shell_from_path("/usr/bin/zsh").unwrap(),
840            ShellType::Zsh
841        );
842        assert_eq!(
843            parse_shell_from_path("/usr/local/bin/zsh").unwrap(),
844            ShellType::Zsh
845        );
846    }
847
848    #[test]
849    fn test_parse_shell_fish() {
850        assert_eq!(parse_shell_from_path("/bin/fish").unwrap(), ShellType::Fish);
851        assert_eq!(
852            parse_shell_from_path("/usr/bin/fish").unwrap(),
853            ShellType::Fish
854        );
855        assert_eq!(
856            parse_shell_from_path("/usr/local/bin/fish").unwrap(),
857            ShellType::Fish
858        );
859        assert_eq!(
860            parse_shell_from_path("/opt/homebrew/bin/fish").unwrap(),
861            ShellType::Fish
862        );
863    }
864
865    #[test]
866    fn test_parse_shell_unsupported() {
867        let result = parse_shell_from_path("/bin/sh");
868        assert!(result.is_err());
869        let err = result.unwrap_err().to_string();
870        assert!(err.contains("Unsupported shell"));
871        assert!(err.contains("sh"));
872
873        let result = parse_shell_from_path("/bin/tcsh");
874        assert!(result.is_err());
875        let err = result.unwrap_err().to_string();
876        assert!(err.contains("tcsh"));
877    }
878
879    #[test]
880    fn test_parse_shell_unsupported_contains_supported_list() {
881        let result = parse_shell_from_path("/bin/ksh");
882        assert!(result.is_err());
883        let err = result.unwrap_err().to_string();
884        assert!(err.contains("bash"));
885        assert!(err.contains("zsh"));
886        assert!(err.contains("fish"));
887    }
888
889    // ======================================================================
890    // Tests for US-001: Path resolution
891    // ======================================================================
892
893    #[test]
894    fn test_completion_path_bash() {
895        let path = get_completion_path(ShellType::Bash).unwrap();
896        let path_str = path.to_string_lossy();
897
898        // Should end with the expected filename
899        assert!(path_str.ends_with("autom8"));
900
901        // Should be in one of the two expected directories
902        assert!(
903            path_str.contains("bash-completion/completions")
904                || path_str.contains(".bash_completion.d"),
905            "Bash path should be in XDG or traditional location: {}",
906            path_str
907        );
908    }
909
910    #[test]
911    fn test_completion_path_zsh() {
912        let path = get_completion_path(ShellType::Zsh).unwrap();
913        let path_str = path.to_string_lossy();
914
915        assert!(
916            path_str.ends_with(".zfunc/_autom8"),
917            "Zsh path should end with .zfunc/_autom8: {}",
918            path_str
919        );
920    }
921
922    #[test]
923    fn test_completion_path_fish() {
924        let path = get_completion_path(ShellType::Fish).unwrap();
925        let path_str = path.to_string_lossy();
926
927        assert!(
928            path_str.ends_with(".config/fish/completions/autom8.fish"),
929            "Fish path should end with .config/fish/completions/autom8.fish: {}",
930            path_str
931        );
932    }
933
934    // ======================================================================
935    // Tests for US-001: Script generation
936    // ======================================================================
937
938    #[test]
939    fn test_generate_completion_script_bash() {
940        let script = generate_completion_script(ShellType::Bash);
941
942        // Should contain autom8 command
943        assert!(script.contains("autom8"), "Script should reference autom8");
944
945        // Should contain subcommands
946        assert!(script.contains("run"), "Script should include run command");
947        assert!(
948            script.contains("status"),
949            "Script should include status command"
950        );
951        assert!(
952            script.contains("resume"),
953            "Script should include resume command"
954        );
955        assert!(
956            script.contains("clean"),
957            "Script should include clean command"
958        );
959        assert!(
960            script.contains("init"),
961            "Script should include init command"
962        );
963        assert!(
964            script.contains("projects"),
965            "Script should include projects command"
966        );
967        assert!(
968            script.contains("list"),
969            "Script should include list command"
970        );
971        assert!(
972            script.contains("describe"),
973            "Script should include describe command"
974        );
975        assert!(
976            script.contains("pr-review"),
977            "Script should include pr-review command"
978        );
979        assert!(
980            script.contains("monitor"),
981            "Script should include monitor command"
982        );
983    }
984
985    #[test]
986    fn test_generate_completion_script_zsh() {
987        let script = generate_completion_script(ShellType::Zsh);
988
989        // Should be a valid zsh completion script
990        assert!(
991            script.contains("#compdef autom8"),
992            "Zsh script should start with #compdef"
993        );
994
995        // Should contain subcommands
996        assert!(script.contains("run"));
997        assert!(script.contains("status"));
998        assert!(script.contains("init"));
999    }
1000
1001    #[test]
1002    fn test_generate_completion_script_fish() {
1003        let script = generate_completion_script(ShellType::Fish);
1004
1005        // Should be a valid fish completion script
1006        assert!(
1007            script.contains("complete"),
1008            "Fish script should contain complete commands"
1009        );
1010        assert!(
1011            script.contains("autom8"),
1012            "Fish script should reference autom8"
1013        );
1014    }
1015
1016    #[test]
1017    fn test_generate_completion_script_contains_flags() {
1018        let script = generate_completion_script(ShellType::Bash);
1019
1020        // Should contain common flags
1021        assert!(
1022            script.contains("verbose") || script.contains("-v"),
1023            "Script should include verbose flag"
1024        );
1025        assert!(script.contains("spec"), "Script should include spec option");
1026        assert!(
1027            script.contains("skip-review"),
1028            "Script should include skip-review flag"
1029        );
1030        assert!(
1031            script.contains("all") || script.contains("-a"),
1032            "Script should include all flag"
1033        );
1034        assert!(
1035            script.contains("global") || script.contains("-g"),
1036            "Script should include global flag"
1037        );
1038        assert!(
1039            script.contains("project") || script.contains("-p"),
1040            "Script should include project flag"
1041        );
1042        assert!(
1043            script.contains("interval") || script.contains("-i"),
1044            "Script should include interval flag"
1045        );
1046    }
1047
1048    // ======================================================================
1049    // Tests for US-001: ShellType utilities
1050    // ======================================================================
1051
1052    #[test]
1053    fn test_shell_type_name() {
1054        assert_eq!(ShellType::Bash.name(), "bash");
1055        assert_eq!(ShellType::Zsh.name(), "zsh");
1056        assert_eq!(ShellType::Fish.name(), "fish");
1057    }
1058
1059    #[test]
1060    fn test_shell_type_display() {
1061        assert_eq!(format!("{}", ShellType::Bash), "bash");
1062        assert_eq!(format!("{}", ShellType::Zsh), "zsh");
1063        assert_eq!(format!("{}", ShellType::Fish), "fish");
1064    }
1065
1066    #[test]
1067    fn test_shell_type_to_clap_shell() {
1068        assert_eq!(ShellType::Bash.to_clap_shell(), Shell::Bash);
1069        assert_eq!(ShellType::Zsh.to_clap_shell(), Shell::Zsh);
1070        assert_eq!(ShellType::Fish.to_clap_shell(), Shell::Fish);
1071    }
1072
1073    // ======================================================================
1074    // Tests for US-001: Directory creation
1075    // ======================================================================
1076
1077    #[test]
1078    fn test_ensure_completion_dir_with_existing_parent() {
1079        use tempfile::TempDir;
1080
1081        let temp_dir = TempDir::new().unwrap();
1082        let path = temp_dir.path().join("autom8");
1083
1084        // Parent already exists
1085        let result = ensure_completion_dir(&path);
1086        assert!(result.is_ok());
1087    }
1088
1089    #[test]
1090    fn test_ensure_completion_dir_creates_parent() {
1091        use tempfile::TempDir;
1092
1093        let temp_dir = TempDir::new().unwrap();
1094        let path = temp_dir.path().join("new_dir").join("autom8");
1095
1096        // Parent doesn't exist
1097        assert!(!path.parent().unwrap().exists());
1098
1099        let result = ensure_completion_dir(&path);
1100        assert!(result.is_ok());
1101        assert!(path.parent().unwrap().exists());
1102    }
1103
1104    #[test]
1105    fn test_ensure_completion_dir_creates_nested_parents() {
1106        use tempfile::TempDir;
1107
1108        let temp_dir = TempDir::new().unwrap();
1109        let path = temp_dir.path().join("a").join("b").join("c").join("autom8");
1110
1111        let result = ensure_completion_dir(&path);
1112        assert!(result.is_ok());
1113        assert!(path.parent().unwrap().exists());
1114    }
1115
1116    // ======================================================================
1117    // Tests for US-001: Write completion script
1118    // ======================================================================
1119
1120    #[test]
1121    fn test_write_completion_script_creates_file() {
1122        use tempfile::TempDir;
1123
1124        let temp_dir = TempDir::new().unwrap();
1125        let path = temp_dir.path().join("autom8");
1126
1127        let result = write_completion_script(ShellType::Bash, &path);
1128        assert!(result.is_ok());
1129        assert!(path.exists());
1130
1131        // Verify content
1132        let content = std::fs::read_to_string(&path).unwrap();
1133        assert!(content.contains("autom8"));
1134    }
1135
1136    #[test]
1137    fn test_write_completion_script_creates_parent_dirs() {
1138        use tempfile::TempDir;
1139
1140        let temp_dir = TempDir::new().unwrap();
1141        let path = temp_dir.path().join("nested").join("dir").join("autom8");
1142
1143        let result = write_completion_script(ShellType::Zsh, &path);
1144        assert!(result.is_ok());
1145        assert!(path.exists());
1146    }
1147
1148    // ======================================================================
1149    // Tests for US-002: Completion installation from init
1150    // ======================================================================
1151
1152    #[test]
1153    fn test_completion_install_result_has_expected_fields() {
1154        // Verify CompletionInstallResult struct has all expected fields
1155        let result = CompletionInstallResult {
1156            shell: ShellType::Zsh,
1157            path: PathBuf::from("/tmp/test"),
1158            setup_instructions: Some("Test instructions".to_string()),
1159        };
1160
1161        assert_eq!(result.shell, ShellType::Zsh);
1162        assert_eq!(result.path, PathBuf::from("/tmp/test"));
1163        assert_eq!(
1164            result.setup_instructions,
1165            Some("Test instructions".to_string())
1166        );
1167    }
1168
1169    #[test]
1170    fn test_completion_install_result_without_setup_instructions() {
1171        // Verify setup_instructions can be None
1172        let result = CompletionInstallResult {
1173            shell: ShellType::Fish,
1174            path: PathBuf::from("/tmp/test"),
1175            setup_instructions: None,
1176        };
1177
1178        assert!(result.setup_instructions.is_none());
1179    }
1180
1181    #[test]
1182    fn test_zsh_setup_instructions_contain_fpath() {
1183        // When fpath check would fail (not in FPATH), instructions should mention fpath
1184        // We can't easily test the actual check without modifying FPATH,
1185        // but we can test the instruction content
1186        let instructions = "fpath=(~/.zfunc $fpath)\nautoload -Uz compinit && compinit";
1187        assert!(instructions.contains("fpath"));
1188        assert!(instructions.contains("compinit"));
1189        assert!(instructions.contains("autoload"));
1190    }
1191
1192    #[test]
1193    fn test_bash_setup_instructions_for_xdg_path() {
1194        let path = PathBuf::from("/home/user/.local/share/bash-completion/completions/autom8");
1195        let instructions = get_bash_setup_instructions(&path);
1196
1197        assert!(instructions.is_some());
1198        let instructions = instructions.unwrap();
1199        // XDG path should just say restart shell
1200        assert!(instructions.contains("Restart"));
1201    }
1202
1203    #[test]
1204    fn test_bash_setup_instructions_for_non_xdg_path() {
1205        let path = PathBuf::from("/home/user/.bash_completion.d/autom8");
1206        let instructions = get_bash_setup_instructions(&path);
1207
1208        assert!(instructions.is_some());
1209        let instructions = instructions.unwrap();
1210        // Non-XDG path should mention sourcing
1211        assert!(instructions.contains("source"));
1212        assert!(instructions.contains(&path.display().to_string()));
1213    }
1214
1215    #[test]
1216    fn test_write_completion_script_overwrites_existing() {
1217        use tempfile::TempDir;
1218
1219        let temp_dir = TempDir::new().unwrap();
1220        let path = temp_dir.path().join("autom8");
1221
1222        // Write initial script
1223        let result = write_completion_script(ShellType::Bash, &path);
1224        assert!(result.is_ok());
1225
1226        let content1 = std::fs::read_to_string(&path).unwrap();
1227
1228        // Write again (should overwrite, not fail)
1229        let result = write_completion_script(ShellType::Bash, &path);
1230        assert!(result.is_ok());
1231
1232        let content2 = std::fs::read_to_string(&path).unwrap();
1233
1234        // Content should be the same (idempotent)
1235        assert_eq!(content1, content2);
1236    }
1237
1238    #[test]
1239    fn test_write_completion_script_overwrites_different_shell() {
1240        use tempfile::TempDir;
1241
1242        let temp_dir = TempDir::new().unwrap();
1243        let path = temp_dir.path().join("autom8");
1244
1245        // Write bash script
1246        write_completion_script(ShellType::Bash, &path).unwrap();
1247        let bash_content = std::fs::read_to_string(&path).unwrap();
1248
1249        // Overwrite with zsh script
1250        write_completion_script(ShellType::Zsh, &path).unwrap();
1251        let zsh_content = std::fs::read_to_string(&path).unwrap();
1252
1253        // Content should be different
1254        assert_ne!(bash_content, zsh_content);
1255        assert!(zsh_content.contains("#compdef"));
1256    }
1257
1258    #[test]
1259    fn test_install_completions_available_as_public_api() {
1260        // Verify install_completions is a public function
1261        // (This test verifies the API exists; actual installation depends on env)
1262        let _: fn() -> Result<CompletionInstallResult> = install_completions;
1263    }
1264
1265    #[test]
1266    fn test_completion_install_result_shell_display() {
1267        // Verify shell type displays correctly for messages
1268        let result = CompletionInstallResult {
1269            shell: ShellType::Zsh,
1270            path: PathBuf::from("/home/user/.zfunc/_autom8"),
1271            setup_instructions: None,
1272        };
1273
1274        let message = format!(
1275            "Installed {} completions to {}",
1276            result.shell,
1277            result.path.display()
1278        );
1279        assert!(message.contains("zsh"));
1280        assert!(message.contains("_autom8"));
1281    }
1282
1283    #[test]
1284    fn test_get_zsh_setup_instructions_content() {
1285        // Test the content of zsh setup instructions (assuming fpath not set)
1286        // Since we can't easily manipulate FPATH in tests, we test the instruction format
1287        let expected_content = "fpath=(~/.zfunc $fpath)";
1288
1289        // The instructions should include this if zfunc is not in fpath
1290        // We can verify the helper function produces valid instructions
1291        let home = dirs::home_dir().unwrap();
1292        let zfunc_path = home.join(".zfunc/_autom8");
1293        assert!(zfunc_path.to_string_lossy().contains(".zfunc"));
1294
1295        // Verify expected content format
1296        assert!(expected_content.contains("fpath"));
1297        assert!(expected_content.contains("$fpath"));
1298    }
1299
1300    // ======================================================================
1301    // Tests for US-003: Dynamic spec file completion and utility subcommand
1302    // ======================================================================
1303
1304    #[test]
1305    fn test_shell_type_from_name_bash() {
1306        let result = ShellType::from_name("bash");
1307        assert!(result.is_ok());
1308        assert_eq!(result.unwrap(), ShellType::Bash);
1309    }
1310
1311    #[test]
1312    fn test_shell_type_from_name_zsh() {
1313        let result = ShellType::from_name("zsh");
1314        assert!(result.is_ok());
1315        assert_eq!(result.unwrap(), ShellType::Zsh);
1316    }
1317
1318    #[test]
1319    fn test_shell_type_from_name_fish() {
1320        let result = ShellType::from_name("fish");
1321        assert!(result.is_ok());
1322        assert_eq!(result.unwrap(), ShellType::Fish);
1323    }
1324
1325    #[test]
1326    fn test_shell_type_from_name_case_insensitive() {
1327        // Should handle uppercase
1328        assert_eq!(ShellType::from_name("BASH").unwrap(), ShellType::Bash);
1329        assert_eq!(ShellType::from_name("ZSH").unwrap(), ShellType::Zsh);
1330        assert_eq!(ShellType::from_name("FISH").unwrap(), ShellType::Fish);
1331
1332        // Should handle mixed case
1333        assert_eq!(ShellType::from_name("Bash").unwrap(), ShellType::Bash);
1334        assert_eq!(ShellType::from_name("Zsh").unwrap(), ShellType::Zsh);
1335        assert_eq!(ShellType::from_name("Fish").unwrap(), ShellType::Fish);
1336    }
1337
1338    #[test]
1339    fn test_shell_type_from_name_invalid() {
1340        let result = ShellType::from_name("powershell");
1341        assert!(result.is_err());
1342        let err = result.unwrap_err().to_string();
1343        assert!(err.contains("Unsupported shell"));
1344        assert!(err.contains("powershell"));
1345    }
1346
1347    #[test]
1348    fn test_shell_type_from_name_error_lists_supported_shells() {
1349        let result = ShellType::from_name("invalid");
1350        assert!(result.is_err());
1351        let err = result.unwrap_err().to_string();
1352        assert!(err.contains("bash"));
1353        assert!(err.contains("zsh"));
1354        assert!(err.contains("fish"));
1355    }
1356
1357    #[test]
1358    fn test_supported_shells_constant() {
1359        assert!(SUPPORTED_SHELLS.contains(&"bash"));
1360        assert!(SUPPORTED_SHELLS.contains(&"zsh"));
1361        assert!(SUPPORTED_SHELLS.contains(&"fish"));
1362        assert_eq!(SUPPORTED_SHELLS.len(), 3);
1363    }
1364
1365    #[test]
1366    fn test_bash_completion_includes_dynamic_spec_function() {
1367        let script = generate_completion_script(ShellType::Bash);
1368
1369        // Should include the dynamic spec completion function
1370        assert!(
1371            script.contains("_autom8_spec_files"),
1372            "Bash script should include _autom8_spec_files function"
1373        );
1374
1375        // Should reference the config directory
1376        assert!(
1377            script.contains(".config/autom8"),
1378            "Bash script should reference the config directory"
1379        );
1380
1381        // Should check for .json and .md files
1382        assert!(
1383            script.contains(".json") && script.contains(".md"),
1384            "Bash script should check for both .json and .md files"
1385        );
1386
1387        // Should include git project detection
1388        assert!(
1389            script.contains("git rev-parse"),
1390            "Bash script should include git project detection"
1391        );
1392    }
1393
1394    #[test]
1395    fn test_zsh_completion_includes_dynamic_spec_function() {
1396        let script = generate_completion_script(ShellType::Zsh);
1397
1398        // Should include the dynamic spec completion function
1399        assert!(
1400            script.contains("_autom8_spec_files"),
1401            "Zsh script should include _autom8_spec_files function"
1402        );
1403
1404        // Should reference the config directory
1405        assert!(
1406            script.contains(".config/autom8"),
1407            "Zsh script should reference the config directory"
1408        );
1409
1410        // Should check for .json and .md files
1411        assert!(
1412            script.contains(".json") && script.contains(".md"),
1413            "Zsh script should check for both .json and .md files"
1414        );
1415
1416        // Should include git project detection
1417        assert!(
1418            script.contains("git rev-parse"),
1419            "Zsh script should include git project detection"
1420        );
1421    }
1422
1423    #[test]
1424    fn test_fish_completion_includes_dynamic_spec_function() {
1425        let script = generate_completion_script(ShellType::Fish);
1426
1427        // Should include the dynamic spec completion function
1428        assert!(
1429            script.contains("__autom8_spec_files"),
1430            "Fish script should include __autom8_spec_files function"
1431        );
1432
1433        // Should reference the config directory
1434        assert!(
1435            script.contains(".config/autom8"),
1436            "Fish script should reference the config directory"
1437        );
1438
1439        // Should check for .json and .md files
1440        assert!(
1441            script.contains(".json") && script.contains(".md"),
1442            "Fish script should check for both .json and .md files"
1443        );
1444
1445        // Should include git project detection
1446        assert!(
1447            script.contains("git rev-parse"),
1448            "Fish script should include git project detection"
1449        );
1450    }
1451
1452    #[test]
1453    fn test_bash_completion_includes_spec_flag_completion() {
1454        let script = generate_completion_script(ShellType::Bash);
1455
1456        // Should have completion for --spec flag
1457        assert!(
1458            script.contains("--spec"),
1459            "Bash script should include --spec flag completion"
1460        );
1461    }
1462
1463    #[test]
1464    fn test_zsh_completion_includes_spec_flag_completion() {
1465        let script = generate_completion_script(ShellType::Zsh);
1466
1467        // Should have completion for --spec flag
1468        assert!(
1469            script.contains("--spec"),
1470            "Zsh script should include --spec flag completion"
1471        );
1472    }
1473
1474    #[test]
1475    fn test_fish_completion_includes_spec_flag_completion() {
1476        let script = generate_completion_script(ShellType::Fish);
1477
1478        // Should have completion for --spec flag
1479        assert!(
1480            script.contains("--spec") || script.contains("-l spec"),
1481            "Fish script should include --spec flag completion"
1482        );
1483    }
1484
1485    #[test]
1486    fn test_bash_completion_includes_subcommands_in_first_arg() {
1487        let script = generate_completion_script(ShellType::Bash);
1488
1489        // Should list subcommands for first argument completion
1490        assert!(
1491            script.contains("run") && script.contains("status") && script.contains("resume"),
1492            "Bash script should include subcommands for first arg completion"
1493        );
1494    }
1495
1496    #[test]
1497    fn test_print_completion_script_exists() {
1498        // Verify the function exists and is callable
1499        let _: fn(ShellType) = print_completion_script;
1500    }
1501
1502    // ======================================================================
1503    // Tests for config subcommand completion
1504    // ======================================================================
1505
1506    #[test]
1507    fn test_bash_completion_includes_config_subcommand() {
1508        let script = generate_completion_script(ShellType::Bash);
1509
1510        // Should include config subcommand
1511        assert!(
1512            script.contains("autom8__config"),
1513            "Bash script should include config subcommand"
1514        );
1515
1516        // Should include config in subcommands list
1517        assert!(
1518            script.contains("run status resume clean config init projects list describe pr-review monitor gui improve"),
1519            "Bash script should include all commands in dynamic subcommands list"
1520        );
1521    }
1522
1523    #[test]
1524    fn test_zsh_completion_includes_config_subcommand() {
1525        let script = generate_completion_script(ShellType::Zsh);
1526
1527        // Should include config subcommand with description
1528        assert!(
1529            script.contains("'config:View, modify, or reset configuration'"),
1530            "Zsh script should include config subcommand with description"
1531        );
1532    }
1533
1534    #[test]
1535    fn test_fish_completion_includes_config_subcommand() {
1536        let script = generate_completion_script(ShellType::Fish);
1537
1538        // Should include config subcommand
1539        assert!(
1540            script.contains("-a \"config\"") || script.contains("config"),
1541            "Fish script should include config subcommand"
1542        );
1543
1544        // Should include config in the exclusion list for spec file completion
1545        assert!(
1546            script.contains("run status resume clean config init projects list describe pr-review monitor gui improve"),
1547            "Fish script should include all commands in dynamic subcommands list"
1548        );
1549    }
1550
1551    #[test]
1552    fn test_bash_completion_includes_config_set_subcommand() {
1553        let script = generate_completion_script(ShellType::Bash);
1554
1555        // Should include config set subcommand
1556        assert!(
1557            script.contains("autom8__config__set"),
1558            "Bash script should include config set subcommand"
1559        );
1560
1561        // Should include config reset subcommand
1562        assert!(
1563            script.contains("autom8__config__reset"),
1564            "Bash script should include config reset subcommand"
1565        );
1566    }
1567
1568    #[test]
1569    fn test_zsh_completion_includes_config_set_subcommand() {
1570        let script = generate_completion_script(ShellType::Zsh);
1571
1572        // Should include config set subcommand
1573        assert!(
1574            script.contains("'set:Set a configuration value'"),
1575            "Zsh script should include config set subcommand"
1576        );
1577
1578        // Should include config reset subcommand
1579        assert!(
1580            script.contains("'reset:Reset configuration to default values'"),
1581            "Zsh script should include config reset subcommand"
1582        );
1583    }
1584
1585    #[test]
1586    fn test_fish_completion_includes_config_set_subcommand() {
1587        let script = generate_completion_script(ShellType::Fish);
1588
1589        // Should include config set subcommand
1590        assert!(
1591            script.contains("\"set\"") && script.contains("Set a configuration value"),
1592            "Fish script should include config set subcommand"
1593        );
1594
1595        // Should include config reset subcommand
1596        assert!(
1597            script.contains("\"reset\"")
1598                && script.contains("Reset configuration to default values"),
1599            "Fish script should include config reset subcommand"
1600        );
1601    }
1602
1603    #[test]
1604    fn test_bash_completion_includes_config_keys() {
1605        let script = generate_completion_script(ShellType::Bash);
1606
1607        // Should include all config keys
1608        let config_keys = [
1609            "review",
1610            "commit",
1611            "pull_request",
1612            "worktree",
1613            "worktree_path_pattern",
1614            "worktree_cleanup",
1615        ];
1616
1617        for key in config_keys {
1618            assert!(
1619                script.contains(key),
1620                "Bash script should include config key: {}",
1621                key
1622            );
1623        }
1624    }
1625
1626    #[test]
1627    fn test_zsh_completion_includes_config_keys() {
1628        let script = generate_completion_script(ShellType::Zsh);
1629
1630        // Should include all config keys in the dynamic completion
1631        let config_keys = [
1632            "review",
1633            "commit",
1634            "pull_request",
1635            "worktree",
1636            "worktree_path_pattern",
1637            "worktree_cleanup",
1638        ];
1639
1640        for key in config_keys {
1641            assert!(
1642                script.contains(key),
1643                "Zsh script should include config key: {}",
1644                key
1645            );
1646        }
1647    }
1648
1649    #[test]
1650    fn test_fish_completion_includes_config_keys() {
1651        let script = generate_completion_script(ShellType::Fish);
1652
1653        // Should include all config keys
1654        let config_keys = [
1655            "review",
1656            "commit",
1657            "pull_request",
1658            "worktree",
1659            "worktree_path_pattern",
1660            "worktree_cleanup",
1661        ];
1662
1663        for key in config_keys {
1664            assert!(
1665                script.contains(key),
1666                "Fish script should include config key: {}",
1667                key
1668            );
1669        }
1670    }
1671
1672    #[test]
1673    fn test_bash_completion_includes_config_boolean_values() {
1674        let script = generate_completion_script(ShellType::Bash);
1675
1676        // Should include true/false completion for boolean config values
1677        assert!(
1678            script.contains("\"true false\""),
1679            "Bash script should include true/false for boolean config values"
1680        );
1681
1682        // Should exclude worktree_path_pattern from boolean completion
1683        assert!(
1684            script.contains("worktree_path_pattern"),
1685            "Bash script should handle worktree_path_pattern specially"
1686        );
1687    }
1688
1689    #[test]
1690    fn test_zsh_completion_includes_config_boolean_values() {
1691        let script = generate_completion_script(ShellType::Zsh);
1692
1693        // Should include true/false completion for boolean config values
1694        assert!(
1695            script.contains("'true' 'false'"),
1696            "Zsh script should include true/false for boolean config values"
1697        );
1698
1699        // Should exclude worktree_path_pattern from boolean completion
1700        assert!(
1701            script.contains("worktree_path_pattern"),
1702            "Zsh script should handle worktree_path_pattern specially"
1703        );
1704    }
1705
1706    #[test]
1707    fn test_fish_completion_includes_config_boolean_values() {
1708        let script = generate_completion_script(ShellType::Fish);
1709
1710        // Should include true/false completion for boolean config values
1711        assert!(
1712            script.contains("'true false'"),
1713            "Fish script should include true/false for boolean config values"
1714        );
1715
1716        // Should exclude worktree_path_pattern from boolean completion
1717        assert!(
1718            script.contains("worktree_path_pattern"),
1719            "Fish script should handle worktree_path_pattern specially"
1720        );
1721    }
1722
1723    #[test]
1724    fn test_bash_completion_config_set_dynamic_completion() {
1725        let script = generate_completion_script(ShellType::Bash);
1726
1727        // Should have dynamic completion for config set
1728        assert!(
1729            script.contains(r#"[[ "${words[1]}" == "config" && "${words[2]}" == "set" ]]"#),
1730            "Bash script should have dynamic completion for config set"
1731        );
1732    }
1733
1734    #[test]
1735    fn test_zsh_completion_config_set_dynamic_completion() {
1736        let script = generate_completion_script(ShellType::Zsh);
1737
1738        // Should have dynamic completion for config set
1739        assert!(
1740            script.contains(r#"[[ "${words[2]}" == "config" && "${words[3]}" == "set" ]]"#),
1741            "Zsh script should have dynamic completion for config set"
1742        );
1743    }
1744
1745    #[test]
1746    fn test_fish_completion_config_set_dynamic_completion() {
1747        let script = generate_completion_script(ShellType::Fish);
1748
1749        // Should have dynamic completion for config set
1750        assert!(
1751            script.contains("__fish_seen_subcommand_from config")
1752                && script.contains("__fish_seen_subcommand_from set"),
1753            "Fish script should have dynamic completion for config set"
1754        );
1755    }
1756
1757    #[test]
1758    fn test_bash_completion_config_flags() {
1759        let script = generate_completion_script(ShellType::Bash);
1760
1761        // Config command should have --global and --project flags
1762        assert!(
1763            script.contains("--global") && script.contains("--project"),
1764            "Bash script should include config --global and --project flags"
1765        );
1766
1767        // Config reset should have --yes flag
1768        assert!(
1769            script.contains("--yes"),
1770            "Bash script should include config reset --yes flag"
1771        );
1772    }
1773
1774    #[test]
1775    fn test_zsh_completion_config_flags() {
1776        let script = generate_completion_script(ShellType::Zsh);
1777
1778        // Config command should have --global and --project flags
1779        assert!(
1780            script.contains("--global[Show only the global configuration]"),
1781            "Zsh script should include config --global flag"
1782        );
1783        assert!(
1784            script.contains("--project[Show only the project configuration]"),
1785            "Zsh script should include config --project flag"
1786        );
1787
1788        // Config reset should have --yes flag
1789        assert!(
1790            script.contains("--yes[Skip confirmation prompt]"),
1791            "Zsh script should include config reset --yes flag"
1792        );
1793    }
1794
1795    #[test]
1796    fn test_fish_completion_config_flags() {
1797        let script = generate_completion_script(ShellType::Fish);
1798
1799        // Config command should have --global and --project flags
1800        assert!(
1801            script.contains("-l global") && script.contains("-l project"),
1802            "Fish script should include config --global and --project flags"
1803        );
1804
1805        // Config reset should have --yes flag
1806        assert!(
1807            script.contains("-l yes"),
1808            "Fish script should include config reset --yes flag"
1809        );
1810    }
1811
1812    #[test]
1813    fn test_all_shells_include_gui_and_improve() {
1814        // Bash: gui and improve in subcommands list
1815        let bash_script = generate_completion_script(ShellType::Bash);
1816        assert!(
1817            bash_script.contains("gui"),
1818            "Bash script should include gui command"
1819        );
1820        assert!(
1821            bash_script.contains("improve"),
1822            "Bash script should include improve command"
1823        );
1824
1825        // Zsh: gui and improve with descriptions
1826        let zsh_script = generate_completion_script(ShellType::Zsh);
1827        assert!(
1828            zsh_script.contains("'gui:Launch the native GUI to monitor autom8 activity'"),
1829            "Zsh script should include gui command with description"
1830        );
1831        assert!(
1832            zsh_script.contains("'improve:Continue iterating on a feature with Claude using context from previous runs'"),
1833            "Zsh script should include improve command with description"
1834        );
1835
1836        // Fish: gui and improve in subcommand exclusion list
1837        let fish_script = generate_completion_script(ShellType::Fish);
1838        assert!(
1839            fish_script.contains("__fish_seen_subcommand_from run status resume clean config init projects list describe pr-review monitor gui improve"),
1840            "Fish script should include gui and improve in subcommand list"
1841        );
1842    }
1843
1844    #[test]
1845    fn test_zsh_completion_config_key_descriptions() {
1846        let script = generate_completion_script(ShellType::Zsh);
1847
1848        // Should include descriptive completions for config keys
1849        assert!(
1850            script.contains("'review:Enable code review step'"),
1851            "Zsh script should include review key with description"
1852        );
1853        assert!(
1854            script.contains("'commit:Enable auto-commit'"),
1855            "Zsh script should include commit key with description"
1856        );
1857        assert!(
1858            script.contains("'pull_request:Enable auto-PR creation'"),
1859            "Zsh script should include pull_request key with description"
1860        );
1861        assert!(
1862            script.contains("'worktree:Enable worktree mode'"),
1863            "Zsh script should include worktree key with description"
1864        );
1865        assert!(
1866            script.contains("'worktree_path_pattern:Pattern for worktree names'"),
1867            "Zsh script should include worktree_path_pattern key with description"
1868        );
1869        assert!(
1870            script.contains("'worktree_cleanup:Auto-cleanup worktrees'"),
1871            "Zsh script should include worktree_cleanup key with description"
1872        );
1873    }
1874}