use std::path::Path;
use std::process::{Command, Stdio};
use assert_cmd::cargo::cargo_bin_cmd;
use predicates::prelude::*;
fn git(dir: &Path, args: &[&str]) {
let ok = Command::new("git")
.args(args)
.current_dir(dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.unwrap()
.success();
assert!(ok, "git {args:?} failed");
}
#[test]
fn dynamic_completion_offers_workspace_ids() {
let home = tempfile::tempdir().unwrap();
let root = home.path();
let proj = root.join("proj");
std::fs::create_dir(&proj).unwrap();
git(&proj, &["init", "-q"]);
git(&proj, &["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init"]);
let worktrees = root.join(".worktrees");
std::fs::create_dir_all(&worktrees).unwrap();
let wt = worktrees.join("proj-ws-abc123");
git(&proj, &["worktree", "add", "-q", "--detach", wt.to_str().unwrap(), "HEAD"]);
cargo_bin_cmd!("workon")
.env("HOME", root)
.env("COMPLETE", "zsh")
.env("_CLAP_COMPLETE_INDEX", "2")
.args(["--", "workon", "attach", ""])
.assert()
.success()
.stdout(predicate::str::contains("ws-abc123"));
}
#[test]
fn version_flag() {
cargo_bin_cmd!("workon")
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains("workon 0."));
}
#[test]
fn help_flag() {
cargo_bin_cmd!("workon")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Development workspace launcher"));
}
#[test]
fn skip_copy_ignored_requires_workspace() {
cargo_bin_cmd!("workon")
.arg("--skip-copy-ignored")
.assert()
.failure()
.stderr(predicate::str::contains("--skip-copy-ignored"));
}
#[test]
fn help_lists_config_flag() {
cargo_bin_cmd!("workon")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--config"));
}
#[test]
fn help_lists_subcommands() {
cargo_bin_cmd!("workon")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("create"))
.stdout(predicate::str::contains("attach"))
.stdout(predicate::str::contains("destroy"))
.stdout(predicate::str::contains("list"))
.stdout(predicate::str::contains("path"));
}
#[test]
fn create_help_lists_its_flags() {
cargo_bin_cmd!("workon")
.args(["create", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--name"))
.stdout(predicate::str::contains("--skip-copy-ignored"))
.stdout(predicate::str::contains("--json"));
}
#[test]
fn path_unknown_reference_fails_cleanly() {
cargo_bin_cmd!("workon")
.args(["path", "definitely-no-such-ws"])
.assert()
.failure()
.stderr(predicate::str::contains("definitely-no-such-ws"));
}
#[test]
fn destroy_help_lists_no_save() {
cargo_bin_cmd!("workon")
.args(["destroy", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--no-save"));
}
#[test]
fn destroy_unknown_reference_fails_cleanly() {
cargo_bin_cmd!("workon")
.args(["destroy", "definitely-no-such-ws"])
.assert()
.failure()
.stderr(predicate::str::contains("definitely-no-such-ws"));
}
#[test]
fn resume_requires_workspace() {
cargo_bin_cmd!("workon")
.args(["--resume", "some-session-id"])
.assert()
.failure()
.stderr(predicate::str::contains("--resume"));
}
#[test]
fn help_lists_name_flag() {
cargo_bin_cmd!("workon")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--name"));
}
#[test]
fn help_lists_new_session_long_flag() {
cargo_bin_cmd!("workon")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--new-session"));
}
#[test]
fn reserved_n_short_flag_is_accepted_as_noop() {
let tmp = tempfile::tempdir().unwrap();
cargo_bin_cmd!("workon")
.env("XDG_CONFIG_HOME", tmp.path())
.args(["-n", "--config", "no-such-config"])
.assert()
.failure()
.stderr(predicate::str::contains("no-such-config"));
}
#[test]
fn workspace_flag_takes_no_value() {
let tmp = tempfile::tempdir().unwrap();
cargo_bin_cmd!("workon")
.env("XDG_CONFIG_HOME", tmp.path())
.args(["-w", "--config", "no-such-config"])
.assert()
.failure()
.stderr(predicate::str::contains("no-such-config"));
}
#[test]
fn missing_named_config_errors_cleanly() {
let tmp = tempfile::tempdir().unwrap();
cargo_bin_cmd!("workon")
.env("XDG_CONFIG_HOME", tmp.path())
.args(["--config", "no-such-config"])
.assert()
.failure()
.stderr(predicate::str::contains("no-such-config"))
.stderr(predicate::str::contains("#creating-a-config"));
}
#[test]
fn invalid_config_name_with_path_traversal_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
cargo_bin_cmd!("workon")
.env("XDG_CONFIG_HOME", tmp.path())
.args(["--config", "../etc/hosts"])
.assert()
.failure()
.stderr(predicate::str::contains("invalid config name"));
}
fn create_json(root: &Path, name: &str) -> serde_json::Value {
let proj = root.join("proj");
if !proj.exists() {
std::fs::create_dir(&proj).unwrap();
git(&proj, &["init", "-q", "-b", "main"]);
git(&proj, &["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "--allow-empty", "-m", "init"]);
let origin = root.join("origin.git");
git(root, &["init", "-q", "--bare", origin.to_str().unwrap()]);
git(&proj, &["remote", "add", "origin", origin.to_str().unwrap()]);
git(&proj, &["push", "-q", "origin", "main"]);
}
let out = cargo_bin_cmd!("workon")
.env("HOME", root)
.env("XDG_CONFIG_HOME", root.join(".config"))
.current_dir(&proj)
.args(["create", "--name", name, "--json"])
.assert()
.success()
.get_output()
.stdout
.clone();
serde_json::from_slice(&out).unwrap()
}
#[test]
fn create_name_labels_the_ws_id_and_empty_name_is_no_name() {
let home = tempfile::tempdir().unwrap();
let labelled = create_json(home.path(), "Fix Bug")["ws_id"].as_str().unwrap().to_string();
assert!(labelled.starts_with("ws-") && labelled.ends_with("-fix-bug"), "{labelled}");
assert_eq!(labelled.len(), "ws-abcdef-fix-bug".len(), "{labelled}");
let unnamed = create_json(home.path(), "")["ws_id"].as_str().unwrap().to_string();
assert_eq!(unnamed.len(), "ws-abcdef".len(), "{unnamed}");
assert!(unnamed.starts_with("ws-") && !unnamed.ends_with('-'), "{unnamed}");
}
#[test]
fn create_trusts_the_worktree_in_claude_json() {
let home = tempfile::tempdir().unwrap();
let report = create_json(home.path(), "");
let path = report["path"].as_str().unwrap();
let claude: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(home.path().join(".claude.json")).unwrap()).unwrap();
assert_eq!(claude["projects"][path]["hasTrustDialogAccepted"], serde_json::Value::Bool(true), "{claude}");
}