use crate::common::{TestRepo, repo, setup_snapshot_settings, wt_command};
use insta_cmd::assert_cmd_snapshot;
use rstest::rstest;
use std::fs;
#[rstest]
fn test_list_config_full_enabled(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"[list]
full = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_branches_enabled(repo: TestRepo) {
repo.run_git(&["branch", "feature"]);
fs::write(
repo.test_config_path(),
r#"[list]
branches = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_cli_override(repo: TestRepo) {
repo.run_git(&["branch", "feature"]);
fs::write(
repo.test_config_path(),
r#"[list]
branches = false
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list")
.arg("--branches")
.current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_full_and_branches(repo: TestRepo) {
repo.run_git(&["branch", "feature"]);
fs::write(
repo.test_config_path(),
r#"[list]
full = true
branches = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_no_config(repo: TestRepo) {
repo.run_git(&["branch", "feature"]);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_project_url_column(repo: TestRepo) {
repo.write_project_config(
r#"[list]
url = "http://localhost:{{ branch | hash_port }}"
"#,
);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_json_url_fields(repo: TestRepo) {
repo.write_project_config(
r#"[list]
url = "http://localhost:{{ branch | hash_port }}"
"#,
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["list", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let items = json.as_array().unwrap();
assert!(!items.is_empty());
let first = &items[0];
let url = first["url"].as_str().unwrap();
assert!(url.starts_with("http://localhost:"));
let port: u16 = url.split(':').next_back().unwrap().parse().unwrap();
assert!((10000..=19999).contains(&port));
assert!(first["url_active"].is_boolean());
}
#[rstest]
fn test_list_json_no_url_without_template(repo: TestRepo) {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["list", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let items = json.as_array().unwrap();
assert!(!items.is_empty());
let first = &items[0];
assert!(first["url"].is_null());
assert!(first["url_active"].is_null());
}
#[rstest]
fn test_list_url_with_branches_flag(repo: TestRepo) {
for branch in &["feature-a", "feature-b", "feature-c"] {
let worktree_path = repo
.root_path()
.parent()
.unwrap()
.join(format!("repo.{}", branch));
if worktree_path.exists() {
let _ = repo
.git_command()
.args([
"worktree",
"remove",
"--force",
worktree_path.to_str().unwrap(),
])
.run();
}
let _ = repo.git_command().args(["branch", "-D", branch]).run();
}
repo.run_git(&["branch", "feature"]);
repo.write_project_config(
r#"[list]
url = "http://localhost:{{ branch | hash_port }}"
"#,
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["list", "--branches", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let items = json.as_array().unwrap();
assert_eq!(items.len(), 2);
let worktree = items.iter().find(|i| i["kind"] == "worktree").unwrap();
let branch = items.iter().find(|i| i["kind"] == "branch").unwrap();
assert!(
worktree["url"]
.as_str()
.unwrap()
.starts_with("http://localhost:"),
"Worktree should have URL"
);
assert!(
branch["url"].is_null(),
"Branch without worktree should not have URL"
);
assert!(
branch["url_active"].is_null(),
"Branch without worktree should not have url_active"
);
}
#[rstest]
fn test_list_url_with_branch_variable(repo: TestRepo) {
repo.write_project_config(
r#"[list]
url = "http://localhost:8080/{{ branch }}"
"#,
);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["list", "--format=json"])
.current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let items = json.as_array().unwrap();
let first = &items[0];
let url = first["url"].as_str().unwrap();
assert_eq!(url, "http://localhost:8080/main");
}
#[rstest]
fn test_list_config_timeout_triggers_timeouts(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"[list]
task-timeout-ms = 1
"#,
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("timed out") || output.status.success(),
"Expected either timeout message in footer or success (if git was fast enough)"
);
}
#[rstest]
fn test_list_config_timeout_zero_means_no_timeout(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"[list]
task-timeout-ms = 0
"#,
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("timed out"),
"Expected no timeout message with task-timeout-ms = 0, but got: {}",
stderr
);
}
#[rstest]
fn test_list_config_env_override_preserves_file_fields(repo: TestRepo) {
repo.run_git(&["branch", "feature"]);
fs::write(
repo.test_config_path(),
r#"[list]
branches = true
"#,
)
.unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK__LIST__TIMEOUT_MS", "0");
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_malformed_config_warns_on_stderr(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"[list]
branches = "not-a-bool"
"#,
)
.unwrap();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"_PARENT_/[^\s,]*test-config\.toml", "[TEST_CONFIG]");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_env_override_bad_value_warns_on_stderr(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK__LIST__BRANCHES", "not-a-bool");
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_env_override_numeric_string_field(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK_WORKTREE_PATH", "42");
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Failed"),
"numeric string should not fail: {stderr}"
);
assert!(output.status.success());
});
}
#[rstest]
fn test_list_config_env_override_mixed_typed_and_string(repo: TestRepo) {
fs::write(repo.test_config_path(), "[list]\nbranches = true\n").unwrap();
repo.run_git(&["branch", "feature"]);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK__LIST__TIMEOUT_MS", "100");
cmd.env("WORKTRUNK_WORKTREE_PATH", "42");
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Failed"),
"mixed typed+string env vars should not fail: {stderr}"
);
assert!(output.status.success(), "exit code should be 0: {stderr}");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("feature"),
"file config (branches=true) should be preserved: {stdout}"
);
});
}
#[rstest]
fn test_list_config_env_override_bad_value_preserves_file_config(repo: TestRepo) {
fs::write(repo.test_config_path(), "[list]\nbranches = true\n").unwrap();
repo.run_git(&["branch", "feature"]);
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK__LIST__BRANCHES", "not-a-bool");
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_env_override_validation_failure(repo: TestRepo) {
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK_WORKTREE_PATH", "");
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_malformed_non_section_field_warns_on_stderr(repo: TestRepo) {
fs::write(
repo.test_config_path(),
"skip-shell-integration-prompt = \"not-a-bool\"\n",
)
.unwrap();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"_PARENT_/[^\s,]*test-config\.toml", "[TEST_CONFIG]");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_validation_error_warns_on_stderr(repo: TestRepo) {
fs::write(repo.test_config_path(), "worktree-path = \"\"\n").unwrap();
let settings = setup_snapshot_settings(&repo);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_malformed_system_config_warns_on_stderr(repo: TestRepo) {
let system_config = repo.root_path().join("system-config.toml");
fs::write(&system_config, "[list]\nbranches = \"not-a-bool\"\n").unwrap();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"_REPO_/system-config\.toml", "[TEST_SYSTEM_CONFIG_FILE]");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_malformed_system_config_non_section_field(repo: TestRepo) {
let system_config = repo.root_path().join("system-config.toml");
fs::write(
&system_config,
"skip-shell-integration-prompt = \"not-a-bool\"\n",
)
.unwrap();
let mut settings = setup_snapshot_settings(&repo);
settings.add_filter(r"_REPO_/system-config\.toml", "[TEST_SYSTEM_CONFIG_FILE]");
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.env("WORKTRUNK_SYSTEM_CONFIG_PATH", &system_config);
cmd.arg("list").current_dir(repo.root_path());
assert_cmd_snapshot!(cmd);
});
}
#[rstest]
fn test_list_config_timeout_disabled_with_full(repo: TestRepo) {
fs::write(
repo.test_config_path(),
r#"[list]
task-timeout-ms = 1
"#,
)
.unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.args(["list", "--full"]).current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("timed out"),
"Expected no timeout message with --full flag, but got: {}",
stderr
);
}