use crate::common::{
TestRepo, configure_directive_files, directive_files, make_snapshot_cmd,
make_snapshot_cmd_with_global_flags, repo, setup_snapshot_settings, wt_bin,
};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::io::Write;
use std::process::Stdio;
#[rstest]
fn test_step_alias_from_project_config(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
hello = "echo Hello from {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"step",
&["hello"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_config_alias_dry_run(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
hello = "echo Hello from {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "hello"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_unknown_with_available(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
hello = "echo Hello"
deploy = "make deploy"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["nonexistent"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_did_you_mean(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy"
hello = "echo Hello"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deplyo"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_unknown_no_aliases(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_binds_referenced_var(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
greet = "echo Hello {{ name }} from {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "greet", "--", "--name=World"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_binds_space_separated_var(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
greet = "echo Hello {{ name }} from {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "greet", "--", "--name", "World"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_unreferenced_key_forwards_to_args(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
run = "echo got {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"run",
&["--env=staging", "foo"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_double_dash_escape(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
run = "echo got {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"run",
&["--", "--env=staging", "literal"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_user_var_overshadows_builtin(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
show = "echo branch={{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"show",
&["--branch=override"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_multi_step_binds_across_pipeline(mut repo: TestRepo) {
repo.write_test_config(
r#"
[aliases]
deploy = [
"echo step1 env={{ env }}",
{ publish = "echo step2 region={{ region }}" },
]
"#,
);
repo.commit("initial");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
let mut cmd = make_snapshot_cmd(
&repo,
"step",
&["deploy", "--env=prod", "--region=us-east"],
Some(&feature_path),
);
cmd.env("WORKTRUNK_TEST_SERIAL_CONCURRENT", "1");
assert_cmd_snapshot!(cmd);
}
#[rstest]
fn test_step_alias_exit_code(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
fail = "exit 42"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"step",
&["fail"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_from_user_config(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
greet = "echo Greetings from {{ branch }}"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["greet"],
Some(&feature_path),
));
}
#[rstest]
fn test_top_level_alias_dispatch(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
hello = "echo Hello from {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"hello",
&[],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_top_level_alias_with_step_builtin_name(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
commit = "echo custom-commit"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"commit",
&[],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_top_level_alias_did_you_mean(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy"
hello = "echo Hello"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(&repo, "deplyo", &[], Some(&feature_path),));
}
#[rstest]
fn test_step_alias_shadows_builtin(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
commit = "echo custom-commit"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["comit"],
Some(&feature_path),
));
}
#[rstest]
fn test_step_alias_merge_user_and_project(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
project-cmd = "echo from-project"
shared = "echo project-version"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
user-cmd = "echo from-user"
shared = "echo user-version"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(
"user_alias",
make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "user-cmd"],
Some(&feature_path),
)
);
assert_cmd_snapshot!(
"project_alias",
make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "project-cmd"],
Some(&feature_path),
)
);
assert_cmd_snapshot!(
"user_and_project_append",
make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "shared"],
Some(&feature_path),
)
);
}
#[rstest]
fn test_alias_append_executes_both(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
greet = "echo PROJECT"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
greet = "echo USER"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"step",
&["greet"],
Some(&feature_path),
&["-y"],
));
}
fn snapshot_alias_approval(
test_name: &str,
repo: &TestRepo,
alias_args: &[&str],
approve: bool,
cwd: Option<&std::path::Path>,
) {
let mut cmd = make_snapshot_cmd(repo, "step", alias_args, cwd);
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().unwrap();
{
let stdin = child.stdin.as_mut().unwrap();
let response = if approve { b"y\n" } else { b"n\n" };
stdin.write_all(response).unwrap();
}
let output = child.wait_with_output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!(
"exit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}",
output.status.code().unwrap_or(-1),
stdout,
stderr
);
insta::assert_snapshot!(test_name, combined);
}
#[rstest]
fn test_alias_approval_project_config_prompts(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo deploying {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_approval_already_approved(mut repo: TestRepo) {
repo.run_git(&["remote", "remove", "origin"]);
repo.write_project_config(
r#"
[aliases]
deploy = "echo deploying {{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
repo.write_test_approvals(&format!(
r#"[projects.'{}']
approved-commands = ["echo deploying {{{{ branch }}}}"]
"#,
repo.project_id()
));
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_approval_user_config_skips(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
deploy = "echo deploying from user config"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_approval_user_and_project_both_need_approval(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo project deploy"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
deploy = "echo user deploy"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"step",
&["deploy"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_alias_approval_yes_bypasses(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo deploying"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(
"alias_approval_yes_first_run",
make_snapshot_cmd_with_global_flags(
&repo,
"step",
&["deploy"],
Some(&feature_path),
&["-y"],
)
);
assert_cmd_snapshot!(
"alias_approval_yes_second_run_prompts",
make_snapshot_cmd(&repo, "step", &["deploy"], Some(&feature_path),)
);
}
#[rstest]
fn test_alias_passes_directive_file_to_subprocess(repo: TestRepo) {
repo.commit("initial");
let wt = wt_bin();
let wt_str = wt.to_string_lossy();
assert!(
!wt_str.contains('\''),
"wt binary path should not contain single quotes: {wt_str}"
);
let wt_toml = wt_str.replace('\\', r"\\");
repo.write_test_config(&format!(
r#"
[aliases]
new-branch = "'{wt_toml}' switch --create alias-created"
"#
));
let (cd_path, exec_path, _guard) = directive_files();
let mut cmd = repo.wt_command();
configure_directive_files(&mut cmd, &cd_path, &exec_path);
cmd.args(["step", "new-branch"]);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"wt step new-branch failed: stdout={}\nstderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let cd_content = std::fs::read_to_string(&cd_path).unwrap_or_default();
assert!(
!cd_content.trim().is_empty(),
"alias wrapping `wt switch --create` should write a path to the \
CD directive file, got: {cd_content:?}"
);
assert!(
cd_content.contains("alias-created"),
"cd directive should target the new worktree (alias-created), got: {cd_content:?}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("shell integration"),
"inner wt should not warn about shell integration being uninstalled, got: {stderr}",
);
}
#[rstest]
fn test_alias_pipeline_announcement(mut repo: TestRepo) {
repo.write_test_config(
r#"
[aliases]
deploy = [
{ install = "echo INSTALL" },
{ build = "echo BUILD", lint = "echo LINT" },
]
"#,
);
repo.commit("initial");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
let mut cmd = make_snapshot_cmd(&repo, "step", &["deploy"], Some(&feature_path));
cmd.env("WORKTRUNK_TEST_SERIAL_CONCURRENT", "1");
assert_cmd_snapshot!(cmd);
}
#[rstest]
fn test_alias_concurrent_steps(mut repo: TestRepo) {
repo.write_test_config(
r#"
[aliases.build]
lint = "echo LINT"
test = "echo TEST"
"#,
);
repo.commit("initial");
let feature_path = repo.add_worktree("feature");
let mut cmd = repo.wt_command();
cmd.args(["step", "build"]).current_dir(&feature_path);
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"concurrent alias failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("LINT"), "expected LINT in output: {stderr}");
assert!(stderr.contains("TEST"), "expected TEST in output: {stderr}");
}
#[rstest]
fn test_alias_concurrent_prefixes_output(mut repo: TestRepo) {
repo.write_test_config(
r#"
[aliases.build]
lint = "echo HELLO_LINT"
test = "echo HELLO_TEST"
"#,
);
repo.commit("initial");
let feature_path = repo.add_worktree("feature");
let mut cmd = repo.wt_command();
cmd.args(["step", "build"])
.current_dir(&feature_path)
.env("NO_COLOR", "1");
let output = cmd.output().unwrap();
assert!(
output.status.success(),
"concurrent alias failed: stderr={}",
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
for (label, body) in [("lint", "HELLO_LINT"), ("test", "HELLO_TEST")] {
let has_prefixed_line = stderr
.lines()
.any(|l| l.starts_with(label) && l.contains('│') && l.contains(body));
assert!(
has_prefixed_line,
"expected a line starting with '{label}' containing '{body}' and a '│' separator, got:\n{stderr}"
);
}
}
#[rstest]
fn test_alias_concurrent_step_failure(repo: TestRepo) {
repo.write_test_config(
r#"
[aliases.check]
ok = "true"
fail = "exit 1"
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "check"]);
let output = cmd.output().expect("wt step check failed to spawn");
assert!(
!output.status.success(),
"wt step check should fail when a concurrent step exits non-zero"
);
}
#[rstest]
#[cfg(unix)]
fn test_alias_concurrent_receives_sigint(repo: TestRepo) {
use crate::common::wait_for_file_content;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use std::os::unix::process::CommandExt;
use std::process::Stdio;
repo.write_test_config(
r#"
[aliases.slow]
one = "sh -c 'echo start-one >> slow_one.log; sleep 30; echo done-one >> slow_one.log'"
two = "sh -c 'echo start-two >> slow_two.log; sleep 30; echo done-two >> slow_two.log'"
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "slow"]);
cmd.current_dir(repo.root_path());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
cmd.process_group(0); let mut child = cmd.spawn().expect("failed to spawn wt step slow");
let one_log = repo.root_path().join("slow_one.log");
let two_log = repo.root_path().join("slow_two.log");
wait_for_file_content(&one_log);
wait_for_file_content(&two_log);
let wt_pgid = Pid::from_raw(child.id() as i32);
kill(Pid::from_raw(-wt_pgid.as_raw()), Signal::SIGINT)
.expect("failed to send SIGINT to wt's process group");
let status = child.wait().expect("failed to wait for wt");
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal() == Some(2) || status.code() == Some(130),
"wt should exit from SIGINT (signal 2) or with code 130, got: {status:?}"
);
std::thread::sleep(std::time::Duration::from_millis(500));
for log in [&one_log, &two_log] {
let contents = std::fs::read_to_string(log).unwrap_or_default();
assert!(
!contents.contains("done"),
"sibling child reached 'done' after SIGINT, log: {contents:?}"
);
}
}
#[rstest]
#[cfg(unix)]
fn test_alias_concurrent_second_sigint_kills(repo: TestRepo) {
use crate::common::wait_for_file_content;
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use std::os::unix::process::CommandExt;
use std::process::Stdio;
repo.write_test_config(
r#"
[aliases.stubborn]
one = "sh -c 'trap \"\" INT TERM; echo start-one >> stubborn_one.log; sleep 30'"
two = "sh -c 'trap \"\" INT TERM; echo start-two >> stubborn_two.log; sleep 30'"
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "stubborn"]);
cmd.current_dir(repo.root_path());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
cmd.process_group(0);
let mut child = cmd.spawn().expect("failed to spawn wt step stubborn");
wait_for_file_content(&repo.root_path().join("stubborn_one.log"));
wait_for_file_content(&repo.root_path().join("stubborn_two.log"));
let wt_pgid = Pid::from_raw(child.id() as i32);
kill(Pid::from_raw(-wt_pgid.as_raw()), Signal::SIGINT).expect("failed to send first SIGINT");
std::thread::sleep(std::time::Duration::from_millis(100));
kill(Pid::from_raw(-wt_pgid.as_raw()), Signal::SIGINT).expect("failed to send second SIGINT");
let start = std::time::Instant::now();
let _status = child.wait().expect("failed to wait for wt");
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(3),
"wt took too long to die after 2nd SIGINT; impatient path may not be firing: {elapsed:?}"
);
}
#[rstest]
fn test_alias_concurrent_handles_non_utf8(repo: TestRepo) {
repo.write_test_config(
r#"
[aliases.noisy]
mixed = "sh -c 'printf \"BEFORE\\n\\xff\\nCRLF-LINE\\r\\nAFTER\\n\"; yes PAYLOAD | head -n 50000'"
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "noisy"]);
let output = cmd.output().expect("wt step noisy failed to spawn");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"alias should succeed despite non-UTF-8 byte, got: {status:?}\nlast 500 bytes of stderr: {tail}",
status = output.status,
tail = &stderr[stderr.len().saturating_sub(500)..],
);
assert!(stderr.contains("BEFORE"), "expected BEFORE in stderr");
assert!(
stderr.contains("AFTER"),
"expected AFTER in stderr (reader stopped at the invalid byte)"
);
assert!(
stderr.contains("CRLF-LINE"),
"expected CRLF-LINE (with the \\r stripped) in stderr"
);
assert!(
!stderr.contains("CRLF-LINE\r"),
"trailing \\r should have been stripped before printing"
);
assert_eq!(
stderr.matches("PAYLOAD").count(),
50_000,
"expected 50000 PAYLOAD lines after the invalid byte, got {}",
stderr.matches("PAYLOAD").count(),
);
}
#[rstest]
fn test_alias_concurrent_large_output(repo: TestRepo) {
repo.write_test_config(
r#"
[aliases.bulk]
first = "yes 'FIRST-PAYLOAD-AAAAA' | head -n 50000"
second = "yes 'SECOND-PAYLOAD-BBBBB' | head -n 50000"
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "bulk"]);
let output = cmd.output().expect("wt step bulk failed to spawn");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"concurrent alias with large output should exit 0, got: {status:?}\nlast 500 bytes of stderr: {tail}",
status = output.status,
tail = &stderr[stderr.len().saturating_sub(500)..],
);
let first_count = stderr.matches("FIRST-PAYLOAD-AAAAA").count();
let second_count = stderr.matches("SECOND-PAYLOAD-BBBBB").count();
assert_eq!(
first_count, 50_000,
"expected 50000 occurrences of first payload in stderr, got {first_count}"
);
assert_eq!(
second_count, 50_000,
"expected 50000 occurrences of second payload in stderr, got {second_count}"
);
}
#[rstest]
fn test_alias_pipeline_vars_across_steps(repo: TestRepo) {
repo.write_test_config(
r#"
[aliases]
deploy = [
"git config worktrunk.state.main.vars.target 'staging'",
{ publish = "echo target={{ vars.target }} > alias_lazy.txt" },
]
"#,
);
repo.commit("initial");
let mut cmd = repo.wt_command();
cmd.args(["step", "deploy"]);
let output = cmd.output().expect("wt step deploy failed to spawn");
assert!(
output.status.success(),
"wt step deploy failed: stdout={}\nstderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let marker = repo.root_path().join("alias_lazy.txt");
let content = std::fs::read_to_string(&marker)
.unwrap_or_else(|e| panic!("missing marker {marker:?}: {e}"));
assert_eq!(
content.trim(),
"target=staging",
"lazy step should see var set by prior serial step"
);
}
#[rstest]
fn test_config_alias_dry_run_vars_across_steps(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = [
"git config worktrunk.state.main.vars.target 'prod'",
{ publish = "echo deploying to {{ vars.target }}" },
]
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_dry_run_catches_syntax_error(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
broken = "echo {{ vars..target }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let output = repo
.wt_command()
.args(["config", "alias", "dry-run", "broken"])
.current_dir(&feature_path)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"dry-run should fail on syntax error; stderr:\n{stderr}"
);
assert!(
stderr.contains("syntax error"),
"expected 'syntax error' in stderr, got:\n{stderr}"
);
}
#[rstest]
fn test_retired_dry_run_flag(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo hi"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"deploy",
&["--dry-run"],
Some(&feature_path),
));
}
#[rstest]
fn test_retired_dry_run_flag_via_step(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo hi"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["deploy", "--dry-run"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_help_flag_prints_hint(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo hi {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"deploy",
&["--help"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_help_flag_after_double_dash_forwards(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"deploy",
&["--", "--help"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_single(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_unknown_suggests(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy"
hello = "echo hi"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "deplyo"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_pipeline(mut repo: TestRepo) {
repo.write_project_config(
r#"
[[aliases.release]]
install = "npm install"
[[aliases.release]]
build = "npm run build"
lint = "npm run lint"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "release"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_dry_run_positional_args(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
s = "wt switch {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "s", "--", "target-branch"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_user_and_project(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo from project"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
repo.write_test_config(
r#"
[aliases]
deploy = "echo from user"
"#,
);
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "deploy"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_unknown_no_suggestions(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo hi"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "zzzzzzzz"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_show_warns_on_shadowed_name(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
list = "echo custom list"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "show", "list"],
Some(&feature_path),
));
}
#[rstest]
fn test_config_alias_dry_run_warns_on_shadowed_name(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
list = "echo custom list"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "list"],
Some(&feature_path),
));
}
#[cfg(not(windows))]
#[rstest]
fn test_step_list_with_aliases(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
port = "echo http://localhost:{{ branch | hash_port }}"
squash = "this shadows the built-in"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(&repo, "step", &[], Some(&feature_path)));
}
#[cfg(not(windows))]
#[rstest]
fn test_step_list_no_aliases(mut repo: TestRepo) {
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(&repo, "step", &[], Some(&feature_path)));
}
#[cfg(not(windows))]
#[rstest]
fn test_step_help_includes_aliases(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "make deploy BRANCH={{ branch }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"step",
&["-h"],
Some(&feature_path)
));
}
#[cfg(not(windows))]
#[rstest]
fn test_step_help_silent_with_deprecated_user_config(repo: TestRepo) {
repo.write_test_config(
r#"worktree-path = "../{{ main_worktree }}.{{ branch }}"
"#,
);
let migration_file = repo.test_config_path().with_extension("toml.new");
let output = repo.wt_command().args(["step", "--help"]).output().unwrap();
assert!(
output.status.success(),
"step --help should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
stderr, "",
"step --help must emit no stderr on deprecated user config"
);
assert!(
!migration_file.exists(),
"step --help must not write .new migration file at {}",
migration_file.display()
);
}
#[cfg(not(windows))]
#[rstest]
fn test_step_help_honors_dash_c(repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
xyzzy = "echo nothing happens"
"#,
);
repo.commit("Add alias config");
let repo_path = repo.root_path().to_path_buf();
let cwd = std::env::temp_dir();
let mut cmd = repo.wt_command();
cmd.current_dir(&cwd)
.args(["-C", repo_path.to_str().unwrap(), "step", "--help"]);
let output = cmd.output().expect("failed to run wt");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("xyzzy"),
"expected `xyzzy` alias to appear in `wt -C <repo> step --help` output\n\
stdout:\n{stdout}\nstderr:\n{stderr}"
);
}
#[rstest]
fn test_step_alias_forwards_positional_args(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
run = "echo got {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"run",
&["one", "two three", "four"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_args_sequence_access(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
show = '''echo first={{ args[0] }}; echo count={{ args | length }}; echo each={% for a in args %} {{ a }}{% endfor %}'''
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"show",
&["alpha", "beta gamma"],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_step_alias_empty_args_renders_empty(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
run = "echo [{{ args }}]"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd_with_global_flags(
&repo,
"run",
&[],
Some(&feature_path),
&["-y"],
));
}
#[rstest]
fn test_top_level_alias_positional_expands_in_dry_run(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
s = "wt switch {{ args }}"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
assert_cmd_snapshot!(make_snapshot_cmd(
&repo,
"config",
&["alias", "dry-run", "s", "--", "target-branch"],
Some(&feature_path),
));
}
#[rstest]
fn test_alias_approval_decline(mut repo: TestRepo) {
repo.write_project_config(
r#"
[aliases]
deploy = "echo deploying"
"#,
);
repo.commit("Add alias config");
let feature_path = repo.add_worktree("feature");
let settings = setup_snapshot_settings(&repo);
let _guard = settings.bind_to_scope();
snapshot_alias_approval(
"alias_approval_decline",
&repo,
&["deploy"],
false,
Some(&feature_path),
);
}