yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
use super::*;
use crate::cli_outbound::{Binary, Cli};
use crate::test_support::spawn_guard;
use std::cell::Cell;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::MutexGuard;
use tempfile::tempdir;

/// A canned [`ToolProbe`] recording every probed argv (so the normative verb list
/// is asserted behaviorally, the private consts staying private). `mode` drives the
/// classification arm: every verb passes, a named verb fails (exit 2), or the
/// binary is absent (spawn error).
enum Mode {
    AllOk,
    Fail(Binary, &'static str),
    Absent,
}

struct Fake {
    mode: Mode,
    calls: Cell<Vec<Vec<String>>>,
}

impl Fake {
    fn new(mode: Mode) -> Self {
        Self {
            mode,
            calls: Cell::new(Vec::new()),
        }
    }
    /// The `argv[0]` (verb) of each probe, in order — the list under test.
    fn verbs(&self) -> Vec<String> {
        let calls = self.calls.take();
        let verbs = calls.iter().filter_map(|a| a.first().cloned()).collect();
        self.calls.set(calls);
        verbs
    }
}

impl ToolProbe for Fake {
    fn status(&self, binary: Binary, args: &[&str]) -> io::Result<i32> {
        let mut calls = self.calls.take();
        calls.push(args.iter().map(|s| (*s).to_owned()).collect());
        self.calls.set(calls);
        match &self.mode {
            Mode::AllOk => Ok(0),
            Mode::Absent => Err(io::Error::other("boom")),
            Mode::Fail(b, v) => Ok(i32::from(binary == *b && args.first() == Some(v)) * 2),
        }
    }
}

/// Write an executable fake tool under `SPAWN_LOCK` (the crate spawn discipline);
/// the guard is held across the spawn the caller then does.
fn write_exe(dir: &Path, name: &str, body: &str) -> (PathBuf, MutexGuard<'static, ()>) {
    let guard = spawn_guard();
    let path = dir.join(name);
    std::fs::write(&path, body).unwrap();
    let mut perms = std::fs::metadata(&path).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&path, perms).unwrap();
    (path, guard)
}

#[test]
fn remediation_and_tool_name_cover_every_tool() {
    for b in [Binary::Bl, Binary::Lernie, Binary::Bz] {
        assert!(remediation(b).contains("cargo install --locked"));
    }
    assert_eq!(remediation(Binary::Bl), BL_FIX);
    assert_eq!(remediation(Binary::Lernie), LERNIE_FIX);
    assert_eq!(remediation(Binary::Bz), BZ_FIX);
    assert_eq!(tool_name(Binary::Bl), "bl");
    assert_eq!(tool_name(Binary::Lernie), "lernie");
    assert_eq!(tool_name(Binary::Bz), "bz");
}

#[test]
fn permits_only_ok_refuses_mismatch_and_missing() {
    assert!(ToolState::Ok.permits());
    assert!(
        !ToolState::Mismatch {
            verb: "prime".to_owned(),
            remediation: "fix".to_owned(),
        }
        .permits()
    );
    assert!(
        !ToolState::Missing {
            remediation: "fix".to_owned(),
        }
        .permits()
    );
}

#[test]
fn remediation_accessor_and_describe_read_each_state() {
    assert_eq!(ToolState::Ok.remediation(), None);
    assert_eq!(ToolState::Ok.describe(), "ok");
    let mismatch = ToolState::Mismatch {
        verb: "prime".to_owned(),
        remediation: "reinstall lernie".to_owned(),
    };
    assert_eq!(mismatch.remediation(), Some("reinstall lernie"));
    assert_eq!(
        mismatch.describe(),
        "missing verb `prime` — reinstall lernie"
    );
    let missing = ToolState::Missing {
        remediation: "install bl".to_owned(),
    };
    assert_eq!(missing.remediation(), Some("install bl"));
    assert_eq!(missing.describe(), "not found — install bl");
}

#[test]
fn classify_probes_the_normative_lernie_and_bl_verbs_by_help() {
    let lernie = Fake::new(Mode::AllOk);
    assert_eq!(classify(&lernie, Binary::Lernie), ToolState::Ok);
    assert_eq!(
        lernie.verbs(),
        [
            "prime", "new", "prompt", "message", "stop", "scan", "config"
        ]
    );
    // The suffix is `--help` (the read-only capability probe).
    assert!(
        lernie
            .calls
            .take()
            .iter()
            .all(|a| a.get(1).map(String::as_str) == Some("--help"))
    );

    let bl = Fake::new(Mode::AllOk);
    assert_eq!(classify(&bl, Binary::Bl), ToolState::Ok);
    assert_eq!(
        bl.verbs(),
        [
            "create", "claim", "unclaim", "close", "update", "list", "show", "conf"
        ]
    );
}

#[test]
fn classify_probes_bz_by_version() {
    let bz = Fake::new(Mode::AllOk);
    assert_eq!(classify(&bz, Binary::Bz), ToolState::Ok);
    assert_eq!(bz.verbs(), ["--version"]);
}

#[test]
fn classify_names_the_missing_verb_with_remediation() {
    // A non-first verb fails, so the earlier verbs pass (the continue arm) and
    // the failing verb is named — the gate working, not a false negative.
    let f = Fake::new(Mode::Fail(Binary::Lernie, "scan"));
    assert_eq!(
        classify(&f, Binary::Lernie),
        ToolState::Mismatch {
            verb: "scan".to_owned(),
            remediation: LERNIE_FIX.to_owned(),
        }
    );
    // bz's `--version` capability can mismatch too.
    let bz = Fake::new(Mode::Fail(Binary::Bz, "--version"));
    assert_eq!(
        classify(&bz, Binary::Bz),
        ToolState::Mismatch {
            verb: "--version".to_owned(),
            remediation: BZ_FIX.to_owned(),
        }
    );
}

#[test]
fn classify_reports_an_unspawnable_binary_missing() {
    let f = Fake::new(Mode::Absent);
    assert_eq!(
        classify(&f, Binary::Bl),
        ToolState::Missing {
            remediation: BL_FIX.to_owned(),
        }
    );
}

#[test]
fn probe_classifies_the_whole_toolchain_and_gates_per_tool() {
    let all_ok = probe(&Fake::new(Mode::AllOk));
    assert!(all_ok.permits(Binary::Bl));
    assert!(all_ok.permits(Binary::Lernie));
    assert!(all_ok.permits(Binary::Bz));
    assert!(all_ok.require(Binary::Lernie).is_ok());

    // A stale lernie (missing `prime`) refuses lernie's verbs; bl/bz still permit.
    let stale = probe(&Fake::new(Mode::Fail(Binary::Lernie, "prime")));
    assert!(!stale.permits(Binary::Lernie));
    assert!(stale.permits(Binary::Bl));
    let refusal = stale.require(Binary::Lernie).unwrap_err();
    assert_eq!(refusal.tool, "lernie");
    assert_eq!(
        refusal.state,
        ToolState::Mismatch {
            verb: "prime".to_owned(),
            remediation: LERNIE_FIX.to_owned(),
        }
    );
    // The refusal Display names the tool and the missing verb + remediation.
    let msg = refusal.to_string();
    assert!(msg.contains("lernie"));
    assert!(msg.contains("missing verb `prime`"));
    assert!(msg.contains("cargo install --locked lernie"));
}

#[test]
fn rows_lists_the_three_tools_in_order() {
    let state = probe(&Fake::new(Mode::AllOk));
    let rows = state.rows();
    assert_eq!(rows.map(|(name, _)| name), ["bl", "lernie", "bz"]);
    assert_eq!(rows[0].1, &ToolState::Ok);
}

#[test]
fn cli_probe_returns_the_exit_code_and_errors_on_a_missing_binary() {
    let dir = tempdir().unwrap();
    // One guard (from the bl write) held across the bz write and every spawn.
    let (bl, _g) = write_exe(dir.path(), "bl", "#!/bin/sh\nexit 0\n");
    let bz = dir.path().join("bz");
    std::fs::write(&bz, "#!/bin/sh\nexit 2\n").unwrap();
    let mut perms = std::fs::metadata(&bz).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&bz, perms).unwrap();
    let probe = CliProbe::new(
        Cli::new(bl),
        Cli::new(dir.path().join("lernie-absent")),
        Cli::new(bz),
    );
    // A verb that exits 0 ⇒ the probe passes; a non-zero exit rides back as the
    // code; an unspawnable binary is an error (classified Missing upstream).
    assert_eq!(probe.status(Binary::Bl, &["list", "--help"]).unwrap(), 0);
    assert_eq!(probe.status(Binary::Bz, &["--version"]).unwrap(), 2);
    assert!(probe.status(Binary::Lernie, &["prime", "--help"]).is_err());
}