smux-cli 0.1.10

Small Rust CLI for tmux session selection and creation
Documentation
use assert_cmd::Command;
use predicates::str::contains;
use std::env;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

fn fake_tool_dir() -> tempfile::TempDir {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");

    for tool in ["tmux", "fzf", "zoxide"] {
        write_fake_tool(tempdir.path(), tool);
    }

    tempdir
}

fn write_fake_tool(dir: &Path, name: &str) {
    let path = dir.join(name);
    fs::write(&path, "#!/bin/sh\nexit 0\n").expect("tool stub should be written");
    let mut permissions = fs::metadata(&path)
        .expect("tool stub metadata should be readable")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions).expect("tool stub should be executable");
}

fn write_fake_tmux_snapshot_tool(dir: &Path) {
    let path = dir.join("tmux");
    let script = r#"#!/bin/sh
case "$1" in
  has-session)
    exit 0
    ;;
  list-windows)
    printf '@1\teditor\t1\n'
    ;;
  show-window-options)
    printf 'off\n'
    ;;
  list-panes)
    printf '0\t/tmp/demo\t1\t0\t0\t100\t40\n1\t/tmp/demo/server\t0\t50\t0\t50\t40\n'
    ;;
  *)
    exit 1
    ;;
esac
"#;
    fs::write(&path, script).expect("tmux stub should be written");
    let mut permissions = fs::metadata(&path)
        .expect("tmux stub metadata should be readable")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions).expect("tmux stub should be executable");
}

fn prepend_path(dir: &Path) -> String {
    let current = env::var("PATH").unwrap_or_default();
    format!("{}:{}", dir.display(), current)
}

#[test]
fn help_includes_subcommands() {
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.arg("--help");

    command
        .assert()
        .success()
        .stdout(contains("select"))
        .stdout(contains("doctor"))
        .stdout(contains("save-project"));
}

#[test]
fn missing_subcommand_is_a_usage_error() {
    let mut command = Command::cargo_bin("smux").expect("binary should build");

    command
        .assert()
        .failure()
        .stderr(contains("Usage:"))
        .stderr(contains("<COMMAND>"));
}

#[test]
fn doctor_succeeds_with_missing_config_file() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let tool_dir = fake_tool_dir();
    let config_path = tempdir.path().join("missing.toml");

    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["doctor", "--config"]);
    command.arg(&config_path);
    command.env("PATH", prepend_path(tool_dir.path()));
    command.env("XDG_CONFIG_HOME", tempdir.path());

    command
        .assert()
        .success()
        .stdout(contains("config"))
        .stdout(contains("missing"))
        .stdout(contains("icons"));
}

#[test]
fn doctor_reports_schema_drift_without_failing() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let tool_dir = fake_tool_dir();
    let config_path = tempdir.path().join("config.toml");
    let project_dir = tempdir.path().join("projects");
    fs::create_dir(&project_dir).expect("project dir should be created");
    fs::write(
        &config_path,
        "[templates.default]\nwindows = [{ name = \"main\" }]\n",
    )
    .expect("config fixture should be written");
    fs::write(
        project_dir.join("demo.toml"),
        "#:schema https://raw.githubusercontent.com/Aietes/smux/v0.1.0/schemas/smux-project.schema.json\npath = \"/tmp/demo\"\n",
    )
    .expect("project fixture should be written");

    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["doctor", "--config"]);
    command.arg(&config_path);
    command.env("PATH", prepend_path(tool_dir.path()));
    command.env("XDG_CONFIG_HOME", tempdir.path());

    command
        .assert()
        .success()
        .stdout(contains("schema_config"))
        .stdout(contains("schema_projects"))
        .stdout(contains("drift=1"))
        .stdout(contains("missing=0"));
}

#[test]
fn doctor_fix_rewrites_missing_and_stale_schema_directives() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let tool_dir = fake_tool_dir();
    let config_path = tempdir.path().join("config.toml");
    let project_dir = tempdir.path().join("projects");
    fs::create_dir(&project_dir).expect("project dir should be created");
    fs::write(
        &config_path,
        "[templates.default]\nwindows = [{ name = \"main\" }]\n",
    )
    .expect("config fixture should be written");
    let stale_project = project_dir.join("stale.toml");
    fs::write(
        &stale_project,
        "#:schema https://raw.githubusercontent.com/Aietes/smux/v0.1.0/schemas/smux-project.schema.json\npath = \"/tmp/demo\"\n",
    )
    .expect("stale project fixture should be written");
    let missing_project = project_dir.join("missing.toml");
    fs::write(&missing_project, "path = \"/tmp/other\"\n")
        .expect("missing project fixture should be written");

    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["doctor", "--fix", "--config"]);
    command.arg(&config_path);
    command.env("PATH", prepend_path(tool_dir.path()));
    command.env("XDG_CONFIG_HOME", tempdir.path());

    command
        .assert()
        .success()
        .stdout(contains("schema_fix"))
        .stdout(contains("updated=1 inserted=2"));

    let config_contents = fs::read_to_string(&config_path).expect("config should be readable");
    assert!(config_contents.starts_with("#:schema "));
    let stale_contents = fs::read_to_string(&stale_project).expect("project should be readable");
    let expected_project_schema = format!(
        "/v{}/schemas/smux-project.schema.json",
        env!("CARGO_PKG_VERSION")
    );
    assert!(stale_contents.contains(&expected_project_schema));
    let missing_contents =
        fs::read_to_string(&missing_project).expect("project should be readable");
    assert!(missing_contents.starts_with("#:schema "));
}

#[test]
fn doctor_fails_with_invalid_config_file() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let tool_dir = fake_tool_dir();
    let config_path = tempdir.path().join("invalid.toml");
    fs::write(&config_path, "not = [valid").expect("config fixture should be written");

    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["doctor", "--config"]);
    command.arg(&config_path);
    command.env("PATH", prepend_path(tool_dir.path()));
    command.env("XDG_CONFIG_HOME", tempdir.path());

    command
        .assert()
        .failure()
        .stdout(contains("config"))
        .stdout(contains("error"))
        .stderr(contains("doctor checks failed"));
}

#[test]
fn completions_command_outputs_zsh_script() {
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["completions", "zsh"]);

    command
        .assert()
        .success()
        .stdout(contains("compdef"))
        .stdout(contains("smux"));
}

#[test]
fn man_command_outputs_manpage() {
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.arg("man");

    command
        .assert()
        .success()
        .stdout(contains(".TH"))
        .stdout(contains("smux"));
}

#[test]
fn completions_command_writes_to_directory() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["completions", "zsh", "--dir"]);
    command.arg(tempdir.path());

    command.assert().success();
    assert!(tempdir.path().join("_smux").exists());
}

#[test]
fn man_command_writes_to_directory() {
    let tempdir = tempfile::tempdir().expect("tempdir should be created");
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["man", "--dir"]);
    command.arg(tempdir.path());

    command.assert().success();
    assert!(tempdir.path().join("smux.1").exists());
    assert!(tempdir.path().join("smux-select.1").exists());
    assert!(tempdir.path().join("smux-config.5").exists());
}

#[test]
fn save_project_requires_session_outside_tmux() {
    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["save-project", "demo", "--stdout"]);
    command.env_remove("TMUX");

    command
        .assert()
        .failure()
        .stderr(contains("--session is required outside tmux"));
}

#[test]
fn save_project_stdout_exports_project_toml() {
    let tool_dir = tempfile::tempdir().expect("tempdir should be created");
    write_fake_tool(tool_dir.path(), "fzf");
    write_fake_tool(tool_dir.path(), "zoxide");
    write_fake_tmux_snapshot_tool(tool_dir.path());

    let mut command = Command::cargo_bin("smux").expect("binary should build");
    command.args(["save-project", "demo", "--session", "demo", "--stdout"]);
    command.env("PATH", prepend_path(tool_dir.path()));
    command.env_remove("TMUX");

    command
        .assert()
        .success()
        .stdout(contains("#:schema "))
        .stdout(contains("path = \"/tmp/demo\""))
        .stdout(contains("session_name = \"demo\""))
        .stdout(contains("startup_window = \"editor\""))
        .stdout(contains("windows = ["));
}