use crate::common::{
TestRepo, repo, set_temp_home_env, set_xdg_config_path, setup_home_snapshot_settings,
setup_snapshot_settings, setup_snapshot_settings_with_home, temp_home, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
use tempfile::TempDir;
#[rstest]
fn test_config_show_with_project_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
fs::write(
global_config_dir.join("approvals.toml"),
r#"[projects."test-project"]
approved-commands = ["npm install"]
"#,
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
r#"post-create = "npm install"
[post-start]
server = "npm run dev"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_no_project_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_with_system_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
r#"[merge]
squash = true
verify = true
[commit.generation]
command = "company-llm-tool"
"#,
)
.unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_system_config_values_used_as_defaults(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
"worktree-path = \".worktrees/{{ branch | sanitize }}\"\n",
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.args(["switch", "--create", "test-feature"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"switch --create should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let expected_path = repo.root_path().join(".worktrees").join("test-feature");
assert!(
expected_path.exists(),
"Worktree should be created at system config template path: {}",
expected_path.display()
);
}
#[rstest]
fn test_user_config_overrides_system_config(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
"worktree-path = \".worktrees/system/{{ branch | sanitize }}\"\n",
)
.unwrap();
let user_config_dir = tempfile::tempdir().unwrap();
let user_config_path = user_config_dir.path().join("config.toml");
fs::write(
&user_config_path,
"worktree-path = \".worktrees/user/{{ branch | sanitize }}\"\n",
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.env("WORKTRUNK_CONFIG_PATH", &user_config_path);
cmd.args(["switch", "--create", "test-feature"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"switch --create should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let user_path = repo.root_path().join(".worktrees/user/test-feature");
let system_path = repo.root_path().join(".worktrees/system/test-feature");
assert!(
user_path.exists(),
"Worktree should be at user config template path: {}",
user_path.display()
);
assert!(
!system_path.exists(),
"Worktree should NOT be at system config template path"
);
}
#[rstest]
fn test_system_and_user_hooks_deep_merged(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
r#"[pre-merge]
company-lint = "company-lint-tool"
"#,
)
.unwrap();
let user_config_dir = tempfile::tempdir().unwrap();
let user_config_path = user_config_dir.path().join("config.toml");
fs::write(
&user_config_path,
r#"[pre-merge]
my-lint = "my-lint-tool"
"#,
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.env("WORKTRUNK_CONFIG_PATH", &user_config_path);
cmd.args(["hook", "show", "pre-merge"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"hook show should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("company-lint-tool"),
"System hook should be preserved with different name, got:\n{stdout}"
);
assert!(
stdout.contains("my-lint-tool"),
"User hook should be present, got:\n{stdout}"
);
}
#[rstest]
fn test_user_hook_replaces_same_named_system_hook(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
r#"[pre-merge]
lint = "company-lint-tool"
"#,
)
.unwrap();
let user_config_dir = tempfile::tempdir().unwrap();
let user_config_path = user_config_dir.path().join("config.toml");
fs::write(
&user_config_path,
r#"[pre-merge]
lint = "my-lint-tool"
"#,
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.env("WORKTRUNK_CONFIG_PATH", &user_config_path);
cmd.args(["hook", "show", "pre-merge"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"hook show should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("my-lint-tool"),
"User's hook command should be present, got:\n{stdout}"
);
assert!(
!stdout.contains("company-lint-tool"),
"System's hook command should be replaced by user's same-named hook, got:\n{stdout}"
);
}
#[rstest]
fn test_system_config_hooks_preserved_when_user_doesnt_override(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
r#"[pre-merge]
company-lint = "company-lint-tool"
[pre-commit]
company-format = "company-format-tool"
"#,
)
.unwrap();
let user_config_dir = tempfile::tempdir().unwrap();
let user_config_path = user_config_dir.path().join("config.toml");
fs::write(
&user_config_path,
r#"[pre-merge]
my-lint = "my-lint-tool"
"#,
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.env("WORKTRUNK_CONFIG_PATH", &user_config_path);
cmd.args(["hook", "show", "pre-commit"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"hook show should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("company-format-tool"),
"System's pre-commit hook should be preserved when user doesn't override it, got:\n{stdout}"
);
}
#[rstest]
fn test_config_show_system_config_hint_under_user_config(repo: TestRepo, temp_home: TempDir) {
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n",
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env_remove("WORKTRUNK_SYSTEM_CONFIG_PATH");
cmd.arg("config").arg("show").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("SYSTEM CONFIG"),
"Should not show SYSTEM CONFIG section when absent, got:\n{stdout}"
);
assert!(
stdout.contains("Optional system config not found")
&& stdout.contains("worktrunk/config.toml"),
"Expected system config hint in output, got:\n{stdout}"
);
}
#[rstest]
fn test_system_config_found_via_xdg_config_dirs(repo: TestRepo) {
let xdg_dir = tempfile::tempdir().unwrap();
let config_dir = xdg_dir.path().join("worktrunk");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("config.toml"),
r#"worktree-path = "/xdg-org/{{ repo }}/{{ branch | sanitize }}"
"#,
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env_remove("WORKTRUNK_SYSTEM_CONFIG_PATH");
cmd.env("XDG_CONFIG_DIRS", xdg_dir.path());
cmd.arg("list")
.arg("--format=json")
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let worktrees = json.as_array().unwrap();
for wt in worktrees {
if wt["is_primary"].as_bool() == Some(false) {
let path = wt["path"].as_str().unwrap();
assert!(
path.contains("/xdg-org/"),
"Expected XDG_CONFIG_DIRS system config, got: {path}"
);
}
}
}
#[rstest]
fn test_system_config_xdg_dirs_set_but_no_config_found(repo: TestRepo) {
let empty_xdg_dir = tempfile::tempdir().unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env_remove("WORKTRUNK_SYSTEM_CONFIG_PATH");
cmd.env("XDG_CONFIG_DIRS", empty_xdg_dir.path());
cmd.arg("list")
.arg("--format=json")
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let worktrees = json.as_array().unwrap();
for wt in worktrees {
if wt["is_primary"].as_bool() == Some(false) {
let path = wt["path"].as_str().unwrap();
assert!(
!path.contains("/xdg-org/"),
"Should not use XDG system config path, got: {path}"
);
}
}
}
#[rstest]
fn test_config_show_empty_system_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(&system_config_path, "").unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_invalid_system_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(&system_config_path, "invalid = [toml\n").unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_system_config_unknown_keys_warning_during_load(repo: TestRepo) {
let system_config_dir = tempfile::tempdir().unwrap();
let system_config_path = system_config_dir.path().join("config.toml");
fs::write(
&system_config_path,
"[totally-unknown-section]\nkey = \"value\"",
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config_path);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"Command should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("has unknown field"),
"Expected unknown field warning from system config load, got: {stderr}"
);
}
#[rstest]
fn test_config_show_outside_git_repo(mut repo: TestRepo, temp_home: TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(temp_dir.path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_zsh_compinit_warning(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".zshrc"),
r#"# wt integration but no compinit!
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_TEST_COMPINIT_MISSING", "1");
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_partial_shell_config_shows_hint(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".bashrc"),
r#"# Some bash config
export PATH="$HOME/bin:$PATH"
"#,
)
.unwrap();
fs::write(
temp_home.path().join(".zshrc"),
r#"# wt integration
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_COMPINIT_CONFIGURED", "1");
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_fish_with_completions(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
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 = temp_home.path().join(".config/fish/completions");
fs::create_dir_all(&completions).unwrap();
fs::write(completions.join("wt.fish"), "# fish completions\n").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_fish_without_completions(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
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_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_fish_outdated_wrapper(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
fs::write(
functions.join("wt.fish"),
"# worktrunk shell integration for fish\nfunction wt\n command wt-old $argv\nend\n",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_nushell_outdated_wrapper(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let autoload = temp_home.path().join(".config/nushell/vendor/autoload");
fs::create_dir_all(&autoload).unwrap();
fs::write(
autoload.join("wt.nu"),
"# worktrunk shell integration for nushell\ndef --wrapped wt [...args] {\n command wt-old ...$args\n}\n",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_zsh_compinit_correct_order(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".zshrc"),
r#"# compinit enabled
autoload -Uz compinit && compinit
# wt integration
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_COMPINIT_CONFIGURED", "1");
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
#[cfg(all(unix, feature = "shell-integration-tests"))]
fn test_config_show_zsh_compinit_real_probe_warns_when_missing(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".zshrc"),
r#"unset -f compdef 2>/dev/null
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("PATH", "/usr/bin:/bin");
cmd.env(
"ZDOTDIR",
crate::common::canonicalize(temp_home.path())
.unwrap_or_else(|_| temp_home.path().to_path_buf()),
);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Completions won't work; add to"),
"Expected compinit warning, got:\n{stdout}"
);
});
}
#[rstest]
#[cfg(all(unix, feature = "shell-integration-tests"))]
fn test_config_show_zsh_compinit_no_warning_when_present(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".zshrc"),
r#"compdef() { :; }
if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init zsh)"; fi
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("PATH", "/usr/bin:/bin");
cmd.env(
"ZDOTDIR",
crate::common::canonicalize(temp_home.path())
.unwrap_or_else(|_| temp_home.path().to_path_buf()),
);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("Completions won't work; add to"),
"Expected no compinit warning, got:\n{stdout}"
);
});
}
#[rstest]
fn test_config_show_warns_unknown_project_keys(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
"[post-merge-command]\ndeploy = \"task deploy\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_warns_unknown_user_keys(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n\n[commit-gen]\ncommand = \"llm\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_unknown_project_key_warning_during_load(repo: TestRepo, temp_home: TempDir) {
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
"[invalid-section-name]\nkey = \"value\"",
)
.unwrap();
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"Command should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("has unknown field"),
"Expected unknown field warning during config load, got: {stderr}"
);
}
#[rstest]
fn test_config_show_suggests_user_config_for_commit_generation(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
"[commit-generation]\ncommand = \"claude\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_suggests_project_config_for_ci(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n\n[ci]\nplatform = \"github\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_invalid_user_toml(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"this is not valid toml {{{",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_invalid_project_toml(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("wt.toml"), "invalid = [unclosed bracket").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_full_not_configured(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_TEST_LATEST_VERSION", env!("CARGO_PKG_VERSION"));
cmd.arg("config")
.arg("show")
.arg("--full")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_full_command_not_found(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[commit.generation]
command = "nonexistent-llm-command-12345 -m test-model"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_TEST_LATEST_VERSION", env!("CARGO_PKG_VERSION"));
cmd.arg("config")
.arg("show")
.arg("--full")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_full_update_available(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_TEST_LATEST_VERSION", "99.0.0");
cmd.arg("config")
.arg("show")
.arg("--full")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_full_version_check_unavailable(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
"worktree-path = \"../{{ repo }}.{{ branch }}\"",
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.env("WORKTRUNK_TEST_LATEST_VERSION", "error");
cmd.arg("config")
.arg("show")
.arg("--full")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_github_remote(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.git_command()
.args([
"remote",
"add",
"origin",
"https://github.com/example/repo.git",
])
.run()
.unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_gitlab_remote(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.git_command()
.args([
"remote",
"add",
"origin",
"https://gitlab.com/example/repo.git",
])
.run()
.unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_empty_project_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("wt.toml"), "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_whitespace_only_project_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("wt.toml"), " \n\t\n ").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_no_user_config(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_unmatched_candidate_warning(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".bashrc"),
r#"# Some bash config
export PATH="$HOME/bin:$PATH"
alias wt="git worktree"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_COMPINIT_CONFIGURED", "1");
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_unmatched_candidate_not_suppressed_by_wrapper(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let functions = temp_home.path().join(".config/fish/functions");
fs::create_dir_all(&functions).unwrap();
fs::write(
functions.join("wt.fish"),
"# worktrunk shell integration for fish\nfunction wt\n command wt-old $argv\nend\n",
)
.unwrap();
fs::write(
temp_home.path().join(".bashrc"),
r#"# Some bash config
alias wt="git worktree"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_COMPINIT_CONFIGURED", "1");
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_deprecated_template_variables_show_warning(repo: TestRepo, temp_home: TempDir) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
post-create = "ln -sf {{ repo_root }}/node_modules {{ worktree }}/node_modules"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert_cmd_snapshot!(cmd);
});
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert!(cmd.output().unwrap().status.success());
let migrated = fs::read_to_string(config_path).unwrap();
assert!(migrated.contains("{{ repo }}"));
assert!(migrated.contains("{{ repo_path }}"));
assert!(migrated.contains("{{ worktree_path }}"));
assert!(
!config_path.with_extension("toml.new").exists(),
"config update must not leave a .new file"
);
}
#[rstest]
fn test_deprecated_template_variables_verbose_shows_content(repo: TestRepo, temp_home: TempDir) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
post-create = "ln -sf {{ repo_root }}/node_modules {{ worktree }}/node_modules"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["-v", "list"]).current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_wt_list_never_writes_migration_file(repo: TestRepo, temp_home: TempDir) {
let project_config_dir = repo.root_path().join(".config");
fs::create_dir_all(&project_config_dir).unwrap();
let project_config_path = project_config_dir.join("wt.toml");
let original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#;
fs::write(&project_config_path, original).unwrap();
for _ in 0..2 {
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt list should succeed: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
assert!(
!project_config_path.with_extension("toml.new").exists(),
"wt list must not write a .new migration file"
);
assert_eq!(
fs::read_to_string(&project_config_path).unwrap(),
original,
"wt list must not modify the config"
);
}
#[rstest]
fn test_fixing_deprecated_config_then_reintroducing_still_warns(
repo: TestRepo,
temp_home: TempDir,
) {
let project_config_dir = repo.root_path().join(".config");
fs::create_dir_all(&project_config_dir).unwrap();
let project_config_path = project_config_dir.join("wt.toml");
fs::write(
&project_config_path,
r#"post-create = "ln -sf {{ main_worktree }}/node_modules"
"#,
)
.unwrap();
{
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert!(cmd.output().unwrap().status.success());
}
fs::write(
&project_config_path,
r#"pre-start = "ln -sf {{ repo }}/node_modules"
"#,
)
.unwrap();
{
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("deprecated"),
"No deprecation warning for clean config"
);
}
fs::write(
&project_config_path,
r#"post-create = "cd {{ worktree }} && npm install"
"#,
)
.unwrap();
{
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("is deprecated"),
"New deprecation should show warning, got: {stderr}"
);
}
assert!(
!project_config_path.with_extension("toml.new").exists(),
"wt list must never write a .new file"
);
}
#[rstest]
fn test_deprecated_project_config_silent_in_feature_worktree(repo: TestRepo, temp_home: TempDir) {
{
let mut cmd = repo.wt_command();
cmd.args(["switch", "--create", "feature"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"Creating feature worktree should succeed: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
let feature_path = repo.root_path().parent().unwrap().join(format!(
"{}.feature",
repo.root_path().file_name().unwrap().to_string_lossy()
));
let feature_config_dir = feature_path.join(".config");
fs::create_dir_all(&feature_config_dir).unwrap();
fs::write(
feature_config_dir.join("wt.toml"),
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#,
)
.unwrap();
{
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(&feature_path);
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt list from feature worktree should succeed: {:?}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("deprecated template variables"),
"Deprecation warning should NOT appear in feature worktree, got: {stderr}"
);
assert!(
!stderr.contains("Wrote migrated"),
"Migration file should NOT be written from feature worktree, got: {stderr}"
);
}
}
#[rstest]
fn test_user_config_deprecation_warns_without_writing(repo: TestRepo, temp_home: TempDir) {
repo.write_test_config(
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#,
);
let user_config_path = repo.test_config_path().to_path_buf();
let original = fs::read_to_string(&user_config_path).unwrap();
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", &user_config_path);
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "wt list should succeed: {stderr}");
assert!(
stderr.contains("User config:") && stderr.contains("is deprecated"),
"Should emit user-config deprecation warning, got: {stderr}"
);
assert!(
!user_config_path.with_extension("toml.new").exists(),
"wt list must not write a .new migration file"
);
assert_eq!(
fs::read_to_string(&user_config_path).unwrap(),
original,
"wt list must not modify user config"
);
}
#[rstest]
fn test_config_show_shell_integration_active(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let directive_file = temp_home.path().join("directive");
fs::write(&directive_file, "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_DIRECTIVE_CD_FILE", &directive_file);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_shell_active_but_not_in_config_file(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n",
)
.unwrap();
fs::write(temp_home.path().join(".zshrc"), "# my zsh config\n").unwrap();
let directive_file = temp_home.path().join("directive");
fs::write(&directive_file, "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_DIRECTIVE_CD_FILE", &directive_file);
cmd.env("SHELL", "/bin/zsh");
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_plugin_installed(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_installed();
TestRepo::setup_plugin_installed(temp_home.path());
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_claude_available_plugin_not_installed(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_installed();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_statusline_configured(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_installed();
TestRepo::setup_plugin_installed(temp_home.path());
TestRepo::setup_statusline_configured(temp_home.path());
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_opencode_available_plugin_not_installed(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_opencode_installed();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_opencode_plugin_installed(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_opencode_installed();
TestRepo::setup_opencode_plugin_installed(temp_home.path());
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_opencode_plugin_outdated(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_opencode_installed();
let plugins_dir = temp_home.path().join("opencode-config/plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(
plugins_dir.join("worktrunk.ts"),
"// outdated plugin content\n",
)
.unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_opencode_install_creates_plugin(temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "install", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugin_path = canonical_home.join("opencode-config/plugins/worktrunk.ts");
assert!(
plugin_path.exists(),
"Plugin file should exist after install"
);
let content = fs::read_to_string(&plugin_path).unwrap();
assert!(
content.contains("session.status"),
"Plugin should contain event handler"
);
}
#[rstest]
fn test_opencode_install_already_installed(temp_home: TempDir) {
TestRepo::setup_opencode_plugin_installed(temp_home.path());
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "install", "--yes"]);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_opencode_install_updates_outdated(temp_home: TempDir) {
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugins_dir = canonical_home.join("opencode-config/plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(plugins_dir.join("worktrunk.ts"), "// outdated\n").unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "install", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let content = fs::read_to_string(plugins_dir.join("worktrunk.ts")).unwrap();
assert!(
content.contains("session.status"),
"Plugin should be updated to current content"
);
}
#[rstest]
fn test_opencode_uninstall_removes_plugin(temp_home: TempDir) {
TestRepo::setup_opencode_plugin_installed(temp_home.path());
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugin_path = canonical_home.join("opencode-config/plugins/worktrunk.ts");
assert!(plugin_path.exists(), "Plugin should exist before uninstall");
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "uninstall", "--yes"]);
assert_cmd_snapshot!(cmd);
});
assert!(
!plugin_path.exists(),
"Plugin file should be removed after uninstall"
);
}
#[rstest]
fn test_opencode_uninstall_not_installed(temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "uninstall", "--yes"]);
assert_cmd_snapshot!(cmd);
});
}
#[cfg(target_os = "linux")]
#[rstest]
fn test_opencode_install_uses_dirs_fallback(temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env_remove("OPENCODE_CONFIG_DIR");
cmd.args(["config", "plugins", "opencode", "install", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugin_path = canonical_home.join(".config/opencode/plugins/worktrunk.ts");
assert!(
plugin_path.exists(),
"Plugin file should exist at dirs::config_dir() fallback path: {}",
plugin_path.display()
);
}
#[rstest]
fn test_opencode_install_prompt_declined(temp_home: TempDir) {
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "install"]);
assert_cmd_snapshot!(cmd);
});
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugin_path = canonical_home.join("opencode-config/plugins/worktrunk.ts");
assert!(
!plugin_path.exists(),
"Plugin should NOT be installed when prompt is declined"
);
}
#[rstest]
fn test_opencode_uninstall_prompt_declined(temp_home: TempDir) {
TestRepo::setup_opencode_plugin_installed(temp_home.path());
let canonical_home =
crate::common::canonicalize(temp_home.path()).unwrap_or_else(|_| temp_home.path().into());
let plugin_path = canonical_home.join("opencode-config/plugins/worktrunk.ts");
assert!(plugin_path.exists(), "Plugin should exist before test");
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "plugins", "opencode", "uninstall"]);
assert_cmd_snapshot!(cmd);
});
assert!(
plugin_path.exists(),
"Plugin should still exist when uninstall prompt is declined"
);
}
#[rstest]
fn test_config_show_powershell_detected_via_psmodulepath(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
fs::write(
temp_home.path().join(".bashrc"),
r#"if command -v wt >/dev/null 2>&1; then eval "$(command wt config shell init bash)"; fi
"#,
)
.unwrap();
let ps_profile_dir = temp_home.path().join(".config").join("powershell");
fs::create_dir_all(&ps_profile_dir).unwrap();
fs::write(
ps_profile_dir.join("Microsoft.PowerShell_profile.ps1"),
"if (Get-Command wt -ErrorAction SilentlyContinue) { Invoke-Expression (& wt config shell init powershell | Out-String) }\n",
)
.unwrap();
let mut settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.add_filter(r"(?m)^.*Get-Command.*\n", "");
settings.add_filter(r"(?m)^.*To configure, run.*\n", "");
settings.add_filter(r"\n\n\n", "\n\n");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_TEST_POWERSHELL_ENV", "1");
cmd.env_remove("SHELL");
cmd.env(
"PSModulePath",
r"C:\Users\user\Documents\PowerShell\Modules",
);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_deprecated_commit_generation_section_shows_warning(repo: TestRepo, temp_home: TempDir) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[commit-generation]
command = "llm"
args = ["-m", "haiku"]
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert_cmd_snapshot!(cmd);
});
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert!(cmd.output().unwrap().status.success());
let migrated = fs::read_to_string(config_path).unwrap();
assert!(
migrated.contains("[commit.generation]"),
"Should rename [commit-generation] to [commit.generation]"
);
assert!(
migrated.contains("command = \"llm -m haiku\""),
"Should merge args into command"
);
assert!(!migrated.contains("[commit-generation]"));
assert!(!migrated.contains("args ="));
}
#[rstest]
fn test_deprecated_commit_generation_project_level_shows_warning(
repo: TestRepo,
temp_home: TempDir,
) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[projects."github.com/example/repo".commit-generation]
command = "llm -m gpt-4"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert_cmd_snapshot!(cmd);
});
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert!(cmd.output().unwrap().status.success());
let migrated = fs::read_to_string(config_path).unwrap();
assert!(
migrated.contains("[projects.\"github.com/example/repo\".commit.generation]"),
"Should rename project-level section"
);
}
#[rstest]
fn test_config_show_displays_deprecation_details(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
post-create = "ln -sf {{ repo_root }}/node_modules"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
assert!(
!config_path.with_extension("toml.new").exists(),
"wt config show must not leave a .new file behind"
);
}
#[rstest]
fn test_config_show_from_linked_worktree_shows_main_worktree_hint(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let project_config_dir = repo.root_path().join(".config");
fs::create_dir_all(&project_config_dir).unwrap();
fs::write(
project_config_dir.join("wt.toml"),
r#"post-create = "ln -sf {{ main_worktree }}/node_modules"
"#,
)
.unwrap();
repo.commit("Add deprecated project config");
let feature_path = repo.root_path().parent().unwrap().join("feature-test");
repo.run_git(&[
"worktree",
"add",
feature_path.to_str().unwrap(),
"-b",
"feature-test",
]);
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(&feature_path);
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_displays_project_commit_generation_deprecations(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[projects."github.com/example/repo".commit-generation]
command = "llm -m gpt-4"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
assert!(
!config_path.with_extension("toml.new").exists(),
"wt config show must not leave a .new file behind"
);
}
#[rstest]
fn test_config_update_copies_approved_commands_to_approvals_file(
repo: TestRepo,
temp_home: TempDir,
) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[projects."github.com/user/repo"]
approved-commands = ["npm install", "npm test"]
"#,
)
.unwrap();
{
let mut cmd = repo.wt_command();
cmd.arg("list").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
assert!(cmd.output().unwrap().status.success());
}
assert!(
!config_path.with_file_name("approvals.toml").exists(),
"wt list must not copy approvals"
);
assert!(
!config_path.with_extension("toml.new").exists(),
"wt list must not write .new file"
);
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_CONFIG_PATH", config_path);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"config update should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let migrated = fs::read_to_string(config_path).unwrap();
assert!(
!migrated.contains("approved-commands"),
"config.toml should no longer contain approved-commands: {migrated}"
);
assert!(
!config_path.with_extension("toml.new").exists(),
"config update must not leave a .new file behind"
);
let approvals_file = config_path.with_file_name("approvals.toml");
assert!(approvals_file.exists(), "approvals.toml should be created");
let approvals = fs::read_to_string(&approvals_file).unwrap();
assert!(
approvals.contains("npm install") && approvals.contains("npm test"),
"approvals.toml should carry both commands: {approvals}"
);
}
#[rstest]
fn test_config_update_applies_project_config_migration(repo: TestRepo) {
repo.write_project_config(
r#"post-create = "ln -sf {{ main_worktree }}/node_modules"
"#,
);
repo.commit("Add deprecated project config");
let project_config_path = repo.root_path().join(".config").join("wt.toml");
let output = repo
.wt_command()
.args(["config", "update", "--yes"])
.output()
.unwrap();
assert!(
output.status.success(),
"config update should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let updated = fs::read_to_string(&project_config_path).unwrap();
assert!(updated.contains("pre-start"));
assert!(updated.contains("{{ repo }}"));
assert!(!updated.contains("post-create"));
}
#[rstest]
fn test_config_update_clean_project_config_is_noop(repo: TestRepo) {
repo.write_project_config(
r#"pre-start = "echo ready"
"#,
);
repo.commit("Add clean project config");
let output = repo
.wt_command()
.args(["config", "update"])
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("No deprecated settings found"),
"Expected no-op message, got: {stderr}"
);
}
#[rstest]
fn test_config_update_project_config_from_linked_worktree_shows_hint(repo: TestRepo) {
repo.write_project_config(
r#"post-create = "ln -sf {{ main_worktree }}/node_modules"
"#,
);
repo.commit("Add deprecated project config");
let project_config_path = repo.root_path().join(".config").join("wt.toml");
let before = fs::read_to_string(&project_config_path).unwrap();
let feature_path = repo.root_path().parent().unwrap().join("feature-test");
repo.run_git(&[
"worktree",
"add",
feature_path.to_str().unwrap(),
"-b",
"feature-test",
]);
let output = repo
.wt_command()
.args(["config", "update", "--yes"])
.current_dir(&feature_path)
.output()
.unwrap();
assert!(output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("To update project config:"),
"Should hint at main worktree, got: {stderr}"
);
assert_eq!(
fs::read_to_string(&project_config_path).unwrap(),
before,
"Project config must not change when run from linked worktree"
);
}
#[rstest]
fn test_config_update_print_emits_both_configs(repo: TestRepo) {
let user_config_path = repo.test_config_path();
fs::write(
user_config_path,
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"post-create = "ln -sf {{ main_worktree }}/node_modules"
"#,
);
repo.commit("Add deprecated project config");
let output = repo
.wt_command()
.args(["config", "update", "--print"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("# User config"));
assert!(stdout.contains("# Project config"));
assert!(stdout.contains("{{ repo }}"));
assert!(stdout.contains("pre-start"));
}
#[rstest]
fn test_config_update_print_on_clean_config_is_silent(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let output = repo
.wt_command()
.args(["config", "update", "--print"])
.output()
.unwrap();
assert!(output.status.success());
assert!(
output.stdout.is_empty(),
"stdout must be empty on clean config"
);
}
#[rstest]
fn test_config_update_print_emits_migrated_without_writing(repo: TestRepo) {
let config_path = repo.test_config_path();
let original = r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#;
fs::write(config_path, original).unwrap();
let output = repo
.wt_command()
.args(["config", "update", "--print"])
.output()
.unwrap();
assert!(
output.status.success(),
"config update --print should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("{{ repo }}") && !stdout.contains("{{ main_worktree }}"),
"stdout should contain migrated content, got: {stdout}"
);
assert_eq!(
fs::read_to_string(config_path).unwrap(),
original,
"--print must not modify the config file"
);
assert!(
!config_path.with_extension("toml.new").exists(),
"--print must not write a .new file"
);
}
#[rstest]
fn test_config_update_no_deprecations(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"]);
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_update_applies_template_var_migration(repo: TestRepo) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
post-create = "ln -sf {{ repo_root }}/node_modules {{ worktree }}/node_modules"
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let updated = fs::read_to_string(config_path).unwrap();
assert!(
updated.contains("{{ repo }}"),
"Should replace main_worktree with repo"
);
assert!(
updated.contains("{{ repo_path }}"),
"Should replace repo_root with repo_path"
);
assert!(
updated.contains("{{ worktree_path }}"),
"Should replace worktree with worktree_path"
);
assert!(
!config_path.with_extension("toml.new").exists(),
".new file should be consumed by the update"
);
}
#[rstest]
fn test_config_show_displays_pre_hook_table_form_deprecation(
mut repo: TestRepo,
temp_home: TempDir,
) {
repo.setup_mock_ci_tools_unauthenticated();
let project_config_dir = repo.root_path().join(".config");
fs::create_dir_all(&project_config_dir).unwrap();
let project_config_path = project_config_dir.join("wt.toml");
fs::write(
&project_config_path,
r#"[pre-merge]
test = "cargo test"
lint = "cargo clippy"
[pre-start]
install = "npm ci"
env = "cp .env.example .env"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_displays_select_section_deprecation(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"[select]
pager = "delta --paging=never"
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_displays_no_ff_deprecation(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"[merge]
no-ff = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_show_displays_no_cd_deprecation(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
let config_path = global_config_dir.join("config.toml");
fs::write(
&config_path,
r#"[switch]
no-cd = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.arg("config").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
set_xdg_config_path(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_config_update_applies_commit_generation_migration(repo: TestRepo) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[commit-generation]
command = "llm"
args = ["-m", "haiku"]
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let updated = fs::read_to_string(config_path).unwrap();
assert!(
updated.contains("[commit.generation]"),
"Should rename section"
);
assert!(
updated.contains("command = \"llm -m haiku\""),
"Should merge args into command"
);
assert!(
!updated.contains("[commit-generation]"),
"Old section name should be gone"
);
assert!(!updated.contains("args ="), "Args field should be removed");
}
#[rstest]
fn test_config_update_applies_approved_commands_migration(repo: TestRepo) {
let config_path = repo.test_config_path();
fs::write(
config_path,
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[projects."github.com/user/repo"]
approved-commands = ["npm install", "npm test"]
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = repo.wt_command();
cmd.args(["config", "update", "--yes"]);
assert_cmd_snapshot!(cmd);
});
let updated = fs::read_to_string(config_path).unwrap();
assert!(
!updated.contains("approved-commands"),
"approved-commands should be removed from config"
);
let approvals_file = config_path.with_file_name("approvals.toml");
assert!(approvals_file.exists(), "approvals.toml should exist");
let approvals = fs::read_to_string(&approvals_file).unwrap();
assert!(approvals.contains("npm install"));
assert!(approvals.contains("npm test"));
}
#[rstest]
fn test_explicit_config_path_not_found_shows_warning(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("--config")
.arg("/nonexistent/worktrunk/config.toml")
.arg("list")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_invalid_plugins_json(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
let plugins_dir = temp_home.path().join(".claude/plugins");
fs::create_dir_all(&plugins_dir).unwrap();
fs::write(plugins_dir.join("installed_plugins.json"), "not valid json").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_already_installed(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
TestRepo::setup_plugin_installed(temp_home.path());
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_claude_not_found(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_uninstall(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
TestRepo::setup_plugin_installed(temp_home.path());
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "uninstall", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_uninstall_not_installed(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "uninstall", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_statusline(repo: TestRepo, temp_home: TempDir) {
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["config", "plugins", "claude", "install-statusline", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
let settings_path = temp_home.path().join(".claude/settings.json");
let content = fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
parsed["statusLine"]["command"],
"wt list statusline --format=claude-code"
);
});
}
#[rstest]
fn test_plugins_claude_install_statusline_already_configured(repo: TestRepo, temp_home: TempDir) {
TestRepo::setup_statusline_configured(temp_home.path());
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["config", "plugins", "claude", "install-statusline", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_statusline_preserves_existing(repo: TestRepo, temp_home: TempDir) {
let claude_dir = temp_home.path().join(".claude");
fs::create_dir_all(&claude_dir).unwrap();
fs::write(
claude_dir.join("settings.json"),
r#"{"existingKey":"existingValue"}"#,
)
.unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["config", "plugins", "claude", "install-statusline", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
let settings_path = temp_home.path().join(".claude/settings.json");
let content = fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed["existingKey"], "existingValue");
assert_eq!(
parsed["statusLine"]["command"],
"wt list statusline --format=claude-code"
);
});
}
#[rstest]
fn test_plugins_claude_install_statusline_empty_file(repo: TestRepo, temp_home: TempDir) {
let claude_dir = temp_home.path().join(".claude");
fs::create_dir_all(&claude_dir).unwrap();
fs::write(claude_dir.join("settings.json"), "").unwrap();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["config", "plugins", "claude", "install-statusline", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
let settings_path = temp_home.path().join(".claude/settings.json");
let content = fs::read_to_string(&settings_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
parsed["statusLine"]["command"],
"wt list statusline --format=claude-code"
);
});
}
#[rstest]
fn test_plugins_claude_install_command_fails(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins_failing();
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_install_second_step_fails(mut repo: TestRepo, temp_home: TempDir) {
use crate::common::mock_commands::{MockConfig, MockResponse};
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_installed();
let mock_bin = repo
.mock_bin_path()
.expect("setup_mock_ci_tools_unauthenticated creates mock-bin");
MockConfig::new("claude")
.command("plugin marketplace", MockResponse::exit(0))
.command(
"plugin install",
MockResponse::exit(1).with_stderr("error: install failed\n"),
)
.write(mock_bin);
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "install", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_plugins_claude_uninstall_command_fails(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins_failing();
TestRepo::setup_plugin_installed(temp_home.path());
let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
repo.configure_mock_commands(&mut cmd);
cmd.args(["config", "plugins", "claude", "uninstall", "--yes"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[cfg(all(unix, feature = "shell-integration-tests"))]
mod plugin_prompt_pty {
use crate::common::pty::{build_pty_command, exec_cmd_in_pty_prompted};
use crate::common::{TestRepo, repo, temp_home, wt_bin};
use rstest::rstest;
use std::path::PathBuf;
use tempfile::TempDir;
fn plugin_env_vars(repo: &TestRepo) -> Vec<(String, String)> {
let mut vars = repo.test_env_vars();
if let Some(mock_bin) = repo.mock_bin_path() {
vars.push((
"MOCK_CONFIG_DIR".to_string(),
mock_bin.display().to_string(),
));
let current_path =
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".to_string());
let mut paths: Vec<PathBuf> = std::env::split_paths(¤t_path).collect();
paths.insert(0, mock_bin.to_path_buf());
let new_path = std::env::join_paths(&paths).unwrap();
vars.retain(|(k, _)| k != "PATH");
vars.push(("PATH".to_string(), new_path.to_string_lossy().to_string()));
}
vars.push((
"WORKTRUNK_TEST_CLAUDE_INSTALLED".to_string(),
"1".to_string(),
));
vars
}
#[rstest]
fn test_plugins_claude_install_statusline_prompt_accept(repo: TestRepo, temp_home: TempDir) {
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "install-statusline"],
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, "Command should succeed. Output:\n{output}");
assert!(
output.contains("Configure statusline"),
"Should show prompt. Output:\n{output}"
);
assert!(
output.contains("Statusline configured"),
"Should confirm configuration. Output:\n{output}"
);
let settings_path = temp_home.path().join(".claude/settings.json");
let content = std::fs::read_to_string(&settings_path)
.expect("settings.json should exist after accepting prompt");
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
parsed["statusLine"]["command"],
"wt list statusline --format=claude-code"
);
}
#[rstest]
fn test_plugins_claude_install_statusline_prompt_preview_then_accept(
repo: TestRepo,
temp_home: TempDir,
) {
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "install-statusline"],
repo.root_path(),
&env_vars,
Some(temp_home.path()),
);
let (output, exit_code) = exec_cmd_in_pty_prompted(cmd, &["?\n", "y\n"], "[y/N");
assert_eq!(exit_code, 0, "Command should succeed. Output:\n{output}");
assert!(
output.contains("statusLine"),
"Should show preview with statusLine JSON. Output:\n{output}"
);
assert!(
output.contains("Statusline configured"),
"Should confirm configuration after preview. Output:\n{output}"
);
let settings_path = temp_home.path().join(".claude/settings.json");
let content = std::fs::read_to_string(&settings_path)
.expect("settings.json should exist after accepting prompt");
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(
parsed["statusLine"]["command"],
"wt list statusline --format=claude-code"
);
}
#[rstest]
fn test_plugins_claude_install_statusline_prompt_decline(repo: TestRepo, temp_home: TempDir) {
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "install-statusline"],
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:\n{output}");
assert!(
output.contains("Configure statusline"),
"Should show prompt. Output:\n{output}"
);
assert!(
!output.contains("Statusline configured"),
"Should NOT configure when declined. Output:\n{output}"
);
let settings_path = temp_home.path().join(".claude/settings.json");
assert!(
!settings_path.exists(),
"settings.json should not exist after declining"
);
}
#[rstest]
fn test_plugins_claude_install_prompt_accept(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "install"],
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, "Command should succeed. Output:\n{output}");
assert!(
output.contains("Install Worktrunk plugin"),
"Should show prompt. Output:\n{output}"
);
assert!(
output.contains("Plugin installed"),
"Should confirm installation. Output:\n{output}"
);
}
#[rstest]
fn test_plugins_claude_install_prompt_decline(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "install"],
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:\n{output}");
assert!(
output.contains("Install Worktrunk plugin"),
"Should show prompt. Output:\n{output}"
);
assert!(
!output.contains("Plugin installed"),
"Should NOT install when declined. Output:\n{output}"
);
}
#[rstest]
fn test_plugins_claude_uninstall_prompt_accept(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
TestRepo::setup_plugin_installed(temp_home.path());
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "uninstall"],
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, "Command should succeed. Output:\n{output}");
assert!(
output.contains("Uninstall Worktrunk plugin"),
"Should show prompt. Output:\n{output}"
);
assert!(
output.contains("Plugin uninstalled"),
"Should confirm uninstallation. Output:\n{output}"
);
}
#[rstest]
fn test_plugins_claude_uninstall_prompt_decline(mut repo: TestRepo, temp_home: TempDir) {
repo.setup_mock_ci_tools_unauthenticated();
repo.setup_mock_claude_with_plugins();
TestRepo::setup_plugin_installed(temp_home.path());
let env_vars = plugin_env_vars(&repo);
let cmd = build_pty_command(
wt_bin().to_str().unwrap(),
&["config", "plugins", "claude", "uninstall"],
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:\n{output}");
assert!(
output.contains("Uninstall Worktrunk plugin"),
"Should show prompt. Output:\n{output}"
);
assert!(
!output.contains("Plugin uninstalled"),
"Should NOT uninstall when declined. Output:\n{output}"
);
}
}
#[rstest]
fn test_config_show_json(repo: TestRepo, temp_home: TempDir) {
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n",
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_xdg_config_path(&mut cmd, temp_home.path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "show", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(json["user"]["exists"].as_bool().unwrap());
assert!(json["user"]["path"].as_str().is_some());
assert!(json["user"]["config"].is_object());
assert!(!json["project"]["exists"].as_bool().unwrap());
}
#[rstest]
fn test_config_show_json_with_project_config(repo: TestRepo, temp_home: TempDir) {
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(global_config_dir.join("config.toml"), "").unwrap();
let config_dir = repo.root_path().join(".config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(
config_dir.join("wt.toml"),
"[list]\nurl = \"http://localhost:3000\"\n",
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_xdg_config_path(&mut cmd, temp_home.path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "show", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(json["project"]["exists"].as_bool().unwrap());
assert!(json["project"]["config"].is_object());
}
#[rstest]
fn test_config_show_json_outside_repo(repo: TestRepo, temp_home: TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let global_config_dir = temp_home.path().join(".config").join("worktrunk");
fs::create_dir_all(&global_config_dir).unwrap();
fs::write(
global_config_dir.join("config.toml"),
"worktree-path = \"../{{ repo }}.{{ branch }}\"\n",
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_xdg_config_path(&mut cmd, temp_home.path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.args(["config", "show", "--format=json"])
.current_dir(temp_dir.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(json["user"]["exists"].as_bool().unwrap());
assert!(json["user"]["config"].is_object());
assert!(json["project"]["path"].is_null());
assert!(!json["project"]["exists"].as_bool().unwrap());
assert!(json["project"]["config"].is_null());
}
#[rstest]
fn test_project_config_path_env_var_override(repo: TestRepo, temp_home: TempDir) {
let in_repo_config = repo.root_path().join(".config").join("wt.toml");
fs::create_dir_all(in_repo_config.parent().unwrap()).unwrap();
fs::write(&in_repo_config, "post-create = \"in-repo-hook\"\n").unwrap();
let override_dir = tempfile::tempdir().unwrap();
let override_path = override_dir.path().join("override.toml");
fs::write(&override_path, "post-create = \"override-hook\"\n").unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_xdg_config_path(&mut cmd, temp_home.path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env("WORKTRUNK_PROJECT_CONFIG_PATH", &override_path);
cmd.args(["config", "show", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert_eq!(
json["project"]["config"]["post-create"], "override-hook",
"expected override config to be loaded, got: {}",
json["project"]
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
set_xdg_config_path(&mut cmd, temp_home.path());
set_temp_home_env(&mut cmd, temp_home.path());
cmd.env(
"WORKTRUNK_PROJECT_CONFIG_PATH",
override_dir.path().join("nonexistent.toml"),
);
cmd.args(["config", "show", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(
json["project"]["config"].is_null(),
"missing override path should resolve to no project config, got: {}",
json["project"]["config"]
);
}