wsb 0.291.8591

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
use super::*;
use tempfile::TempDir;

fn with_config(text: Option<&str>) -> TempDir {
    let root = TempDir::new().unwrap();
    if let Some(text) = text {
        std::fs::create_dir_all(root.path().join(".wsb")).unwrap();
        std::fs::write(root.path().join(FILE), text).unwrap();
    }
    root
}

#[test]
fn named_paths_are_relative_to_the_scan_root() {
    let root = with_config(Some(r#"{"declared": "workspace.json", "build_root": ".build"}"#));
    assert_eq!(
        StampConfig::declared_file(root.path()).unwrap(),
        root.path().join("workspace.json")
    );
    assert_eq!(StampConfig::build_root(root.path()).unwrap(), root.path().join(".build"));
}

/// TEST20108: a build root inside `.wsb/` is refused, naming the file.
///
/// Everything in `.wsb/` is committed. A build there was hidden by an exclude
/// file while `.wsb/` held local state too; with that gone, it would turn up
/// in every `git status` and be committed by the first `git add -A`.
#[test]
fn test20108_a_build_root_inside_the_committed_folder_is_refused() {
    let root = with_config(Some(r#"{"build_root": ".wsb/disposable/build"}"#));
    let said = format!("{:#}", StampConfig::build_root(root.path()).unwrap_err());
    assert!(said.contains("stamp.json") && said.contains("committed"), "{said}");
}

#[test]
fn asking_without_a_configuration_names_the_file() {
    let root = with_config(None);
    let said = format!("{:#}", StampConfig::declared_file(root.path()).unwrap_err());
    assert!(said.contains(".wsb/stamp.json"), "{said}");
    assert!(said.contains("there is no"), "{said}");
}

#[test]
fn asking_for_what_the_configuration_does_not_name_says_so() {
    let root = with_config(Some(r#"{"declared": "workspace.json"}"#));
    let said = format!("{:#}", StampConfig::build_root(root.path()).unwrap_err());
    assert!(said.contains("\"build_root\""), "{said}");
    assert!(said.contains("names none"), "{said}");
}

#[test]
fn an_unknown_key_is_refused_rather_than_ignored() {
    let root = with_config(Some(r#"{"declared": "workspace.json", "build": "b"}"#));
    let said = format!("{:#}", StampConfig::load(root.path()).unwrap_err());
    assert!(said.contains("unknown field"), "{said}");
}

fn program_at(dir: &Path, name: &str) -> PathBuf {
    std::fs::create_dir_all(dir).unwrap();
    let path = dir.join(name);
    std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
    }
    path
}

/// TEST20226: a hook declared by a plugin's name runs the program the plugin
/// was enabled with, whatever program of that name comes first on the PATH;
/// a plugin with no program recorded is refused, naming how to record one;
/// a hook declared by a path runs that file; any other name is the PATH's.
///
/// On macOS `/usr/bin/sdx` is a system tool. `local off` started with
/// `/usr/bin` ahead of `~/.qrust/bin` ran it as the `sdx` release hook, which
/// printed its usage, and reported nothing to take back (21 Oct 2026).
#[cfg(unix)]
#[test]
fn test20226_a_hook_runs_the_program_its_plugin_is() {
    let scratch = TempDir::new().unwrap();
    let base = scratch.path();
    let system = program_at(&base.join("usr-bin"), "sdx");
    let installed = program_at(&base.join("qrust-bin"), "sdx");
    let other = program_at(&base.join("qrust-bin"), "make-ish");
    let home = base.join("home");
    let in_home = program_at(&home.join("bin"), "tool");
    let scan_root = base.join("fleet");
    let in_tree = program_at(&scan_root.join("tools"), "prep");
    let workshop = base.join("workshop");
    let path = format!(
        "{}:{}:{}",
        system.parent().unwrap().display(),
        installed.parent().unwrap().display(),
        std::env::var("PATH").unwrap_or_default()
    );
    let _env = crate::test_environment::Environment::set(&[("PATH", &path), ("HOME", &home.to_string_lossy())]);

    assert_eq!(hook_program("sdx", &scan_root, Some(&workshop)).unwrap(), system, "not a plugin: the PATH's");

    let sdx = crate::plugin::PluginName::parse("sdx").unwrap();
    crate::plugin::enable(&workshop, &sdx).unwrap();
    let refused = hook_program("sdx", &scan_root, Some(&workshop)).unwrap_err().to_string();
    assert!(refused.contains("wsb plugin add sdx --program"), "{refused}");

    crate::plugin::set_program(&workshop, &sdx, &installed).unwrap();
    assert_eq!(hook_program("sdx", &scan_root, Some(&workshop)).unwrap(), installed);
    std::fs::remove_file(&installed).unwrap();
    let gone = hook_program("sdx", &scan_root, Some(&workshop)).unwrap_err().to_string();
    assert!(gone.contains("is not there"), "{gone}");

    assert_eq!(hook_program("make-ish", &scan_root, Some(&workshop)).unwrap(), other);
    assert_eq!(hook_program("~/bin/tool", &scan_root, Some(&workshop)).unwrap(), in_home);
    assert_eq!(hook_program("tools/prep", &scan_root, Some(&workshop)).unwrap(), in_tree);
    assert!(hook_program("tools/missing", &scan_root, Some(&workshop)).is_err());
}