use crate::common::{TestRepo, repo};
use rstest::rstest;
use std::fs;
use worktrunk::config::UserConfig;
#[rstest]
fn test_switch_with_active_shell_integration_no_prompt(repo: TestRepo) {
let create_output = repo
.wt_command()
.args(["switch", "--create", "feature"])
.output()
.unwrap();
assert!(
create_output.status.success(),
"First switch should succeed: {}",
String::from_utf8_lossy(&create_output.stderr)
);
let cd_file = repo.root_path().join("directive_cd.txt");
let exec_file = repo.root_path().join("directive_exec.txt");
fs::write(&cd_file, "").unwrap();
fs::write(&exec_file, "").unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_DIRECTIVE_CD_FILE", &cd_file);
cmd.env("WORKTRUNK_DIRECTIVE_EXEC_FILE", &exec_file);
let output = cmd.args(["switch", "feature"]).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"Switch should succeed.\nstderr: {stderr}\nstdout: {stdout}"
);
let cd_content = fs::read_to_string(&cd_file).unwrap_or_default();
assert!(
!cd_content.trim().is_empty(),
"CD file should contain a path when shell integration active"
);
assert!(
!stderr.contains("Install shell integration"),
"Should not show install prompt when shell integration active: {stderr}"
);
}
#[rstest]
fn test_switch_with_skip_prompt_flag(repo: TestRepo) {
let config_path = repo.test_config_path();
let config = UserConfig {
skip_shell_integration_prompt: true,
..Default::default()
};
config.save_to(config_path).unwrap();
let output = repo
.wt_command()
.args(["switch", "--create", "feature"])
.output()
.unwrap();
assert!(output.status.success(), "Switch should succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Install shell integration"),
"Should not show install prompt when already prompted: {stderr}"
);
}
#[rstest]
fn test_switch_non_tty_shows_hint(repo: TestRepo) {
use std::process::Stdio;
let output = repo
.wt_command()
.args(["switch", "--create", "feature"])
.stdin(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "Switch should succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Created branch") && stderr.contains("and worktree"),
"Should create worktree: {stderr}"
);
assert!(
stderr.contains("wt config shell install"),
"Should show install hint: {stderr}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
!config_content.contains("skip-shell-integration-prompt"),
"Should not mark as prompted for non-TTY (hints are not prompts): {config_content}"
);
let output2 = repo
.wt_command()
.args(["switch", "--create", "feature2"])
.stdin(Stdio::piped())
.output()
.unwrap();
let stderr2 = String::from_utf8_lossy(&output2.stderr);
assert!(
stderr2.contains("wt config shell install"),
"Should show hint on every non-TTY run: {stderr2}"
);
}
#[rstest]
fn test_switch_unsupported_shell_shows_hint(repo: TestRepo) {
use std::process::Stdio;
let mut cmd = repo.wt_command();
cmd.env("SHELL", "/bin/tcsh");
let output = cmd
.args(["switch", "--create", "feature"])
.stdin(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "Switch should succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("not yet supported for tcsh"),
"Should show unsupported shell message: {stderr}"
);
assert!(
stderr.contains("bash, zsh, fish, nu, PowerShell"),
"Should list supported shells: {stderr}"
);
}
#[rstest]
fn test_switch_no_shell_env_shows_hint(repo: TestRepo) {
use std::process::Stdio;
let mut cmd = repo.wt_command();
cmd.env_remove("SHELL");
let output = cmd
.args(["switch", "--create", "feature"])
.stdin(Stdio::piped())
.output()
.unwrap();
assert!(output.status.success(), "Switch should succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("wt config shell install"),
"Should show install hint when SHELL not set: {stderr}"
);
}
#[cfg(all(unix, feature = "shell-integration-tests"))]
mod pty_tests {
use super::*;
use crate::common::pty::{build_pty_command, exec_cmd_in_pty, exec_cmd_in_pty_prompted};
use crate::common::{add_pty_filters, setup_snapshot_settings, wt_bin};
use insta::assert_snapshot;
use std::path::Path;
use tempfile::TempDir;
fn prompt_pty_settings(repo: &TestRepo, home_dir: &Path) -> insta::Settings {
let mut settings = setup_snapshot_settings(repo);
add_pty_filters(&mut settings);
settings.add_filter(®ex::escape(&home_dir.to_string_lossy()), "[HOME]");
settings
}
#[rstest]
fn test_already_installed_skips_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bashrc = temp_home.path().join(".bashrc");
let config_line = "if command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init bash)\"; fi";
fs::write(&bashrc, format!("{config_line}\n")).unwrap();
let mut env_vars = repo.test_env_vars();
env_vars.retain(|(k, _)| {
k != "WORKTRUNK_DIRECTIVE_CD_FILE"
&& k != "WORKTRUNK_DIRECTIVE_EXEC_FILE"
&& k != "WORKTRUNK_DIRECTIVE_FILE"
});
env_vars.push(("SHELL".to_string(), "/bin/bash".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty(cmd, "");
assert_eq!(exit_code, 0);
assert!(
!output.contains("Install shell integration"),
"Should not prompt when already installed: {output}"
);
assert!(
output.contains("Created branch") && output.contains("and worktree"),
"Should create worktree: {output}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
!config_content.contains("skip-shell-integration-prompt"),
"Should NOT mark as prompted when just showing hint: {config_content}"
);
}
#[rstest]
fn test_user_declines_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bashrc = temp_home.path().join(".bashrc");
fs::write(&bashrc, "# empty bashrc\n").unwrap();
let mut env_vars = repo.test_env_vars();
env_vars.retain(|(k, _)| k != "WORKTRUNK_DIRECTIVE_FILE");
env_vars.push(("SHELL".to_string(), "/bin/bash".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["n\n"], "[y/N");
assert_eq!(exit_code, 0);
assert!(
output.contains("Install shell integration"),
"Should show prompt: {output}"
);
assert!(
output.contains("Created branch") && output.contains("and worktree"),
"Should create worktree: {output}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
config_content.contains("skip-shell-integration-prompt = true"),
"Should mark as prompted after decline: {config_content}"
);
let bashrc_content = fs::read_to_string(&bashrc).unwrap();
assert!(
!bashrc_content.contains("eval \"$(command wt"),
"Should not install when declined: {bashrc_content}"
);
prompt_pty_settings(&repo, temp_home.path()).bind(|| {
assert_snapshot!("prompt_decline", &output);
});
}
#[rstest]
fn test_user_accepts_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bashrc = temp_home.path().join(".bashrc");
fs::write(&bashrc, "# empty bashrc\n").unwrap();
let mut env_vars = repo.test_env_vars();
env_vars.retain(|(k, _)| k != "WORKTRUNK_DIRECTIVE_FILE");
env_vars.push(("SHELL".to_string(), "/bin/bash".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["y\n"], "[y/N");
assert_eq!(exit_code, 0);
assert!(
output.contains("Install shell integration"),
"Should show prompt: {output}"
);
assert!(
output.contains("Configured") && output.contains("bash"),
"Should show configured message: {output}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
!config_content.contains("skip-shell-integration-prompt = true"),
"Should not set skip flag after accept (installation itself prevents future prompts): {config_content}"
);
let bashrc_content = fs::read_to_string(&bashrc).unwrap();
assert!(
bashrc_content.contains("eval \"$(command wt"),
"Should install when accepted: {bashrc_content}"
);
prompt_pty_settings(&repo, temp_home.path()).bind(|| {
assert_snapshot!("prompt_accept", &output);
});
}
#[rstest]
fn test_user_requests_preview_then_declines(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bashrc = temp_home.path().join(".bashrc");
fs::write(&bashrc, "# empty bashrc\n").unwrap();
let mut env_vars = repo.test_env_vars();
env_vars.retain(|(k, _)| k != "WORKTRUNK_DIRECTIVE_FILE");
env_vars.push(("SHELL".to_string(), "/bin/bash".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["?\n", "n\n"], "[y/N");
assert_eq!(exit_code, 0);
assert!(
output.contains("Install shell integration"),
"Should show prompt: {output}"
);
assert!(
output.contains("Will add") && output.contains("bash"),
"Should show preview: {output}"
);
assert!(
output.contains("eval") && output.contains("wt config shell init"),
"Should show config line in preview: {output}"
);
let bashrc_content = fs::read_to_string(&bashrc).unwrap();
assert!(
!bashrc_content.contains("eval \"$(command wt"),
"Should not install when declined after preview: {bashrc_content}"
);
prompt_pty_settings(&repo, temp_home.path()).bind(|| {
assert_snapshot!("prompt_preview_decline", &output);
});
}
#[rstest]
fn test_no_prompt_after_first_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bashrc = temp_home.path().join(".bashrc");
fs::write(&bashrc, "# empty bashrc\n").unwrap();
let mut env_vars = repo.test_env_vars();
env_vars.retain(|(k, _)| k != "WORKTRUNK_DIRECTIVE_FILE");
env_vars.push(("SHELL".to_string(), "/bin/bash".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature1"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (_, _) = exec_cmd_in_pty_prompted(cmd, &["n\n"], "[y/N");
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["switch", "--create", "feature2"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty(cmd, "");
assert_eq!(exit_code, 0);
assert!(
!output.contains("Install shell integration"),
"Should not prompt on second switch: {output}"
);
}
}
#[cfg(all(unix, feature = "shell-integration-tests"))]
mod commit_generation_prompt_tests {
use super::*;
use crate::common::pty::{build_pty_command, exec_cmd_in_pty, exec_cmd_in_pty_prompted};
use crate::common::wt_bin;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn setup_fake_claude(temp_home: &Path) -> PathBuf {
let bin_dir = temp_home.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let claude_path = bin_dir.join("claude");
fs::write(&claude_path, "#!/bin/sh\nexit 0\n").unwrap();
let mut perms = fs::metadata(&claude_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&claude_path, perms).unwrap();
bin_dir
}
#[rstest]
fn test_no_llm_tool_sets_skip_flag(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let test_file = repo.root_path().join("test.txt");
fs::write(&test_file, "test content\n").unwrap();
repo.run_git(&["add", "test.txt"]);
let mut env_vars = repo.test_env_vars();
env_vars.push(("PATH".to_string(), "/usr/bin:/bin".to_string()));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["step", "commit"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty(cmd, "");
assert_eq!(exit_code, 0, "Command should succeed: {output}");
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
config_content.contains("skip-commit-generation-prompt = true"),
"Should set skip flag when no tool found: {config_content}"
);
}
#[rstest]
fn test_user_declines_llm_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bin_dir = setup_fake_claude(temp_home.path());
let test_file = repo.root_path().join("test.txt");
fs::write(&test_file, "test content\n").unwrap();
repo.run_git(&["add", "test.txt"]);
let mut env_vars = repo.test_env_vars();
let path = format!("{}:/usr/bin:/bin", bin_dir.display());
env_vars.push(("PATH".to_string(), path));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["step", "commit"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["n\n"], "[y/N");
assert_eq!(exit_code, 0, "Command should succeed: {output}");
assert!(
output.contains("Configure") && output.contains("claude"),
"Should show LLM config prompt: {output}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
config_content.contains("skip-commit-generation-prompt = true"),
"Should set skip flag when declined: {config_content}"
);
}
#[rstest]
fn test_user_accepts_llm_prompt(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bin_dir = setup_fake_claude(temp_home.path());
let test_file = repo.root_path().join("test.txt");
fs::write(&test_file, "test content\n").unwrap();
repo.run_git(&["add", "test.txt"]);
let mut env_vars = repo.test_env_vars();
let path = format!("{}:/usr/bin:/bin", bin_dir.display());
env_vars.push(("PATH".to_string(), path));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["step", "commit"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, _exit_code) = exec_cmd_in_pty_prompted(cmd, &["y\n"], "[y/N");
assert!(
output.contains("Added to user config"),
"Should show config added message: {output}"
);
let config_content = fs::read_to_string(repo.test_config_path()).unwrap_or_default();
assert!(
config_content.contains("[commit.generation]") && config_content.contains("command"),
"Should add commit generation config: {config_content}"
);
}
#[rstest]
fn test_user_requests_preview(repo: TestRepo) {
let temp_home = TempDir::new().unwrap();
let bin_dir = setup_fake_claude(temp_home.path());
let test_file = repo.root_path().join("test.txt");
fs::write(&test_file, "test content\n").unwrap();
repo.run_git(&["add", "test.txt"]);
let mut env_vars = repo.test_env_vars();
let path = format!("{}:/usr/bin:/bin", bin_dir.display());
env_vars.push(("PATH".to_string(), path));
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["step", "commit"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["?\n", "n\n"], "[y/N");
assert_eq!(exit_code, 0, "Command should succeed: {output}");
assert!(
output.contains("Would add to") && output.contains("[commit.generation]"),
"Should show preview: {output}"
);
}
}