mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
209 lines
6.2 KiB
Rust
209 lines
6.2 KiB
Rust
//! End-to-end shell integration tests.
|
|
#![cfg(all(unix, feature = "shell-integration-tests"))]
|
|
|
|
use crate::common::{
|
|
TestRepo, repo,
|
|
shell::{execute_shell_script, generate_init_code, path_export_syntax, wt_bin_dir},
|
|
};
|
|
use rstest::rstest;
|
|
|
|
#[rstest]
|
|
// Test with bash (baseline) and fish (alternate syntax)
|
|
#[case("bash")]
|
|
#[case("fish")]
|
|
#[case("zsh")]
|
|
fn test_shell_integration_switch_and_remove(#[case] shell: &str, repo: TestRepo) {
|
|
let init_code = generate_init_code(&repo, shell);
|
|
let bin_path = wt_bin_dir();
|
|
|
|
let script = format!(
|
|
r#"
|
|
{}
|
|
{}
|
|
wt switch --create combo-branch
|
|
echo "__PWD_AFTER_SWITCH__ $PWD"
|
|
wt remove
|
|
echo "__PWD_AFTER_REMOVE__ $PWD"
|
|
"#,
|
|
path_export_syntax(shell, &bin_path),
|
|
init_code
|
|
);
|
|
|
|
let output = execute_shell_script(&repo, shell, &script);
|
|
|
|
// Ensure human output is still visible (not just directives)
|
|
assert!(
|
|
output.contains("combo-branch"),
|
|
"Combined e2e run should mention combo-branch, got:\n{}",
|
|
output
|
|
);
|
|
|
|
// Directives must remain hidden from the user.
|
|
assert!(
|
|
!output.contains("__WORKTRUNK"),
|
|
"Directive leakage detected in shell output:\n{}",
|
|
output
|
|
);
|
|
|
|
let after_switch = extract_pwd_marker(&output, "__PWD_AFTER_SWITCH__").unwrap();
|
|
let after_remove = extract_pwd_marker(&output, "__PWD_AFTER_REMOVE__").unwrap();
|
|
|
|
assert!(
|
|
after_switch.contains("combo-branch"),
|
|
"Shell should cd into combo-branch worktree, saw: {}",
|
|
after_switch
|
|
);
|
|
|
|
let repo_root = repo.root_path().to_string_lossy();
|
|
assert!(
|
|
after_remove.ends_with(repo_root.as_ref()),
|
|
"Shell should cd back to repo root {} after remove, got: {}",
|
|
repo_root,
|
|
after_remove
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
fn test_bash_shell_integration_error_handling(repo: TestRepo) {
|
|
let init_code = generate_init_code(&repo, "bash");
|
|
let bin_path = wt_bin_dir();
|
|
|
|
let script = format!(
|
|
r#"
|
|
{}
|
|
{}
|
|
wt switch --create dup-branch
|
|
if wt switch --create dup-branch 2>&1; then
|
|
echo "__UNEXPECTED_SUCCESS__"
|
|
else
|
|
echo "__DUPLICATE_ERROR__"
|
|
fi
|
|
"#,
|
|
path_export_syntax("bash", &bin_path),
|
|
init_code
|
|
);
|
|
|
|
let output = execute_shell_script(&repo, "bash", &script);
|
|
assert!(
|
|
output.contains("__DUPLICATE_ERROR__"),
|
|
"Second switch should have failed, output: {}",
|
|
output
|
|
);
|
|
assert!(
|
|
output.contains("already exists") || output.contains("Branch \"dup-branch\""),
|
|
"User-facing error details missing: {}",
|
|
output
|
|
);
|
|
assert!(
|
|
!output.contains("__WORKTRUNK"),
|
|
"Directive leakage detected in error flow: {}",
|
|
output
|
|
);
|
|
}
|
|
|
|
#[rstest]
|
|
fn test_bash_shell_integration_switch_existing_worktree(repo: TestRepo) {
|
|
let init_code = generate_init_code(&repo, "bash");
|
|
let bin_path = wt_bin_dir();
|
|
let repo_root = repo.root_path().display();
|
|
|
|
let script = format!(
|
|
r#"
|
|
{}
|
|
{}
|
|
wt switch --create existing-branch
|
|
echo "__AFTER_CREATE__ $PWD"
|
|
REPO_ROOT="{}"
|
|
cd "$REPO_ROOT"
|
|
wt switch existing-branch
|
|
echo "__AFTER_EXISTING__ $PWD"
|
|
"#,
|
|
path_export_syntax("bash", &bin_path),
|
|
init_code,
|
|
repo_root
|
|
);
|
|
|
|
let output = execute_shell_script(&repo, "bash", &script);
|
|
let after_create = extract_pwd_marker(&output, "__AFTER_CREATE__").unwrap();
|
|
let after_existing = extract_pwd_marker(&output, "__AFTER_EXISTING__").unwrap();
|
|
|
|
assert!(
|
|
after_create.contains("existing-branch"),
|
|
"First switch should cd into worktree, saw: {}",
|
|
after_create
|
|
);
|
|
assert!(
|
|
after_existing.contains("existing-branch"),
|
|
"Switching to existing worktree should cd there again, saw: {}",
|
|
after_existing
|
|
);
|
|
}
|
|
|
|
/// Reproduction for #2643: when `cd` is aliased before worktrunk's shell
|
|
/// integration is loaded (e.g., zoxide's `eval "$(zoxide init zsh)"` runs
|
|
/// first), the `wt()` function's `cd` token is alias-expanded at eval time.
|
|
/// This rewrites the function body to call `__zoxide_z -- "$path"`, which
|
|
/// queries zoxide's database instead of changing to the absolute path, so
|
|
/// `wt switch` reports success but the shell stays put.
|
|
///
|
|
/// The init code is written to a file and `source`d so that the file is
|
|
/// parsed *after* the alias takes effect, matching the real-world
|
|
/// `eval "$(wt config shell init zsh)"` flow.
|
|
#[rstest]
|
|
#[case("zsh")]
|
|
#[case("bash")]
|
|
fn test_shell_integration_cd_alias_does_not_break_switch(#[case] shell: &str, repo: TestRepo) {
|
|
let init_code = generate_init_code(&repo, shell);
|
|
let bin_path = wt_bin_dir();
|
|
let init_file = repo.home_path().join("wt-init.sh");
|
|
std::fs::write(&init_file, &init_code).unwrap();
|
|
let init_path = init_file.display();
|
|
|
|
// bash only expands aliases in non-interactive shells when
|
|
// expand_aliases is set; zsh expands them unconditionally.
|
|
let pre_init = match shell {
|
|
"bash" => "shopt -s expand_aliases\n",
|
|
_ => "",
|
|
};
|
|
|
|
let script = format!(
|
|
r#"
|
|
{}
|
|
{}
|
|
__zoxide_z() {{
|
|
echo "zoxide: no match found" >&2
|
|
return 1
|
|
}}
|
|
alias cd='__zoxide_z'
|
|
source "{}"
|
|
wt switch --create zoxide-branch
|
|
echo "__PWD_AFTER_SWITCH__ $PWD"
|
|
"#,
|
|
path_export_syntax(shell, &bin_path),
|
|
pre_init,
|
|
init_path,
|
|
);
|
|
|
|
let output = execute_shell_script(&repo, shell, &script);
|
|
let after_switch = extract_pwd_marker(&output, "__PWD_AFTER_SWITCH__").unwrap();
|
|
|
|
assert!(
|
|
after_switch.contains("zoxide-branch"),
|
|
"Shell should cd into zoxide-branch worktree even when cd is aliased, saw: {}\nFull output:\n{}",
|
|
after_switch,
|
|
output,
|
|
);
|
|
assert!(
|
|
!output.contains("zoxide: no match found"),
|
|
"cd should bypass the alias; got zoxide error in output:\n{}",
|
|
output,
|
|
);
|
|
}
|
|
|
|
fn extract_pwd_marker(output: &str, marker: &str) -> Option<String> {
|
|
output
|
|
.lines()
|
|
.find(|line| line.contains(marker))
|
|
.map(|line| line.split(marker).nth(1).unwrap_or("").trim().to_string())
|
|
}
|