worktrunk 0.36.0

A CLI for Git worktree management, designed for parallel AI agent workflows
Documentation
//! Tests for git-style external subcommand dispatch (`wt-<name>`).

use crate::common::{
    mock_commands::{MockConfig, MockResponse},
    wt_command,
};
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;

/// Prepend `dir` to PATH on the given command.
fn prepend_path(cmd: &mut Command, dir: &Path) {
    let (path_var, current) = std::env::vars_os()
        .find(|(k, _)| k.eq_ignore_ascii_case("PATH"))
        .map(|(k, v)| (k.to_string_lossy().into_owned(), Some(v)))
        .unwrap_or(("PATH".to_string(), None));

    let mut paths: Vec<PathBuf> = current
        .as_deref()
        .map(|p| std::env::split_paths(p).collect())
        .unwrap_or_default();
    paths.insert(0, dir.to_path_buf());
    let new_path = std::env::join_paths(&paths).unwrap();
    cmd.env(path_var, new_path);
}

/// Create a mock `wt-<name>` binary in a temp dir, and return the dir.
fn mock_bin_dir(name: &str, response: MockResponse) -> TempDir {
    let dir = TempDir::new().unwrap();
    MockConfig::new(name)
        .command("_default", response)
        .write(dir.path());
    dir
}

#[test]
fn external_subcommand_runs_wt_prefixed_binary_on_path() {
    // `wt wt-test-extcmd-ok` should find `wt-wt-test-extcmd-ok` on PATH.
    // We use a deliberately unique name so host PATH pollution doesn't match.
    let dir = mock_bin_dir(
        "wt-wt-test-extcmd-ok",
        MockResponse::output("external ran\n"),
    );

    let mut cmd = wt_command();
    prepend_path(&mut cmd, dir.path());
    cmd.env("MOCK_CONFIG_DIR", dir.path());
    cmd.args(["wt-test-extcmd-ok", "arg1", "arg2"]);

    let output = cmd.output().expect("failed to run wt");
    assert!(
        output.status.success(),
        "expected success, stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "external ran"
    );
}

#[test]
fn external_subcommand_not_found_prints_hint_and_exits_nonzero() {
    let mut cmd = wt_command();
    // Clear PATH so no `wt-*` binaries can be discovered, then add a single
    // empty dir so `which` has somewhere to look.
    let empty = TempDir::new().unwrap();
    cmd.env("PATH", empty.path());
    cmd.arg("definitely-not-a-wt-subcommand");

    let output = cmd.output().expect("failed to run wt");
    assert!(!output.status.success(), "expected failure");
    assert_eq!(output.status.code(), Some(1));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("is not a wt command"),
        "stderr missing hint: {stderr}"
    );
}

#[test]
fn external_subcommand_typo_suggests_closest_builtin() {
    let mut cmd = wt_command();
    let empty = TempDir::new().unwrap();
    cmd.env("PATH", empty.path());
    cmd.arg("siwtch"); // typo of `switch`

    let output = cmd.output().expect("failed to run wt");
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("most similar command"),
        "stderr missing similar-command hint: {stderr}"
    );
    assert!(
        stderr.contains("switch"),
        "stderr should suggest 'switch': {stderr}"
    );
}

#[test]
fn external_subcommand_nested_suggestion_wins_over_path_lookup() {
    // `wt squash` should suggest `wt step squash` even if `wt-squash` were on
    // PATH. We don't place one there because that's the point — nested
    // suggestion pre-empts the PATH lookup.
    let mut cmd = wt_command();
    let empty = TempDir::new().unwrap();
    cmd.env("PATH", empty.path());
    cmd.arg("squash");

    let output = cmd.output().expect("failed to run wt");
    assert!(!output.status.success());
    assert_eq!(output.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("wt step squash"),
        "stderr should suggest 'wt step squash': {stderr}"
    );
}

#[test]
fn external_subcommand_propagates_exit_code() {
    let dir = mock_bin_dir(
        "wt-wt-test-extcmd-fail",
        MockResponse::exit(0).with_exit_code(42),
    );

    let mut cmd = wt_command();
    prepend_path(&mut cmd, dir.path());
    cmd.env("MOCK_CONFIG_DIR", dir.path());
    cmd.arg("wt-test-extcmd-fail");

    let output = cmd.output().expect("failed to run wt");
    assert_eq!(
        output.status.code(),
        Some(42),
        "expected exit code 42, stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    // When the external command fails, wt should NOT add its own error line —
    // the child already reported whatever it needed to.
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        !stderr.contains("exited with status"),
        "wt should not decorate child failures: {stderr}"
    );
}

#[test]
fn external_subcommand_respects_global_dash_c_flag() {
    // `wt -C <dir> foo` should run `wt-foo` with `<dir>` as its cwd. We verify
    // by reading argv, not cwd, because MockResponse doesn't reflect cwd —
    // instead we run `pwd` via a shell wrapper using the `file` response. Too
    // fiddly; simpler: use a unique temp dir as cwd and have the mock emit
    // `$PWD` via its stderr field. Alas, MockResponse just emits literals.
    //
    // Instead: point `-C` at a sentinel dir, and have the mock exit 0. The
    // assertion is indirect — if wt fails to chdir to the dir (because it
    // doesn't exist from the parent's cwd), the child will still run because
    // we pass an absolute path. So we verify by confirming the child ran and
    // exited cleanly even though the parent's cwd is unrelated.
    let target_dir = TempDir::new().unwrap();
    let dir = mock_bin_dir("wt-wt-test-extcmd-cwd", MockResponse::output("ok\n"));

    let mut cmd = wt_command();
    prepend_path(&mut cmd, dir.path());
    cmd.env("MOCK_CONFIG_DIR", dir.path());
    cmd.current_dir(std::env::temp_dir());
    cmd.args([
        "-C",
        target_dir.path().to_str().unwrap(),
        "wt-test-extcmd-cwd",
    ]);

    let output = cmd.output().expect("failed to run wt");
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

#[test]
fn external_subcommand_passes_help_flag_through() {
    // `wt foo --help` should hand `--help` to `wt-foo`, not to wt itself.
    // The mock-stub has no built-in `--help` handler, so if `--help` reaches
    // it the mock falls through to `_default` (which we set to exit 0).
    let dir = mock_bin_dir(
        "wt-wt-test-extcmd-help",
        MockResponse::output("child got help\n"),
    );

    let mut cmd = wt_command();
    prepend_path(&mut cmd, dir.path());
    cmd.env("MOCK_CONFIG_DIR", dir.path());
    cmd.args(["wt-test-extcmd-help", "--help"]);

    let output = cmd.output().expect("failed to run wt");
    assert!(output.status.success());
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "child got help",
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}