diff --git a/cli/src/commands/base.rs b/cli/src/commands/base.rs index 0adf1109..4631f04a 100644 --- a/cli/src/commands/base.rs +++ b/cli/src/commands/base.rs @@ -148,7 +148,18 @@ pub async fn run_cli() -> std::io::Result<()> { .unwrap_or_else(|e| { out.err(ExitKind::Api, format!("failed to generate completions: {e}")) }); - io::stdout().write_all(&output.stdout).ok(); + let mut stdout = io::stdout(); + stdout.write_all(&output.stdout).ok(); + // clap's dynamic zsh script registers the completer only when sourced; installed + // as an fpath autoload file it yields nothing on the first TAB. Bridge the autoload + // case so the function completes on its first invocation too. + if shell == Shell::Zsh { + stdout + .write_all( + b"\n[[ ${funcstack[1]} = _gradient ]] && _clap_dynamic_completer_gradient \"$@\"\n", + ) + .ok(); + } } MainCommands::Config { key, value } => { set_get_value_from_string(key, value, false) diff --git a/cli/tests/completion.rs b/cli/tests/completion.rs index 24f50247..f197afba 100644 --- a/cli/tests/completion.rs +++ b/cli/tests/completion.rs @@ -45,3 +45,22 @@ fn completion_zsh_registers_lowercase_binary() { assert!(!script.contains("Gradient"), "zsh script: {script}"); assert!(script.contains("gradient"), "zsh script: {script}"); } + +// clap's dynamic zsh script is built to be sourced; the Nix package installs it as an +// fpath autoload `_gradient` file, where the completer is otherwise registered only on +// the first TAB (producing nothing). The appended bridge must run it on first invocation. +#[test] +fn completion_zsh_bridges_autoload_first_tab() { + let output = Command::cargo_bin("gradient") + .unwrap() + .args(["completion", "zsh"]) + .output() + .unwrap(); + + assert!(output.status.success()); + let script = String::from_utf8(output.stdout).unwrap(); + assert!( + script.contains(r#"[[ ${funcstack[1]} = _gradient ]] && _clap_dynamic_completer_gradient "$@""#), + "zsh script must bridge the autoload first-TAB case:\n{script}" + ); +} diff --git a/docs/src/tests.md b/docs/src/tests.md index 482a91ac..7c86fedb 100644 --- a/docs/src/tests.md +++ b/docs/src/tests.md @@ -2516,7 +2516,7 @@ loading degrades silently when `/etc/ssl/certs` is missing. CLI integration tests in `cli/tests/`: - `download_attr.rs` - `gradient download '#attr' --json` writes the right files; `--json` without args returns a structured missing-argument envelope and exits 2. -- `completion.rs` - regression for the broken completion bin name: `gradient completion {bash,zsh}` must emit a script that registers against the real `gradient` binary (`-F _clap_complete_gradient gradient`) and never the capitalised `Gradient` app name, which silently disabled `gradient `. +- `completion.rs` - regression for the broken completion bin name: `gradient completion {bash,zsh}` must emit a script that registers against the real `gradient` binary (`-F _clap_complete_gradient gradient`) and never the capitalised `Gradient` app name, which silently disabled `gradient `. Also asserts the zsh script appends the autoload bridge (`[[ ${funcstack[1]} = _gradient ]] && _clap_dynamic_completer_gradient "$@"`) so the fpath autoload file the Nix package installs completes on the first TAB instead of only after a second. Dynamic completer unit tests live in `cli/src/commands/completion.rs` (`#[cfg(test)]`, wiremock-backed). They drive each completer core against a mock server and assert it