yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
use super::*;
use crate::test_support::spawn_guard;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tempfile::tempdir;

/// A canned [`ConfRunner`]: fixed responses to the two reads and to any write,
/// or a spawn error. Pure — the writes [`apply`] issues are verified through the
/// `ops.jsonl` it logs, not through recorded calls, so no interior mutability.
struct Fake {
    remote: Outcome,
    branch: Outcome,
    write: Outcome,
    err: bool,
}

impl Fake {
    /// A primed checkout: `task-remote` = `remote_out` (provenance `remote_prov`),
    /// `task-branch` = `branch_out`, every write succeeds.
    fn ok(remote_out: &str, remote_prov: &str, branch_out: &str) -> Self {
        Self {
            remote: out(0, remote_out, remote_prov),
            branch: out(0, branch_out, "conf: task-branch from landing"),
            write: out(0, "", "conf set"),
            err: false,
        }
    }
}

impl ConfRunner for Fake {
    fn conf(&self, _project: &Path, args: &[&str]) -> io::Result<Outcome> {
        if self.err {
            return Err(io::Error::other("boom"));
        }
        Ok(match args {
            ["conf", "task-remote"] => self.remote.clone(),
            ["conf", "task-branch"] => self.branch.clone(),
            _ => self.write.clone(),
        })
    }
}

fn out(exit: i32, stdout: &str, stderr: &str) -> Outcome {
    Outcome {
        exit,
        stdout: stdout.to_owned(),
        stderr: stderr.to_owned(),
    }
}

/// Owned `plan` output → borrowed argvs, for terse literal comparison.
fn argvs(p: &[Vec<String>]) -> Vec<Vec<&str>> {
    p.iter()
        .map(|a| a.iter().map(String::as_str).collect())
        .collect()
}

#[test]
fn custom_branch_rejects_empty_and_default() {
    assert_eq!(Mode::custom_branch(""), None);
    assert_eq!(Mode::custom_branch("   "), None);
    assert_eq!(Mode::custom_branch("balls/tasks"), None);
    assert_eq!(
        Mode::custom_branch("  my/universe  "),
        Some(Mode::CustomBranch("my/universe".to_owned()))
    );
}

#[test]
fn plan_maps_each_mode_to_its_conf_writes() {
    assert_eq!(
        argvs(&plan(&Mode::Shared)),
        vec![
            vec!["conf", "set", "task-remote", "origin"],
            vec!["conf", "set", "task-branch", "balls/tasks"],
        ]
    );
    assert_eq!(
        argvs(&plan(&Mode::Stealth)),
        vec![vec!["conf", "set", "task-remote", "none"]]
    );
    assert_eq!(
        argvs(&plan(&Mode::CustomBranch("my/x".to_owned()))),
        vec![
            vec!["conf", "set", "task-branch", "my/x"],
            vec!["conf", "set", "task-remote", "origin"],
        ]
    );
}

#[test]
fn read_unprimed_project_is_none() {
    let fake = Fake {
        remote: out(1, "", "bl: no balls checkout here — run `bl prime` first"),
        branch: out(0, "balls/tasks", ""),
        write: out(0, "", ""),
        err: false,
    };
    assert_eq!(read(&fake, Path::new("/p")).unwrap(), None);
}

#[test]
fn read_classifies_stealth_shared_and_custom() {
    // Stealth via the `(none)` sentinel; the remote's provenance is kept.
    let s = read(
        &Fake::ok("(none)", "conf: task-remote from stealth", "balls/tasks"),
        Path::new("/p"),
    )
    .unwrap()
    .unwrap();
    assert_eq!(s.mode, Mode::Stealth);
    assert_eq!(s.provenance, "stealth");

    // Stealth via an EMPTY remote value (the `set task-remote ""` shape).
    let s = read(
        &Fake::ok("", "conf: task-remote from binding", "balls/tasks"),
        Path::new("/p"),
    )
    .unwrap()
    .unwrap();
    assert_eq!(s.mode, Mode::Stealth);

    // A real remote + the default branch = shared coordination.
    let s = read(
        &Fake::ok("origin", "conf: task-remote from binding", "balls/tasks"),
        Path::new("/p"),
    )
    .unwrap()
    .unwrap();
    assert_eq!(s.mode, Mode::Shared);
    assert_eq!(s.provenance, "binding");

    // A real remote + a non-default branch = a parallel universe.
    let s = read(
        &Fake::ok("origin", "conf: task-remote from origin", "team/parallel"),
        Path::new("/p"),
    )
    .unwrap()
    .unwrap();
    assert_eq!(s.mode, Mode::CustomBranch("team/parallel".to_owned()));
}

#[test]
fn read_provenance_falls_back_to_the_whole_line() {
    let s = read(
        &Fake::ok("origin", "unexpected shape", "balls/tasks"),
        Path::new("/p"),
    )
    .unwrap()
    .unwrap();
    assert_eq!(s.provenance, "unexpected shape");
}

#[test]
fn read_propagates_a_spawn_error() {
    let fake = Fake {
        remote: out(0, "", ""),
        branch: out(0, "", ""),
        write: out(0, "", ""),
        err: true,
    };
    assert!(read(&fake, Path::new("/p")).is_err());
}

#[test]
fn apply_stealth_logs_one_write_and_rereads() {
    let dir = tempdir().unwrap();
    let sr = dir.path();
    let fake = Fake::ok("(none)", "conf: task-remote from landing", "balls/tasks");
    let after = apply(&fake, sr, "42", Path::new("/proj"), &Mode::Stealth)
        .unwrap()
        .unwrap();
    assert_eq!(after.mode, Mode::Stealth);
    let ops = opslog::tail(sr, 10);
    assert_eq!(ops.len(), 1);
    assert_eq!(
        ops[0].argv,
        vec!["bl", "conf", "set", "task-remote", "none"]
    );
    assert_eq!(ops[0].cwd, "/proj");
    assert_eq!(ops[0].exit, 0);
    assert_eq!(ops[0].ts, "42");
}

#[test]
fn apply_shared_logs_both_writes() {
    let dir = tempdir().unwrap();
    let sr = dir.path();
    let fake = Fake::ok("origin", "conf: task-remote from binding", "balls/tasks");
    let after = apply(&fake, sr, "1", Path::new("/proj"), &Mode::Shared)
        .unwrap()
        .unwrap();
    assert_eq!(after.mode, Mode::Shared);
    let ops = opslog::tail(sr, 10);
    assert_eq!(ops.len(), 2);
    assert_eq!(
        ops[0].argv,
        vec!["bl", "conf", "set", "task-remote", "origin"]
    );
    assert_eq!(
        ops[1].argv,
        vec!["bl", "conf", "set", "task-branch", "balls/tasks"]
    );
}

#[test]
fn apply_stops_at_the_first_failed_write() {
    let dir = tempdir().unwrap();
    let sr = dir.path();
    let fake = Fake {
        remote: out(0, "origin", "conf: task-remote from binding"),
        branch: out(0, "balls/tasks", "conf: task-branch from landing"),
        write: out(1, "", "conf set: refused"),
        err: false,
    };
    // Shared plans two writes; the first fails, so only it is logged.
    let after = apply(&fake, sr, "9", Path::new("/proj"), &Mode::Shared).unwrap();
    assert!(after.is_some());
    let ops = opslog::tail(sr, 10);
    assert_eq!(ops.len(), 1);
    assert_eq!(ops[0].exit, 1);
    assert_eq!(ops[0].stderr, "conf set: refused");
}

#[test]
fn apply_propagates_a_spawn_error() {
    let dir = tempdir().unwrap();
    let fake = Fake {
        remote: out(0, "", ""),
        branch: out(0, "", ""),
        write: out(0, "", ""),
        err: true,
    };
    assert!(apply(&fake, dir.path(), "0", Path::new("/proj"), &Mode::Stealth).is_err());
}

/// An executable `bl` recorder: appends argv (one per line) to `log`, drops a
/// marker file `here` in its cwd (to prove `run_in` set it), prints canned
/// stdout/stderr, exits 0.
fn recorder(dir: &Path, log: &Path) -> PathBuf {
    let path = dir.join("bl");
    let body = format!(
        "#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {}; done\n\
         : > here\nprintf 'origin'\nprintf 'conf: task-remote from binding' 1>&2\nexit 0\n",
        log.display()
    );
    fs::write(&path, body).unwrap();
    let mut perms = fs::metadata(&path).unwrap().permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&path, perms).unwrap();
    path
}

#[test]
fn blconf_runs_in_project_and_captures_output() {
    let g = spawn_guard();
    let dir = tempdir().unwrap();
    let proj = tempdir().unwrap();
    let log = dir.path().join("argv");
    let bin = recorder(dir.path(), &log);
    let runner = BlConf::new(Cli::new(bin));
    let out = runner.conf(proj.path(), &["conf", "task-remote"]).unwrap();
    drop(g);
    assert_eq!(fs::read_to_string(&log).unwrap(), "conf\ntask-remote\n");
    // The marker proves the child ran with cwd = the project.
    assert!(proj.path().join("here").exists());
    assert_eq!(out.stdout, "origin");
    assert_eq!(out.stderr, "conf: task-remote from binding");
    assert!(out.ok());
}

#[test]
fn blconf_resolve_uses_the_bl_binary_name() {
    // The world env supplies only the standing overrides, not the binary name.
    let world = crate::xdg::Env::from_pairs([("HOME", "/h"), ("XDG_DATA_HOME", "/d")]);
    assert_eq!(BlConf::resolve(&world).cli.binary(), Path::new("bl"));
}