#![cfg(all(unix, feature = "shell-integration-tests"))]
use crate::common::{TestRepo, repo, wt_bin};
use insta::assert_snapshot;
use portable_pty::CommandBuilder;
use rstest::rstest;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::{Duration, Instant};
const TERM_ROWS: u16 = 30;
const TERM_COLS: u16 = 120;
const READY_TIMEOUT: Duration = Duration::from_secs(30);
const STABILIZE_TIMEOUT: Duration = Duration::from_secs(30);
const STABLE_DURATION: Duration = Duration::from_millis(500);
const POLL_INTERVAL: Duration = Duration::from_millis(10);
const SEPARATOR_COL: u16 = 60;
struct PtyResult {
parser: vt100::Parser,
exit_code: i32,
}
impl PtyResult {
fn screen(&self) -> String {
self.parser
.screen()
.rows(0, TERM_COLS)
.map(|row| row.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string()
}
fn panels(&self) -> (String, String) {
let screen = self.parser.screen();
let list = screen
.rows(0, SEPARATOR_COL)
.map(|row| row.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string();
let preview = screen
.rows(SEPARATOR_COL + 1, TERM_COLS - SEPARATOR_COL - 1)
.map(|row| row.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n")
.trim_end()
.to_string();
(list, preview)
}
}
fn assert_valid_abort_exit_code(exit_code: i32) {
assert!(
exit_code == 0 || exit_code == 1 || exit_code == 130,
"Unexpected exit code: {} (expected 0, 1, or 130 for skim abort)",
exit_code
);
}
fn is_skim_ready(screen_content: &str) -> bool {
screen_content.starts_with("> ") || screen_content.contains("\n> ")
}
fn exec_in_pty_with_input(
command: &str,
args: &[&str],
working_dir: &Path,
env_vars: &[(String, String)],
input: &str,
) -> PtyResult {
exec_in_pty_with_input_expectations(command, args, working_dir, env_vars, &[(input, None)])
}
fn exec_in_pty_with_input_expectations(
command: &str,
args: &[&str],
working_dir: &Path,
env_vars: &[(String, String)],
inputs: &[(&str, Option<&str>)],
) -> PtyResult {
let pair = crate::common::open_pty_with_size(TERM_ROWS, TERM_COLS);
let mut cmd = CommandBuilder::new(command);
for arg in args {
cmd.arg(arg);
}
cmd.cwd(working_dir);
crate::common::configure_pty_command(&mut cmd);
cmd.env("CLICOLOR_FORCE", "1");
cmd.env("TERM", "xterm-256color");
for (key, value) in env_vars {
cmd.env(key, value);
}
let mut child = pair.slave.spawn_command(cmd).unwrap();
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().unwrap();
let mut writer = pair.master.take_writer().unwrap();
let (tx, rx) = mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut temp_buf = [0u8; 4096];
loop {
match reader.read(&mut temp_buf) {
Ok(0) => break,
Ok(n) => {
if tx.send(temp_buf[..n].to_vec()).is_err() {
break;
}
}
Err(_) => break,
}
}
});
let mut parser = vt100::Parser::new(TERM_ROWS, TERM_COLS, 0);
let drain_output = |rx: &mpsc::Receiver<Vec<u8>>, parser: &mut vt100::Parser| {
while let Ok(chunk) = rx.try_recv() {
parser.process(&chunk);
}
};
let start = Instant::now();
loop {
drain_output(&rx, &mut parser);
let screen_content = parser.screen().contents();
if is_skim_ready(&screen_content) {
break;
}
if start.elapsed() > READY_TIMEOUT {
eprintln!(
"Warning: Timed out waiting for skim ready state. Screen content:\n{}",
screen_content
);
break;
}
std::thread::sleep(POLL_INTERVAL);
}
wait_for_stable(&rx, &mut parser);
for (input, expected_content) in inputs {
writer.write_all(input.as_bytes()).unwrap();
writer.flush().unwrap();
wait_for_stable_with_content(&rx, &mut parser, *expected_content);
}
drop(writer);
let start = std::time::Instant::now();
let timeout = Duration::from_secs(5);
while start.elapsed() < timeout {
if child.try_wait().unwrap().is_some() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let _ = child.kill();
drain_output(&rx, &mut parser);
let exit_status = child.wait().unwrap();
let exit_code = exit_status.exit_code() as i32;
PtyResult { parser, exit_code }
}
fn exec_in_pty_capture_before_abort(
command: &str,
args: &[&str],
working_dir: &Path,
env_vars: &[(String, String)],
pre_abort_inputs: &[(&str, Option<&str>)],
) -> PtyResult {
let pair = crate::common::open_pty_with_size(TERM_ROWS, TERM_COLS);
let mut cmd = CommandBuilder::new(command);
for arg in args {
cmd.arg(arg);
}
cmd.cwd(working_dir);
crate::common::configure_pty_command(&mut cmd);
cmd.env("CLICOLOR_FORCE", "1");
cmd.env("TERM", "xterm-256color");
for (key, value) in env_vars {
cmd.env(key, value);
}
let mut child = pair.slave.spawn_command(cmd).unwrap();
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().unwrap();
let mut writer = pair.master.take_writer().unwrap();
let (tx, rx) = mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut temp_buf = [0u8; 4096];
loop {
match reader.read(&mut temp_buf) {
Ok(0) => break,
Ok(n) => {
if tx.send(temp_buf[..n].to_vec()).is_err() {
break;
}
}
Err(_) => break,
}
}
});
let mut parser = vt100::Parser::new(TERM_ROWS, TERM_COLS, 0);
let drain_output = |rx: &mpsc::Receiver<Vec<u8>>, parser: &mut vt100::Parser| {
while let Ok(chunk) = rx.try_recv() {
parser.process(&chunk);
}
};
let start = Instant::now();
loop {
drain_output(&rx, &mut parser);
let screen_content = parser.screen().contents();
if is_skim_ready(&screen_content) {
break;
}
if start.elapsed() > READY_TIMEOUT {
eprintln!(
"Warning: Timed out waiting for skim ready state. Screen content:\n{}",
screen_content
);
break;
}
std::thread::sleep(POLL_INTERVAL);
}
wait_for_stable(&rx, &mut parser);
for (input, expected_content) in pre_abort_inputs {
writer.write_all(input.as_bytes()).unwrap();
writer.flush().unwrap();
wait_for_stable_with_content(&rx, &mut parser, *expected_content);
}
writer.write_all(b"\x1b").unwrap();
writer.flush().unwrap();
drop(writer);
let start = Instant::now();
let timeout = Duration::from_secs(5);
loop {
while rx.try_recv().is_ok() {} if child.try_wait().unwrap().is_some() {
break;
}
if start.elapsed() >= timeout {
let _ = child.kill();
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let exit_status = child.wait().unwrap();
let exit_code = exit_status.exit_code() as i32;
PtyResult { parser, exit_code }
}
fn wait_for_stable(rx: &mpsc::Receiver<Vec<u8>>, parser: &mut vt100::Parser) {
wait_for_stable_with_content(rx, parser, None);
}
fn display_matches_count(screen: &str) -> bool {
let Some(matched) = parse_match_count(screen) else {
return true;
};
visible_list_rows(screen) == matched
}
fn parse_match_count(screen: &str) -> Option<usize> {
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| regex::Regex::new(r"(\d+)/\d+\s*$").unwrap());
screen
.lines()
.find_map(|line| re.captures(line.trim_end()))
.and_then(|caps| caps[1].parse().ok())
}
fn visible_list_rows(screen: &str) -> usize {
let width = SEPARATOR_COL as usize;
screen
.lines()
.skip(2) .filter(|line| line.chars().take(width).any(|c| !c.is_whitespace()))
.count()
}
fn wait_for_stable_with_content(
rx: &mpsc::Receiver<Vec<u8>>,
parser: &mut vt100::Parser,
expected_content: Option<&str>,
) {
let start = Instant::now();
let mut last_change = Instant::now();
let mut last_content = parser.screen().contents();
let mut content_found_at: Option<Instant> = None;
while start.elapsed() < STABILIZE_TIMEOUT {
while let Ok(chunk) = rx.try_recv() {
parser.process(&chunk);
}
let current_content = parser.screen().contents();
if current_content != last_content {
last_content = current_content.clone();
last_change = Instant::now();
}
let content_ready = match expected_content {
Some(expected) => {
let found = current_content.contains(expected);
if found {
content_found_at.get_or_insert(Instant::now());
} else {
content_found_at = None;
}
found
}
None => true,
};
let display_ready = display_matches_count(¤t_content);
if last_change.elapsed() >= STABLE_DURATION && content_ready && display_ready {
return;
}
if let Some(found_time) = content_found_at
&& found_time.elapsed() >= STABLE_DURATION
&& display_ready
{
return;
}
std::thread::sleep(POLL_INTERVAL);
}
if let Some(expected) = expected_content
&& !last_content.contains(expected)
{
panic!(
"Timed out after {:?} waiting for expected content {:?} to appear on screen.\n\
Screen content:\n{}",
STABILIZE_TIMEOUT, expected, last_content
);
}
eprintln!(
"Warning: Screen did not fully stabilize within {:?}",
STABILIZE_TIMEOUT
);
}
fn switch_picker_settings(repo: &TestRepo) -> insta::Settings {
let mut settings = crate::common::setup_snapshot_settings(repo);
settings.add_filter(r"\A> [^\n]*", "> [QUERY]");
settings.add_filter(r"(?m)summary?\w*\s*\d+/\d+[ \t]*$", "summary [N/M]");
settings.add_filter(r"(?m)\s+\d+/\d+[ \t]*$", " [N/M]");
settings.add_filter(r"\b[0-9a-f]{7,8}\b", "[HASH]");
settings.add_filter(r"\b[0-9a-f]{6,8}\.\.", "[HASH]..");
settings.add_filter(r"\b\d+[dhms]\b", "[TIME]");
settings
}
#[rstest]
fn test_switch_picker_abort_with_escape(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[], );
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_abort_escape_list", list);
assert_snapshot!("switch_picker_abort_escape_preview", preview);
});
}
#[rstest]
fn test_switch_picker_with_multiple_worktrees(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("feature-one");
repo.add_worktree("feature-two");
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[("", Some("feature-two"))],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_multiple_worktrees_list", list);
assert_snapshot!("switch_picker_multiple_worktrees_preview", preview);
});
}
#[rstest]
fn test_switch_picker_with_branches(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("active-worktree");
let output = repo
.git_command()
.args(["branch", "orphan-branch"])
.run()
.unwrap();
assert!(output.status.success(), "Failed to create branch");
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch", "--branches"],
repo.root_path(),
&env_vars,
&[("", Some("orphan-branch"))],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_with_branches_list", list);
assert_snapshot!("switch_picker_with_branches_preview", preview);
});
}
#[rstest]
fn test_switch_picker_preview_panel_uncommitted(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let feature_path = repo.add_worktree("feature");
std::fs::write(feature_path.join("tracked.txt"), "Original content\n").unwrap();
let output = repo
.git_command()
.args(["-C", feature_path.to_str().unwrap(), "add", "tracked.txt"])
.run()
.unwrap();
assert!(output.status.success(), "Failed to add file");
let output = repo
.git_command()
.args([
"-C",
feature_path.to_str().unwrap(),
"commit",
"-m",
"Add tracked file",
])
.run()
.unwrap();
assert!(output.status.success(), "Failed to commit");
std::fs::write(
feature_path.join("tracked.txt"),
"Modified content\nNew line added\nAnother line\n",
)
.unwrap();
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("feature", None),
("1", Some("diff --git")), ],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_preview_uncommitted_list", list);
assert_snapshot!("switch_picker_preview_uncommitted_preview", preview);
});
}
#[rstest]
fn test_switch_picker_preview_panel_log(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let feature_path = repo.add_worktree("feature");
for i in 1..=5 {
std::fs::write(
feature_path.join(format!("file{i}.txt")),
format!("Content for file {i}\n"),
)
.unwrap();
let output = repo
.git_command()
.args(["-C", feature_path.to_str().unwrap(), "add", "."])
.run()
.unwrap();
assert!(output.status.success(), "Failed to add files");
let output = repo
.git_command()
.args([
"-C",
feature_path.to_str().unwrap(),
"commit",
"-m",
&format!("Add file {i} with content"),
])
.run()
.unwrap();
assert!(output.status.success(), "Failed to commit");
}
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("feature", None),
("2", Some("* ")), ],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_preview_log_list", list);
assert_snapshot!("switch_picker_preview_log_preview", preview);
});
}
#[rstest]
fn test_switch_picker_preview_panel_main_diff(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let feature_path = repo.add_worktree("feature");
std::fs::write(
feature_path.join("feature_code.rs"),
r#"fn new_feature() {
println!("This is a new feature!");
let x = 42;
let y = x * 2;
println!("Result: {}", y);
}
"#,
)
.unwrap();
let output = repo
.git_command()
.args(["-C", feature_path.to_str().unwrap(), "add", "."])
.run()
.unwrap();
assert!(output.status.success(), "Failed to add files");
let output = repo
.git_command()
.args([
"-C",
feature_path.to_str().unwrap(),
"commit",
"-m",
"Add new feature implementation",
])
.run()
.unwrap();
assert!(output.status.success(), "Failed to commit");
std::fs::write(
feature_path.join("tests.rs"),
r#"#[test]
fn test_new_feature() {
assert_eq!(42 * 2, 84);
}
"#,
)
.unwrap();
let output = repo
.git_command()
.args(["-C", feature_path.to_str().unwrap(), "add", "."])
.run()
.unwrap();
assert!(output.status.success(), "Failed to add files");
let output = repo
.git_command()
.args([
"-C",
feature_path.to_str().unwrap(),
"commit",
"-m",
"Add tests for new feature",
])
.run()
.unwrap();
assert!(output.status.success(), "Failed to commit");
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("feature", None),
("3", Some("diff --git")), ],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_preview_main_diff_list", list);
assert_snapshot!("switch_picker_preview_main_diff_preview", preview);
});
}
#[rstest]
fn test_switch_picker_preview_panel_summary(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let feature_path = repo.add_worktree("feature");
std::fs::write(feature_path.join("new.txt"), "content\n").unwrap();
let output = repo
.git_command()
.args(["-C", feature_path.to_str().unwrap(), "add", "."])
.run()
.unwrap();
assert!(output.status.success(), "Failed to add file");
let output = repo
.git_command()
.args([
"-C",
feature_path.to_str().unwrap(),
"commit",
"-m",
"Add new file",
])
.run()
.unwrap();
assert!(output.status.success(), "Failed to commit");
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("feature", None),
("5", Some("Configure")), ],
);
assert_valid_abort_exit_code(result.exit_code);
let (list, preview) = result.panels();
let settings = switch_picker_settings(&repo);
settings.bind(|| {
assert_snapshot!("switch_picker_preview_summary_list", list);
assert_snapshot!("switch_picker_preview_summary_preview", preview);
});
}
#[rstest]
fn test_switch_picker_respects_list_config(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("active-worktree");
let output = repo
.git_command()
.args(["branch", "orphan-branch"])
.run()
.unwrap();
assert!(output.status.success(), "Failed to create branch");
repo.write_test_config(
r#"
[list]
branches = true
"#,
);
let env_vars = repo.test_env_vars();
let result = exec_in_pty_capture_before_abort(
wt_bin().to_str().unwrap(),
&["switch"], repo.root_path(),
&env_vars,
&[("", Some("orphan-branch"))], );
assert_valid_abort_exit_code(result.exit_code);
let screen = result.screen();
assert!(
screen.contains("orphan-branch"),
"orphan-branch should appear when [list] branches = true in config.\nScreen:\n{}",
screen
);
}
#[rstest]
fn test_switch_picker_create_worktree_with_alt_c(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let env_vars = repo.test_env_vars();
let result = exec_in_pty_with_input_expectations(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("new-feature", None), ("\x1bc", None), ],
);
assert_eq!(
result.exit_code, 0,
"Expected exit code 0 for successful create"
);
let screen = result.screen();
assert!(
screen.contains("new-feature") || screen.contains("Switched"),
"Expected success message showing new-feature branch.\nScreen:\n{}",
screen
);
let branch_output = repo
.git_command()
.args(["branch", "--list", "new-feature"])
.run()
.unwrap();
assert!(
String::from_utf8_lossy(&branch_output.stdout).contains("new-feature"),
"Branch new-feature should have been created"
);
}
#[rstest]
fn test_switch_picker_create_with_empty_query_fails(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
let env_vars = repo.test_env_vars();
let result = exec_in_pty_with_input(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
"\x1bc", );
assert_ne!(
result.exit_code, 0,
"Expected non-zero exit for empty query"
);
let screen = result.screen();
assert!(
screen.contains("no branch name entered") || screen.contains("Cannot create"),
"Expected error message about missing branch name.\nScreen:\n{}",
screen
);
}
#[rstest]
fn test_switch_picker_switch_to_existing_worktree(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("target-branch");
let env_vars = repo.test_env_vars();
let result = exec_in_pty_with_input_expectations(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("target", None), ("\r", None), ],
);
assert_eq!(
result.exit_code, 0,
"Expected exit code 0 for successful switch"
);
let screen = result.screen();
assert!(
screen.contains("target-branch") || screen.contains("Switched") || screen.contains("cd "),
"Expected switch output showing target-branch.\nScreen:\n{}",
screen
);
}
fn directive_files_for_pty() -> (PathBuf, PathBuf, (tempfile::TempPath, tempfile::TempPath)) {
let cd = tempfile::NamedTempFile::new().expect("failed to create cd temp file");
let exec = tempfile::NamedTempFile::new().expect("failed to create exec temp file");
let cd_path = cd.path().to_path_buf();
let exec_path = exec.path().to_path_buf();
(
cd_path,
exec_path,
(cd.into_temp_path(), exec.into_temp_path()),
)
}
#[rstest]
fn test_switch_picker_no_cd_suppresses_directive(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("target-branch");
let (cd_path, exec_path, _guard) = directive_files_for_pty();
let mut env_vars = repo.test_env_vars();
env_vars.push((
"WORKTRUNK_DIRECTIVE_CD_FILE".to_string(),
cd_path.display().to_string(),
));
env_vars.push((
"WORKTRUNK_DIRECTIVE_EXEC_FILE".to_string(),
exec_path.display().to_string(),
));
let result = exec_in_pty_with_input_expectations(
wt_bin().to_str().unwrap(),
&["switch", "--no-cd"],
repo.root_path(),
&env_vars,
&[
("target", None), ("\r", None), ],
);
assert_eq!(
result.exit_code, 0,
"Expected exit code 0 for successful switch"
);
let cd_content = std::fs::read_to_string(&cd_path).unwrap_or_default();
assert!(
cd_content.trim().is_empty(),
"CD file should be empty with --no-cd via picker, got: {}",
cd_content
);
}
#[rstest]
fn test_switch_picker_emits_cd_directive_by_default(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("target-branch");
let (cd_path, exec_path, _guard) = directive_files_for_pty();
let mut env_vars = repo.test_env_vars();
env_vars.push((
"WORKTRUNK_DIRECTIVE_CD_FILE".to_string(),
cd_path.display().to_string(),
));
env_vars.push((
"WORKTRUNK_DIRECTIVE_EXEC_FILE".to_string(),
exec_path.display().to_string(),
));
let result = exec_in_pty_with_input_expectations(
wt_bin().to_str().unwrap(),
&["switch"],
repo.root_path(),
&env_vars,
&[
("target", None), ("\r", None), ],
);
assert_eq!(
result.exit_code, 0,
"Expected exit code 0 for successful switch"
);
let cd_content = std::fs::read_to_string(&cd_path).unwrap_or_default();
assert!(
!cd_content.trim().is_empty(),
"CD file should contain a path without --no-cd, got: {}",
cd_content
);
}
#[rstest]
fn test_switch_picker_no_cd_prints_branch_without_switching(mut repo: TestRepo) {
repo.remove_fixture_worktrees();
repo.run_git(&["remote", "remove", "origin"]);
repo.add_worktree("target-branch");
let (cd_path, exec_path, _guard) = directive_files_for_pty();
let mut env_vars = repo.test_env_vars();
env_vars.push((
"WORKTRUNK_DIRECTIVE_CD_FILE".to_string(),
cd_path.display().to_string(),
));
env_vars.push((
"WORKTRUNK_DIRECTIVE_EXEC_FILE".to_string(),
exec_path.display().to_string(),
));
let result = exec_in_pty_with_input_expectations(
wt_bin().to_str().unwrap(),
&["switch", "--no-cd"],
repo.root_path(),
&env_vars,
&[
("target", None), ("\r", None), ],
);
assert_eq!(
result.exit_code, 0,
"Expected exit code 0 for --no-cd selection"
);
let screen = result.screen();
assert!(
screen.contains("target-branch"),
"Expected branch name in output with --no-cd.\nScreen:\n{}",
screen
);
let cd_content = std::fs::read_to_string(&cd_path).unwrap_or_default();
assert!(
cd_content.trim().is_empty(),
"CD file should be empty with --no-cd, got: {}",
cd_content
);
}