use crate::common::{
TestRepo, repo, set_temp_home_env, setup_home_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_hook_show_with_both_configs(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[pre-commit]
user-lint = "pre-commit run --all-files"
"#,
)
.unwrap();
repo.write_project_config(
r#"pre-merge = [
{build = "cargo build"},
{test = "cargo test"},
]
[post-start]
deps = "npm install"
"#,
);
repo.commit("Add project config");
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("hook").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_no_hooks(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"),
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);
cmd.arg("hook").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
fn setup_all_hook_types(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"pre-merge = [
{build = "cargo build"},
{test = "cargo test"},
]
[post-start]
deps = "npm install"
[post-merge]
deploy = "scripts/deploy.sh"
[pre-remove]
cleanup = "echo cleanup"
[post-remove]
notify = "echo removed"
"#,
);
repo.commit("Add project config");
}
#[rstest]
fn test_hook_show_filter_by_type(repo: TestRepo, temp_home: TempDir) {
setup_all_hook_types(&repo, &temp_home);
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("hook")
.arg("show")
.arg("pre-merge")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_filter_post_merge(repo: TestRepo, temp_home: TempDir) {
setup_all_hook_types(&repo, &temp_home);
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("hook")
.arg("show")
.arg("post-merge")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_filter_pre_remove(repo: TestRepo, temp_home: TempDir) {
setup_all_hook_types(&repo, &temp_home);
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("hook")
.arg("show")
.arg("pre-remove")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_filter_post_remove(repo: TestRepo, temp_home: TempDir) {
setup_all_hook_types(&repo, &temp_home);
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("hook")
.arg("show")
.arg("post-remove")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_approval_status(repo: TestRepo, temp_home: TempDir) {
repo.run_git(&["remote", "remove", "origin"]);
let project_id_str = repo.project_id();
let canonical_home = crate::common::canonicalize(temp_home.path())
.unwrap_or_else(|_| temp_home.path().to_path_buf());
let global_config_dir = canonical_home.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 }}"
"#,
)
.unwrap();
let approvals_path = global_config_dir.join("approvals.toml");
fs::write(
&approvals_path,
format!(
r#"[projects.'{project_id_str}']
approved-commands = ["cargo build"]
"#
),
)
.unwrap();
repo.write_project_config(
r#"pre-merge = [
{build = "cargo build"},
{test = "cargo test"},
]
"#,
);
repo.commit("Add project config");
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.env("WORKTRUNK_APPROVALS_PATH", &approvals_path);
cmd.arg("hook").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_error_with_context_formatting(temp_home: TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let settings = setup_home_snapshot_settings(&temp_home);
settings.bind(|| {
let mut cmd = wt_command();
cmd.arg("remove").current_dir(temp_dir.path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_project_config_no_hooks(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"# Project config with no hooks
[list]
url = "http://localhost:8080"
"#,
);
repo.commit("Add project config without hooks");
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("hook").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_outside_git_repo(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[pre-commit]
lint = "pre-commit run"
"#,
)
.unwrap();
let mut settings = setup_home_snapshot_settings(&temp_home);
let canonical_home = crate::common::canonicalize(temp_home.path())
.unwrap_or_else(|_| temp_home.path().to_path_buf());
settings.add_filter(®ex::escape(&canonical_home.to_string_lossy()), "~");
settings.add_filter(r"thread '([^']+)' \(\d+\)", "thread '$1' ([THREAD_ID])");
settings.bind(|| {
let mut cmd = wt_command();
cmd.arg("hook").arg("show").current_dir(temp_dir.path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_expanded_syntax_error(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"[pre-commit]
broken = "echo {{ branch"
"#,
);
repo.commit("Add project config with broken template");
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("hook")
.arg("show")
.arg("pre-commit")
.arg("--expanded")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_show_expanded_undefined_var(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"[pre-commit]
optional-var = "echo {{ base }}"
"#,
);
repo.commit("Add project config with optional variable");
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("hook")
.arg("show")
.arg("pre-commit")
.arg("--expanded")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_hook_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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[pre-commit]
user-lint = "pre-commit run --all-files"
"#,
)
.unwrap();
repo.write_project_config(
r#"pre-merge = [
{build = "cargo build"},
]
[post-start]
deps = "npm install"
"#,
);
repo.commit("Add project config");
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env(
"WORKTRUNK_CONFIG_PATH",
global_config_dir.join("config.toml"),
);
cmd.args(["hook", "show", "--format=json"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(output.status.success(), "hook show --format=json failed");
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
let entries = parsed.as_array().expect("array");
assert_eq!(
entries.len(),
3,
"user pre-commit + project pre-merge + project post-start"
);
let user_lint = entries
.iter()
.find(|e| e["name"] == "user-lint")
.expect("user-lint present");
assert_eq!(user_lint["type"], "pre-commit");
assert_eq!(user_lint["source"], "user");
assert_eq!(user_lint["template"], "pre-commit run --all-files");
assert_eq!(user_lint["needs_approval"], false);
let build = entries
.iter()
.find(|e| e["name"] == "build")
.expect("build present");
assert_eq!(build["type"], "pre-merge");
assert_eq!(build["source"], "project");
assert_eq!(build["needs_approval"], true);
}
#[rstest]
fn test_hook_show_filtered_expanded_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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
[pre-commit]
user-lint = "echo {{ branch }}"
[post-start]
user-greet = "echo hi"
"#,
)
.unwrap();
repo.write_project_config(
r#"[pre-commit]
project-fmt = "echo fmt {{ branch }}"
[post-start]
project-deps = "echo deps"
"#,
);
repo.commit("Add project config");
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env(
"WORKTRUNK_CONFIG_PATH",
global_config_dir.join("config.toml"),
);
cmd.args(["hook", "show", "pre-commit", "--expanded", "--format=json"])
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"hook show --expanded --format=json failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
let entries = parsed.as_array().expect("array");
let names: Vec<&str> = entries
.iter()
.map(|e| e["name"].as_str().unwrap())
.collect();
assert_eq!(entries.len(), 2, "only pre-commit hooks survive: {names:?}");
assert!(names.contains(&"user-lint"));
assert!(names.contains(&"project-fmt"));
for entry in entries {
assert_eq!(entry["type"], "pre-commit");
let expanded = entry["expanded"].as_str().expect("expanded field present");
assert!(
expanded.starts_with("echo "),
"expanded should render template: {expanded}"
);
}
}
#[rstest]
fn test_hook_show_expanded_valid_template(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"),
r#"worktree-path = "../{{ repo }}.{{ branch }}"
"#,
)
.unwrap();
repo.write_project_config(
r#"[pre-commit]
valid = "echo branch={{ branch }} repo={{ repo }}"
"#,
);
repo.commit("Add project config with valid template");
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("hook")
.arg("show")
.arg("pre-commit")
.arg("--expanded")
.current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());
assert_cmd_snapshot!(cmd);
});
}