yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The production [`BzRunner`] impl — the thin shell behind the pure
//! view-model. [`RealBzRunner`] wraps [`Cli`] (its `run` collected to a
//! [`BzOutcome`]), covered the way `cli_outbound`/`lock_probe` are: recorder
//! scripts. The filesystem seam ([`RealFileIo`](super::super::RealFileIo)) is
//! shared across every editor and lives in `config_edit::effects`.

use super::{BzOutcome, BzRunner};
use crate::cli_outbound::{Binary, Chunk, Cli, ExitInfo};
use crate::xdg::Env;
use std::path::Path;

/// `bz` runner over a resolved [`Cli`]. Every method is `bz <control-flag>…`
/// collected to one [`BzOutcome`].
#[derive(Debug, Clone)]
pub struct RealBzRunner {
    cli: Cli,
}

impl RealBzRunner {
    pub fn new(cli: Cli) -> Self {
        Self { cli }
    }

    /// Resolve `bz` via `BZ_BINARY` / PATH (DESIGN §8) in the composed `world`
    /// (§16.6 W2): the standing overrides nest lernie/balls state, while
    /// `BRAZEN_CONFIG` rides through ambient (§16.2) — so validation calls
    /// (`bz --dump-config …`) read the shared ambient config, which is exactly
    /// the file yog edits.
    pub fn resolve(world: &Env) -> Self {
        Self::new(Cli::resolve_in_world(
            Binary::Bz,
            &crate::world::overrides(world),
        ))
    }

    /// Spawn `bz <args>`, drain stdout/stderr, fold to a [`BzOutcome`]. A
    /// spawn failure is itself a non-success outcome carrying the error.
    fn collect(&self, args: &[&str]) -> BzOutcome {
        match self.cli.run(args) {
            Ok(stream) => {
                let mut out = Vec::new();
                let mut err = Vec::new();
                let mut success = false;
                for chunk in stream {
                    match chunk {
                        Chunk::Stdout(b) => out.extend(b),
                        Chunk::Stderr(b) => err.extend(b),
                        Chunk::Exited(e) => success = e == ExitInfo::Code(0),
                    }
                }
                BzOutcome {
                    success,
                    stdout: String::from_utf8_lossy(&out).into_owned(),
                    stderr: String::from_utf8_lossy(&err).into_owned(),
                }
            }
            Err(e) => BzOutcome {
                success: false,
                stdout: String::new(),
                stderr: e.to_string(),
            },
        }
    }
}

impl BzRunner for RealBzRunner {
    fn dump_config_at(&self, config: &Path) -> BzOutcome {
        self.collect(&["--config", &config.display().to_string(), "--dump-config"])
    }

    fn dump_config_effective(&self) -> BzOutcome {
        self.collect(&["--dump-config"])
    }

    fn list_models(&self, provider: &str) -> BzOutcome {
        self.collect(&["--list-models", "--provider", provider, "--json"])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::spawn_guard;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use std::path::PathBuf;
    use tempfile::tempdir;

    /// Write an executable recorder that logs argv to `log`, prints canned
    /// stdout/stderr, and exits `code`. Returns the script path; the caller
    /// holds `SPAWN_LOCK` across write+spawn (the ETXTBSY discipline).
    fn recorder(dir: &Path, log: &Path, code: i32) -> PathBuf {
        let path = dir.join("bz");
        let body = format!(
            "#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {}; done\n\
             printf 'OUT'\nprintf 'ERR' 1>&2\nexit {}\n",
            log.display(),
            code
        );
        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 resolve_uses_bz_binary_name() {
        // No BZ_BINARY manipulation: default PATH name is `bz`. The world env
        // supplies only the standing overrides, not the binary name.
        let world = Env::from_pairs([("HOME", "/h"), ("XDG_DATA_HOME", "/d")]);
        assert_eq!(RealBzRunner::resolve(&world).cli.binary(), Path::new("bz"));
    }

    #[test]
    fn dump_config_at_argv_and_success() {
        let g = spawn_guard();
        let dir = tempdir().unwrap();
        let log = dir.path().join("argv");
        let bin = recorder(dir.path(), &log, 0);
        let runner = RealBzRunner::new(Cli::new(bin));
        let out = runner.dump_config_at(Path::new("/x/temp.toml"));
        drop(g);
        assert_eq!(
            fs::read_to_string(&log).unwrap(),
            "--config\n/x/temp.toml\n--dump-config\n"
        );
        assert_eq!(
            out,
            BzOutcome {
                success: true,
                stdout: "OUT".into(),
                stderr: "ERR".into(),
            }
        );
    }

    #[test]
    fn dump_config_effective_argv() {
        let g = spawn_guard();
        let dir = tempdir().unwrap();
        let log = dir.path().join("argv");
        let bin = recorder(dir.path(), &log, 0);
        let runner = RealBzRunner::new(Cli::new(bin));
        let _ = runner.dump_config_effective();
        drop(g);
        assert_eq!(fs::read_to_string(&log).unwrap(), "--dump-config\n");
    }

    #[test]
    fn list_models_argv_and_nonzero_is_failure() {
        let g = spawn_guard();
        let dir = tempdir().unwrap();
        let log = dir.path().join("argv");
        let bin = recorder(dir.path(), &log, 3);
        let runner = RealBzRunner::new(Cli::new(bin));
        let out = runner.list_models("openai");
        drop(g);
        assert_eq!(
            fs::read_to_string(&log).unwrap(),
            "--list-models\n--provider\nopenai\n--json\n"
        );
        assert!(!out.success);
        assert_eq!(out.stderr, "ERR");
    }

    #[test]
    fn spawn_failure_is_a_nonsuccess_outcome() {
        let _g = spawn_guard();
        let runner = RealBzRunner::new(Cli::new("/definitely/not/a/real/bz-xyz"));
        let out = runner.dump_config_effective();
        assert!(!out.success);
        assert!(!out.stderr.is_empty());
    }
}