use crate::common::{
BareRepoTest, TestRepo, TestRepoBase, make_snapshot_cmd, repo, set_temp_home_env,
setup_snapshot_settings, setup_snapshot_settings_with_home, setup_temp_snapshot_settings,
temp_home, wt_command,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
use tempfile::TempDir;
use worktrunk::config::Approvals;
fn snapshot_add_approvals(test_name: &str, repo: &TestRepo, args: &[&str]) {
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("add").args(args);
assert_cmd_snapshot!(test_name, cmd);
});
}
fn snapshot_clear_approvals(test_name: &str, repo: &TestRepo, args: &[&str]) {
let settings = setup_snapshot_settings(repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(repo, "config", &[], None);
cmd.arg("approvals").arg("clear").args(args);
assert_cmd_snapshot!(test_name, cmd);
});
}
#[rstest]
fn test_add_approvals_no_config(repo: TestRepo) {
snapshot_add_approvals("add_approvals_no_config", &repo, &[]);
}
#[rstest]
fn test_add_approvals_all_with_none_approved(repo: TestRepo) {
repo.write_project_config(r#"post-create = "echo 'test'""#);
repo.commit("Add config");
snapshot_add_approvals("add_approvals_all_none_approved", &repo, &["--all"]);
}
#[rstest]
fn test_add_approvals_empty_config(repo: TestRepo) {
repo.write_project_config("");
repo.commit("Add empty config");
snapshot_add_approvals("add_approvals_empty_config", &repo, &[]);
}
#[rstest]
#[case::add("add", "hook_approvals_deprecated_add")]
#[case::clear("clear", "hook_approvals_deprecated_clear")]
fn test_hook_approvals_emits_deprecation_warning(
repo: TestRepo,
#[case] action: &str,
#[case] snapshot_name: &str,
) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = make_snapshot_cmd(&repo, "hook", &[], None);
cmd.arg("approvals").arg(action);
assert_cmd_snapshot!(snapshot_name, cmd);
});
}
#[rstest]
fn test_add_approvals_includes_aliases(repo: TestRepo) {
repo.write_project_config(
r#"[aliases]
deploy = "echo deploying {{ branch }}"
"#,
);
repo.commit("Add alias-only project config");
snapshot_add_approvals("add_approvals_includes_aliases", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_no_approvals(repo: TestRepo) {
snapshot_clear_approvals("clear_approvals_no_approvals", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_with_approvals(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"post-create = "echo 'test'""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_with_approvals", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_global_no_approvals(repo: TestRepo) {
snapshot_clear_approvals("clear_approvals_global_no_approvals", &repo, &["--global"]);
}
#[rstest]
fn test_clear_approvals_global_with_approvals(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"post-create = "echo 'test'""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
snapshot_clear_approvals(
"clear_approvals_global_with_approvals",
&repo,
&["--global"],
);
}
#[rstest]
fn test_clear_approvals_after_clear(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"post-create = "echo 'test'""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
let mut cmd = make_snapshot_cmd(&repo, "config", &[], None);
cmd.arg("approvals").arg("clear");
cmd.output().unwrap();
snapshot_clear_approvals("clear_approvals_after_clear", &repo, &[]);
}
#[rstest]
fn test_clear_approvals_legacy_config_storage(repo: TestRepo, temp_home: TempDir) {
repo.run_git(&["remote", "remove", "origin"]);
let project_id_str = repo.project_id();
let config_path = repo.test_approvals_path().with_file_name("config.toml");
fs::write(
&config_path,
format!(
r#"worktree-path = "../{{{{ repo }}}}.{{{{ branch }}}}"
[projects.'{project_id_str}']
approved-commands = ["cargo build", "cargo test", "npm install"]
"#
),
)
.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("WORKTRUNK_CONFIG_PATH", &config_path);
cmd.args(["config", "approvals", "clear"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!("clear_approvals_legacy_config_storage", cmd);
});
}
#[rstest]
fn test_clear_approvals_multiple_approvals(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(
r#"
post-create = "echo 'first'"
post-start = "echo 'second'"
[pre-commit]
lint = "echo 'third'"
"#,
);
repo.commit("Add config with multiple commands");
let project_id = repo.project_id();
let mut approvals = Approvals::default();
approvals
.approve_command(
project_id.clone(),
"echo 'first'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
approvals
.approve_command(
project_id.clone(),
"echo 'second'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
approvals
.approve_command(
project_id,
"echo 'third'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
snapshot_clear_approvals("clear_approvals_multiple_approvals", &repo, &[]);
}
#[rstest]
fn test_add_approvals_all_already_approved(repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.commit("Initial commit");
repo.write_project_config(r#"post-create = "echo 'test'""#);
repo.commit("Add config");
let mut approvals = Approvals::default();
approvals
.approve_command(
repo.project_id(),
"echo 'test'".to_string(),
Some(repo.test_approvals_path()),
)
.unwrap();
snapshot_add_approvals("add_approvals_all_already_approved", &repo, &[]);
}
#[rstest]
fn test_add_approvals_project_config_no_commands(repo: TestRepo) {
repo.write_project_config(
r#"# Project config without any hook sections
[list]
url = "http://localhost:8080"
"#,
);
repo.commit("Add config without hooks");
snapshot_add_approvals("add_approvals_no_commands", &repo, &[]);
}
#[test]
fn test_add_approvals_bare_repo_config_in_primary_worktree() {
let test = BareRepoTest::new();
let main_worktree = test.create_worktree("main", "main");
test.commit_in(&main_worktree, "Initial commit");
let config_dir = main_worktree.join(".config");
std::fs::create_dir_all(&config_dir).unwrap();
std::fs::write(
config_dir.join("wt.toml"),
r#"post-create = "echo 'hello'"
"#,
)
.unwrap();
let settings = setup_temp_snapshot_settings(test.temp_path());
settings.bind(|| {
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(&main_worktree)
.args(["config", "approvals", "add", "--all"]);
assert_cmd_snapshot!("add_approvals_bare_repo_config_in_primary_worktree", cmd);
});
}
#[test]
fn test_config_create_project_bare_repo_no_worktrees_errors() {
let test = BareRepoTest::new();
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "create", "--project"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"wt config create --project should fail with no worktrees"
);
let bare_root_config = test.bare_repo_path().join(".config").join("wt.toml");
assert!(
!bare_root_config.exists(),
"Config should NOT be created in bare repo root at {:?}",
bare_root_config
);
}
#[test]
fn test_hook_commands_bare_repo_no_worktrees_errors() {
let test = BareRepoTest::new();
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "approvals", "add", "--all"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"config approvals add should fail with no worktrees"
);
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["hook", "show"]);
let output = cmd.output().unwrap();
assert!(
!output.status.success(),
"hook show should fail with no worktrees"
);
}
#[test]
fn test_config_create_project_bare_repo_uses_primary_worktree() {
let test = BareRepoTest::new();
let main_worktree = test.create_worktree("main", "main");
test.commit_in(&main_worktree, "Initial commit");
let mut cmd = wt_command();
test.configure_wt_cmd(&mut cmd);
cmd.current_dir(test.bare_repo_path())
.args(["config", "create", "--project"]);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt config create --project failed:\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let primary_config = main_worktree.join(".config").join("wt.toml");
let bare_root_config = test.bare_repo_path().join(".config").join("wt.toml");
assert!(
primary_config.exists(),
"Config should be created in primary worktree at {:?}",
primary_config
);
assert!(
!bare_root_config.exists(),
"Config should NOT be created in bare repo root at {:?}",
bare_root_config
);
}