use crate::common::{
TestRepo, repo, set_temp_home_env, set_xdg_config_path, setup_home_snapshot_settings,
temp_home, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
use tempfile::TempDir;
#[rstest]
fn test_configure_shell_with_yes(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mSkipped [4mbash[24m; [4m~/.bashrc[24m not found[22m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[32m✓[39m [32mConfigured 1 shell[39m
[33mâ–²[39m [33mCompletions require compinit; add to ~/.zshrc before the wt line:[39m
[107m [0m [2m[0m[2m[34mautoload[0m[2m [0m[2m[36m-Uz[0m[2m compinit [0m[2m[36m&&[0m[2m [0m[2m[34mcompinit[0m[2m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(content.contains("eval \"$(command wt config shell init zsh)\""));
}
#[rstest]
fn test_configure_shell_specific_shell(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
[33mâ–²[39m [33mCompletions require compinit; add to ~/.zshrc before the wt line:[39m
[107m [0m [2m[0m[2m[34mautoload[0m[2m [0m[2m[36m-Uz[0m[2m compinit [0m[2m[36m&&[0m[2m [0m[2m[34mcompinit[0m[2m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(content.contains("eval \"$(command wt config shell init zsh)\""));
}
#[rstest]
fn test_configure_shell_already_exists(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[2mâ—‹[22m Already configured shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m
[32m✓[39m [32mAll shells already configured[39m
");
});
let content = fs::read_to_string(&zshrc_path).unwrap();
let count = content.matches("wt config shell init").count();
assert_eq!(count, 1, "Should only have one wt config shell init line");
}
#[rstest]
fn test_configure_shell_fish(repo: TestRepo, temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mCreated shell extension for [1mfish[22m @ [1m~/.config/fish/functions/wt.fish[22m[39m
[32m✓[39m [32mCreated completions for [1mfish[22m @ [1m~/.config/fish/completions/wt.fish[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
let fish_config = temp_home.path().join(".config/fish/functions/wt.fish");
assert!(fish_config.exists());
let content = fs::read_to_string(&fish_config).unwrap();
assert!(
content.contains("function wt"),
"Should contain function definition: {}",
content
);
}
#[rstest]
fn test_configure_shell_fish_dry_run(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let fish_config = functions.join("wt.fish");
assert!(
!fish_config.exists(),
"Dry-run should not create files: {:?}",
fish_config
);
}
#[rstest]
fn test_configure_shell_fish_extension_exists(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let fish_config = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&fish_config, format!("{}\n", wrapper_content)).unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[2mâ—‹[22m Already configured shell extension for [1mfish[22m @ [1m~/.config/fish/functions/wt.fish[22m
[32m✓[39m [32mCreated completions for [1mfish[22m @ [1m~/.config/fish/completions/wt.fish[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
");
});
let completions_file = temp_home.path().join(".config/fish/completions/wt.fish");
assert!(
completions_file.exists(),
"Fish completions file should be created"
);
let contents = std::fs::read_to_string(&completions_file).unwrap();
assert!(
contents.contains(r#"test -n \"\$WORKTRUNK_BIN\""#),
"Fish completions should check WORKTRUNK_BIN is non-empty with fallback"
);
}
#[rstest]
fn test_configure_shell_fish_all_already_configured(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let fish_config = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&fish_config, format!("{}\n", wrapper_content)).unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
let completions_file = completions_d.join("wt.fish");
fs::write(&completions_file, "# existing completions").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_configure_shell_fish_legacy_conf_d_cleanup(repo: TestRepo, temp_home: TempDir) {
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let new_file = temp_home.path().join(".config/fish/functions/wt.fish");
assert!(
new_file.exists(),
"Should create functions/wt.fish: {:?}",
new_file
);
assert!(
!legacy_file.exists(),
"Should remove legacy conf.d/wt.fish: {:?}",
legacy_file
);
}
#[rstest]
fn test_configure_shell_fish_legacy_cleanup_even_when_already_exists(
repo: TestRepo,
temp_home: TempDir,
) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let new_file = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&new_file, format!("{}\n", wrapper_content)).unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
fs::write(completions_d.join("wt.fish"), "# completions").unwrap();
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
assert!(
!legacy_file.exists(),
"Should remove legacy conf.d/wt.fish even when functions/wt.fish already exists: {:?}",
legacy_file
);
assert!(
new_file.exists(),
"Should preserve existing functions/wt.fish: {:?}",
new_file
);
}
#[rstest]
fn test_uninstall_shell_fish_legacy_conf_d_cleanup(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let new_file = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&new_file, format!("{}\n", wrapper_content)).unwrap();
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
let completions_file = completions_d.join("wt.fish");
fs::write(&completions_file, "# completions").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
assert!(
!new_file.exists(),
"Should remove functions/wt.fish: {:?}",
new_file
);
assert!(
!legacy_file.exists(),
"Should remove legacy conf.d/wt.fish: {:?}",
legacy_file
);
assert!(
!completions_file.exists(),
"Should remove completions/wt.fish: {:?}",
completions_file
);
}
#[rstest]
fn test_configure_shell_fish_dry_run_does_not_delete_legacy(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let new_file = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&new_file, format!("{}\n", wrapper_content)).unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
fs::write(completions_d.join("wt.fish"), "# completions").unwrap();
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
assert!(
legacy_file.exists(),
"--dry-run must NOT delete legacy conf.d/wt.fish: {:?}",
legacy_file
);
assert!(
new_file.exists(),
"functions/wt.fish should be preserved: {:?}",
new_file
);
}
#[rstest]
fn test_config_show_detects_fish_legacy_conf_d(mut repo: TestRepo, temp_home: TempDir) {
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
repo.setup_mock_ci_tools_unauthenticated();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = repo.wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config").arg("show").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_fish_legacy_with_functions_dir(mut repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let legacy_file = conf_d.join("wt.fish");
fs::write(&legacy_file, "wt config shell init fish | source").unwrap();
repo.setup_mock_ci_tools_unauthenticated();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = repo.wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config").arg("show").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_configure_shell_no_files(repo: TestRepo, temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
[2m↳[22m [2mSkipped [4mbash[24m; [4m~/.bashrc[24m not found[22m
[2m↳[22m [2mSkipped [4mzsh[24m; [4m~/.zshrc[24m not found[22m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[31m✗[39m [31mNo shell config files found[39m
");
});
}
#[rstest]
fn test_configure_shell_multiple_configs(repo: TestRepo, temp_home: TempDir) {
let bash_config_path = temp_home.path().join(".bashrc");
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&bash_config_path, "# Existing bash config\n").unwrap();
fs::write(&zshrc_path, "# Existing zsh config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mbash[22m @ [1m~/.bashrc[22m[39m
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[32m✓[39m [32mConfigured 2 shells[39m
[33mâ–²[39m [33mCompletions require compinit; add to ~/.zshrc before the wt line:[39m
[107m [0m [2m[0m[2m[34mautoload[0m[2m [0m[2m[36m-Uz[0m[2m compinit [0m[2m[36m&&[0m[2m [0m[2m[34mcompinit[0m[2m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
let bash_content = fs::read_to_string(&bash_config_path).unwrap();
assert!(
bash_content.contains("eval \"$(command wt config shell init bash)\""),
"Bash config should be updated"
);
let zsh_content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
zsh_content.contains("eval \"$(command wt config shell init zsh)\""),
"Zsh config should be updated"
);
}
#[rstest]
fn test_configure_shell_mixed_states(repo: TestRepo, temp_home: TempDir) {
let bash_config_path = temp_home.path().join(".bashrc");
fs::write(
&bash_config_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init bash)\"; fi\n",
)
.unwrap();
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing zsh config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[2mâ—‹[22m Already configured shell extension & completions for [1mbash[22m @ [1m~/.bashrc[22m
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[32m✓[39m [32mConfigured 1 shell[39m
[33mâ–²[39m [33mCompletions require compinit; add to ~/.zshrc before the wt line:[39m
[107m [0m [2m[0m[2m[34mautoload[0m[2m [0m[2m[36m-Uz[0m[2m compinit [0m[2m[36m&&[0m[2m [0m[2m[34mcompinit[0m[2m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
let bash_content = fs::read_to_string(&bash_config_path).unwrap();
let bash_wt_count = bash_content.matches("wt config shell init").count();
assert_eq!(
bash_wt_count, 1,
"Bash should still have exactly one wt config shell init line"
);
let zsh_content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
zsh_content.contains("eval \"$(command wt config shell init zsh)\""),
"Zsh config should be updated"
);
}
#[rstest]
fn test_uninstall_shell(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mRemoved shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mNo [4mbash[24m shell extension & completions in ~/.bashrc[22m
[2m↳[22m [2mNo [4mfish[24m shell extension in ~/.config/fish/functions/wt.fish[22m
[2m↳[22m [2mNo [4mnu[24m shell extension in ~/.config/nushell/vendor/autoload/wt.nu[22m
[2m↳[22m [2mNo [4mfish[24m completions in ~/.config/fish/completions/wt.fish[22m
[32m✓[39m [32mRemoved integration from 1 shell[39m
[2m↳[22m [2mRestart shell to complete uninstall[22m
");
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!content.contains("wt config shell init"),
"Integration should be removed"
);
assert!(
content.contains("# Existing config"),
"Other content should be preserved"
);
}
#[rstest]
fn test_uninstall_shell_multiple(repo: TestRepo, temp_home: TempDir) {
let bash_config_path = temp_home.path().join(".bashrc");
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&bash_config_path,
"# Bash config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init bash)\"; fi\n",
)
.unwrap();
fs::write(
&zshrc_path,
"# Zsh config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mRemoved shell extension & completions for [1mbash[22m @ [1m~/.bashrc[22m[39m
[32m✓[39m [32mRemoved shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mNo [4mfish[24m shell extension in ~/.config/fish/functions/wt.fish[22m
[2m↳[22m [2mNo [4mnu[24m shell extension in ~/.config/nushell/vendor/autoload/wt.nu[22m
[2m↳[22m [2mNo [4mfish[24m completions in ~/.config/fish/completions/wt.fish[22m
[32m✓[39m [32mRemoved integration from 2 shells[39m
[2m↳[22m [2mRestart shell to complete uninstall[22m
");
});
let bash_content = fs::read_to_string(&bash_config_path).unwrap();
assert!(
!bash_content.contains("wt config shell init"),
"Bash integration should be removed"
);
let zsh_content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!zsh_content.contains("wt config shell init"),
"Zsh integration should be removed"
);
}
#[rstest]
fn test_uninstall_shell_not_found(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[33mâ–²[39m [33mNo shell extension & completions found in ~/.zshrc[39m
");
});
}
#[rstest]
fn test_uninstall_shell_fish(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let fish_config = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&fish_config, format!("{}\n", wrapper_content)).unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("fish")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mRemoved shell extension for [1mfish[22m @ [1m~/.config/fish/functions/wt.fish[22m[39m
[32m✓[39m [32mRemoved integration from 1 shell[39m
[2m↳[22m [2mRestart shell to complete uninstall[22m
");
});
assert!(!fish_config.exists());
}
#[rstest]
fn test_install_uninstall_roundtrip(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nexport PATH=$HOME/bin:$PATH\n",
)
.unwrap();
{
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Install should succeed");
}
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(content.contains("wt config shell init zsh"));
{
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Uninstall should succeed");
}
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!content.contains("wt config shell init"),
"Integration should be removed"
);
assert!(
content.contains("# Existing config"),
"Comment should be preserved"
);
assert!(
content.contains("export PATH=$HOME/bin:$PATH"),
"PATH export should be preserved"
);
}
#[rstest]
fn test_install_uninstall_no_blank_line_accumulation(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
let initial_content =
"[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh\n\nautoload -Uz compinit && compinit\n";
fs::write(&zshrc_path, initial_content).unwrap();
{
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.args(["config", "shell", "install", "zsh", "--yes"]);
cmd.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Install should succeed");
}
let after_install = fs::read_to_string(&zshrc_path).unwrap();
assert!(
after_install.contains("wt config shell init zsh"),
"Integration should be added"
);
{
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.args(["config", "shell", "uninstall", "zsh", "--yes"]);
cmd.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Uninstall should succeed");
}
let after_uninstall = fs::read_to_string(&zshrc_path).unwrap();
assert_eq!(
initial_content, after_uninstall,
"Uninstall should restore original content"
);
{
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.args(["config", "shell", "install", "zsh", "--yes"]);
cmd.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Re-install should succeed");
}
let after_reinstall = fs::read_to_string(&zshrc_path).unwrap();
assert_eq!(
after_install, after_reinstall,
"Re-install should produce same result as initial install.\n\
After first install:\n{after_install}\n---\n\
After uninstall:\n{after_uninstall}\n---\n\
After re-install:\n{after_reinstall}"
);
}
#[rstest]
fn test_configure_shell_no_warning_when_compinit_enabled(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nautoload -Uz compinit && compinit\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("ZDOTDIR", crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().to_path_buf()));
cmd.env("WORKTRUNK_TEST_COMPINIT_CONFIGURED", "1"); cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
}
#[rstest]
fn test_configure_shell_no_warning_for_bash_user(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
let bashrc_path = temp_home.path().join(".bashrc");
fs::write(&zshrc_path, "# Existing zsh config\n").unwrap();
fs::write(&bashrc_path, "# Existing bash config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/bash"); cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mbash[22m @ [1m~/.bashrc[22m[39m
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[32m✓[39m [32mConfigured 2 shells[39m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
}
#[rstest]
fn test_configure_shell_create_zshrc_when_missing(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
assert!(!zshrc_path.exists(), "zshrc should not exist before test");
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh") .arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mCreated shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
[33mâ–²[39m [33mCompletions require compinit; add to ~/.zshrc before the wt line:[39m
[107m [0m [2m[0m[2m[34mautoload[0m[2m [0m[2m[36m-Uz[0m[2m compinit [0m[2m[36m&&[0m[2m [0m[2m[34mcompinit[0m[2m
[2m↳[22m [2mRestart shell to activate shell integration[22m
");
});
assert!(zshrc_path.exists(), "zshrc should exist after install");
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
content.contains("eval \"$(command wt config shell init zsh)\""),
"Created file should contain wt integration: {}",
content
);
}
#[rstest]
fn test_configure_shell_no_warning_for_fish_install(repo: TestRepo, temp_home: TempDir) {
let fish_conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&fish_conf_d).unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh"); cmd.arg("config")
.arg("shell")
.arg("install")
.arg("fish") .arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mCreated shell extension for [1mfish[22m @ [1m~/.config/fish/functions/wt.fish[22m[39m
[32m✓[39m [32mCreated completions for [1mfish[22m @ [1m~/.config/fish/completions/wt.fish[22m[39m
[32m✓[39m [32mConfigured 1 shell[39m
");
});
}
#[rstest]
fn test_configure_shell_no_warning_when_already_configured(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[2mâ—‹[22m Already configured shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m
[32m✓[39m [32mAll shells already configured[39m
");
});
}
#[rstest]
fn test_configure_shell_no_warning_when_shell_unset(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
let bashrc_path = temp_home.path().join(".bashrc");
fs::write(&zshrc_path, "# Existing zsh config\n").unwrap();
fs::write(&bashrc_path, "# Existing bash config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env_remove("SHELL"); cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[32m✓[39m [32mAdded shell extension & completions for [1mbash[22m @ [1m~/.bashrc[22m[39m
[32m✓[39m [32mAdded shell extension & completions for [1mzsh[22m @ [1m~/.zshrc[22m[39m
[2m↳[22m [2mSkipped [4mfish[24m; [4m~/.config/fish/functions[24m not found[22m
[2m↳[22m [2mSkipped [4mnu[24m; [4m~/.config/nushell/vendor/autoload[24m not found[22m
[32m✓[39m [32mConfigured 2 shells[39m
");
});
}
#[rstest]
fn test_configure_shell_dry_run(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!content.contains("wt config shell init"),
"File should not be modified with --dry-run"
);
assert_eq!(content, "# Existing config\n", "File should be unchanged");
}
#[rstest]
fn test_configure_shell_dry_run_multiple(repo: TestRepo, temp_home: TempDir) {
let bash_config_path = temp_home.path().join(".bashrc");
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&bash_config_path, "# Existing bash config\n").unwrap();
fs::write(&zshrc_path, "# Existing zsh config\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let bash_content = fs::read_to_string(&bash_config_path).unwrap();
assert!(
!bash_content.contains("wt config shell init"),
"Bash config should not be modified with --dry-run"
);
let zsh_content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!zsh_content.contains("wt config shell init"),
"Zsh config should not be modified with --dry-run"
);
}
#[rstest]
fn test_configure_shell_dry_run_already_configured(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("zsh")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_uninstall_shell_dry_run(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&zshrc_path,
"# Existing config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("zsh")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
content.contains("wt config shell init"),
"File should not be modified with --dry-run"
);
}
#[rstest]
fn test_uninstall_shell_dry_run_fish(repo: TestRepo, temp_home: TempDir) {
let conf_d = temp_home.path().join(".config/fish/conf.d");
fs::create_dir_all(&conf_d).unwrap();
let fish_config = conf_d.join("wt.fish");
fs::write(
&fish_config,
"if type -q wt; command wt config shell init fish | source; end\n",
)
.unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
let completions_file = completions_d.join("wt.fish");
fs::write(&completions_file, "# fish completions").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("fish")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
assert!(fish_config.exists(), "Fish config should still exist");
assert!(
completions_file.exists(),
"Fish completions should still exist"
);
}
#[rstest]
fn test_uninstall_shell_dry_run_fish_canonical(repo: TestRepo, temp_home: TempDir) {
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
let fish_config = functions.join("wt.fish");
let init =
worktrunk::shell::ShellInit::with_prefix(worktrunk::shell::Shell::Fish, "wt".to_string());
let wrapper_content = init.generate_fish_wrapper().unwrap();
fs::write(&fish_config, format!("{}\n", wrapper_content)).unwrap();
let completions_d = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions_d).unwrap();
let completions_file = completions_d.join("wt.fish");
fs::write(&completions_file, "# fish completions").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/fish");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("fish")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
assert!(fish_config.exists(), "Fish config should still exist");
assert!(
completions_file.exists(),
"Fish completions should still exist"
);
}
#[rstest]
fn test_uninstall_shell_dry_run_multiple(repo: TestRepo, temp_home: TempDir) {
let bash_config_path = temp_home.path().join(".bashrc");
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(
&bash_config_path,
"# Bash config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init bash)\"; fi\n",
)
.unwrap();
fs::write(
&zshrc_path,
"# Zsh config\nif command -v wt >/dev/null 2>&1; then eval \"$(command wt config shell init zsh)\"; fi\n",
)
.unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("uninstall")
.arg("--dry-run")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
let bash_content = fs::read_to_string(&bash_config_path).unwrap();
assert!(
bash_content.contains("wt config shell init"),
"Bash config should not be modified with --dry-run"
);
let zsh_content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
zsh_content.contains("wt config shell init"),
"Zsh config should not be modified with --dry-run"
);
}
#[cfg(all(unix, feature = "shell-integration-tests"))]
mod pty_tests {
use crate::common::pty::exec_cmd_in_pty_prompted;
use crate::common::{
TestRepo, add_pty_filters, configure_pty_command, repo, temp_home, wt_bin,
};
use insta::assert_snapshot;
use portable_pty::CommandBuilder;
use rstest::rstest;
use std::fs;
use tempfile::TempDir;
fn exec_install_in_pty(temp_home: &TempDir, repo: &TestRepo, input: &str) -> (String, i32) {
let mut cmd = CommandBuilder::new(wt_bin());
cmd.arg("-C");
cmd.arg(repo.root_path());
cmd.arg("config");
cmd.arg("shell");
cmd.arg("install");
cmd.cwd(repo.root_path());
configure_pty_command(&mut cmd);
cmd.env("HOME", temp_home.path());
cmd.env("XDG_CONFIG_HOME", temp_home.path().join(".config"));
cmd.env("WORKTRUNK_TEST_NUSHELL_ENV", "0");
cmd.env("SHELL", "/bin/zsh");
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
exec_cmd_in_pty_prompted(cmd, &[input], "[y/N")
}
fn install_pty_settings(temp_home: &TempDir) -> insta::Settings {
let mut settings = insta::Settings::clone_current();
add_pty_filters(&mut settings);
settings.add_filter(r"(\[y/N/\?\](?:\x1b\[22m)?) [yn]", "$1 ");
settings.add_filter(r"^[yn]\n", "");
settings.add_filter(®ex::escape(&temp_home.path().to_string_lossy()), "~");
settings
}
#[rstest]
fn test_install_preview_with_gutter(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let (output, exit_code) = exec_install_in_pty(&temp_home, &repo, "y\n");
assert_eq!(exit_code, 0);
install_pty_settings(&temp_home).bind(|| {
assert_snapshot!(output.trim_start_matches('\n'));
});
}
#[rstest]
fn test_install_preview_declined(repo: TestRepo, temp_home: TempDir) {
let zshrc_path = temp_home.path().join(".zshrc");
fs::write(&zshrc_path, "# Existing config\n").unwrap();
let (output, exit_code) = exec_install_in_pty(&temp_home, &repo, "n\n");
assert_eq!(exit_code, 1);
install_pty_settings(&temp_home).bind(|| {
assert_snapshot!(output.trim_start_matches('\n'));
});
let content = fs::read_to_string(&zshrc_path).unwrap();
assert!(
!content.contains("wt config shell init"),
"File should not be modified when user declines"
);
}
}
#[rstest]
fn test_configure_shell_nushell(repo: TestRepo, temp_home: TempDir) {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/nu");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("nu")
.arg("--yes")
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(
output.status.success(),
"Install should succeed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Created shell extension for") && stderr.contains("nu"),
"Output should show nushell was created:\n{}",
stderr
);
let home = std::fs::canonicalize(temp_home.path()).unwrap();
let nu_config = home
.join(".config")
.join("nushell")
.join("vendor")
.join("autoload")
.join("wt.nu");
assert!(
nu_config.exists(),
"wt.nu should be created at {:?}",
nu_config
);
let content = fs::read_to_string(&nu_config).unwrap();
assert!(
content.contains("def --env --wrapped wt"),
"Should contain nushell function definition: {}",
content
);
}
#[rstest]
fn test_uninstall_shell_nushell(repo: TestRepo, temp_home: TempDir) {
let home = std::fs::canonicalize(temp_home.path()).unwrap();
let nu_config = home
.join(".config")
.join("nushell")
.join("vendor")
.join("autoload")
.join("wt.nu");
let mut install_cmd = wt_command();
repo.configure_wt_cmd(&mut install_cmd);
set_temp_home_env(&mut install_cmd, temp_home.path());
install_cmd.env("SHELL", "/bin/nu");
install_cmd
.args(["config", "shell", "install", "nu", "--yes"])
.current_dir(repo.root_path());
let install_output = install_cmd.output().expect("Failed to execute install");
assert!(
install_output.status.success(),
"Install should succeed:\nstderr: {}",
String::from_utf8_lossy(&install_output.stderr)
);
assert!(
nu_config.exists(),
"wt.nu should exist after install at {:?}",
nu_config
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/nu");
cmd.args(["config", "shell", "uninstall", "nu", "--yes"])
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute uninstall");
assert!(
output.status.success(),
"Uninstall should succeed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Removed shell extension for") && stderr.contains("nu"),
"Output should show nushell was removed:\n{}",
stderr
);
assert!(
!nu_config.exists(),
"wt.nu should be deleted after uninstall: {:?}",
nu_config
);
}
#[rstest]
fn test_uninstall_nushell_cleans_all_candidate_locations(repo: TestRepo, temp_home: TempDir) {
let home = std::fs::canonicalize(temp_home.path()).unwrap();
let mut install_cmd = wt_command();
repo.configure_wt_cmd(&mut install_cmd);
set_temp_home_env(&mut install_cmd, temp_home.path());
install_cmd.env("SHELL", "/bin/nu");
install_cmd
.args(["config", "shell", "install", "nu", "--yes"])
.current_dir(repo.root_path());
let install_output = install_cmd.output().expect("Failed to execute install");
assert!(
install_output.status.success(),
"Install should succeed:\nstderr: {}",
String::from_utf8_lossy(&install_output.stderr)
);
let primary_config = home
.join(".config")
.join("nushell")
.join("vendor")
.join("autoload")
.join("wt.nu");
assert!(primary_config.exists(), "Primary config should exist");
let secondary_dir = home
.join("custom-config")
.join("nushell")
.join("vendor")
.join("autoload");
fs::create_dir_all(&secondary_dir).unwrap();
let secondary_config = secondary_dir.join("wt.nu");
fs::copy(&primary_config, &secondary_config).unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("HOME", &home);
cmd.env("USERPROFILE", &home);
cmd.env("XDG_CONFIG_HOME", home.join("custom-config"));
cmd.env("APPDATA", home.join("custom-config"));
cmd.env("SHELL", "/bin/nu");
cmd.args(["config", "shell", "uninstall", "nu", "--yes"])
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute uninstall");
assert!(
output.status.success(),
"Uninstall should succeed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!primary_config.exists(),
"Primary config at ~/.config/nushell should be deleted: {primary_config:?}"
);
assert!(
!secondary_config.exists(),
"Secondary config at custom XDG path should be deleted: {secondary_config:?}"
);
}
#[rstest]
#[cfg_attr(
windows,
ignore = "Windows uses Documents folder which can't be easily overridden"
)]
fn test_powershell_env_detection(repo: TestRepo, temp_home: TempDir) {
let powershell_dir = temp_home.path().join(".config/powershell");
fs::create_dir_all(&powershell_dir).unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_POWERSHELL_ENV", "1");
cmd.env("SHELL", "/bin/bash");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(output.status.success(), "Command should succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Created shell extension for") && stderr.contains("powershell"),
"Output should show PowerShell was created:\n{}",
stderr
);
let profile_path = powershell_dir.join("Microsoft.PowerShell_profile.ps1");
assert!(
profile_path.exists(),
"PowerShell profile should be created at {:?}",
profile_path
);
let content = fs::read_to_string(&profile_path).unwrap();
assert!(
content.contains("wt config shell init powershell"),
"Profile should contain shell init: {}",
content
);
}
#[rstest]
fn test_nushell_auto_detection_creates_vendor_autoload(repo: TestRepo, temp_home: TempDir) {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_NUSHELL_ENV", "1");
cmd.env("SHELL", "/bin/zsh");
cmd.arg("config")
.arg("shell")
.arg("install")
.arg("--yes")
.current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute command");
assert!(
output.status.success(),
"Command should succeed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Created shell extension for") && stderr.contains("nu"),
"Nushell should be auto-configured when detected:\n{}",
stderr
);
assert!(
!stderr.contains("Skipped nu"),
"Nushell should not be skipped when detected:\n{}",
stderr
);
let home = std::fs::canonicalize(temp_home.path()).unwrap();
let nu_config = home
.join(".config")
.join("nushell")
.join("vendor")
.join("autoload")
.join("wt.nu");
assert!(
nu_config.exists(),
"wt.nu should be created at {:?}",
nu_config
);
let content = fs::read_to_string(&nu_config).unwrap();
assert!(
content.contains("def --env --wrapped wt"),
"Should contain nushell function definition: {}",
content
);
}
#[rstest]
fn test_config_show_detects_nushell_integration(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let mut install_cmd = wt_command();
repo.configure_wt_cmd(&mut install_cmd);
set_temp_home_env(&mut install_cmd, temp_home.path());
install_cmd.env("SHELL", "/bin/nu");
install_cmd
.args(["config", "shell", "install", "nu", "--yes"])
.current_dir(repo.root_path());
let install_output = install_cmd.output().expect("Failed to execute install");
assert!(
install_output.status.success(),
"Install should succeed:\nstderr: {}",
String::from_utf8_lossy(&install_output.stderr)
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("SHELL", "/bin/nu");
cmd.args(["config", "show"]).current_dir(repo.root_path());
let output = cmd.output().expect("Failed to execute config show");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("wt.nu") || stderr.contains("nushell"),
"config show should detect nushell integration:\n{stderr}"
);
}