yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! View-model transitions driven by a fake [`BzRunner`]/[`FileIo`] pair —
//! no live `bz`, no real disk. Every Apply terminal state (§9.1) and every
//! derived read has a test here.

use super::*;
use crate::test_support::FakeFs;
use crate::xdg::{Env, Os};
use std::path::PathBuf;
use std::sync::Mutex;

/// A runner that returns one preset outcome and logs each call.
struct FakeRunner {
    outcome: BzOutcome,
    log: Mutex<Vec<String>>,
}

impl FakeRunner {
    fn ok() -> Self {
        Self::with(true, "", "")
    }
    fn with(success: bool, stdout: &str, stderr: &str) -> Self {
        Self {
            outcome: BzOutcome {
                success,
                stdout: stdout.into(),
                stderr: stderr.into(),
            },
            log: Mutex::new(Vec::new()),
        }
    }

    /// The call log, locked (poison-immune).
    fn log(&self) -> std::sync::MutexGuard<'_, Vec<String>> {
        self.log
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }
}

impl BzRunner for FakeRunner {
    fn dump_config_at(&self, _config: &Path) -> BzOutcome {
        self.log().push("at".into());
        self.outcome.clone()
    }
    fn dump_config_effective(&self) -> BzOutcome {
        self.log().push("effective".into());
        self.outcome.clone()
    }
    fn list_models(&self, provider: &str) -> BzOutcome {
        self.log().push(format!("list:{provider}"));
        self.outcome.clone()
    }
}

fn cfg() -> PathBuf {
    PathBuf::from("/cfg/brazen/config.toml")
}

fn paths() -> BrazenPaths {
    BrazenPaths {
        config: cfg(),
        credentials_dir: PathBuf::from("/creds"),
        models_cache_dir: PathBuf::from("/cache/models"),
    }
}

fn loaded(fs: &FakeFs) -> BrazenEditor {
    BrazenEditor::load(paths(), fs).unwrap()
}

#[test]
fn load_missing_file_is_empty_draft() {
    let ed = loaded(&FakeFs::default());
    assert_eq!(ed.draft(), "");
}

#[test]
fn load_reads_existing_bytes_into_draft() {
    let ed = loaded(&FakeFs::seed(&cfg(), b"name = \"openai\"\n"));
    assert_eq!(ed.draft(), "name = \"openai\"\n");
}

#[test]
fn draft_mut_and_set_draft_edit_the_buffer() {
    let mut ed = loaded(&FakeFs::default());
    ed.draft_mut().push_str("x = 1");
    assert_eq!(ed.draft(), "x = 1");
    ed.set_draft("y = 2".into());
    assert_eq!(ed.draft(), "y = 2");
}

#[test]
fn apply_ok_validates_guards_and_renames() {
    let fs = FakeFs::seed(&cfg(), b"A");
    let mut ed = loaded(&fs);
    ed.set_draft("B".into());
    let runner = FakeRunner::ok();
    assert_eq!(ed.apply(&runner, &fs), Applied::Ok);
    // The draft is now the on-disk content.
    assert_eq!(fs.get(&cfg()), Some(b"B".to_vec()));
    // A repeat Apply succeeds — the loaded hash tracked the rename.
    ed.set_draft("C".into());
    assert_eq!(ed.apply(&runner, &fs), Applied::Ok);
    assert_eq!(fs.get(&cfg()), Some(b"C".to_vec()));
}

#[test]
fn apply_rejects_on_nonzero_bz_exit_keeping_draft() {
    let fs = FakeFs::seed(&cfg(), b"A");
    let mut ed = loaded(&fs);
    ed.set_draft("bad".into());
    let runner = FakeRunner::with(false, "", "MalformedFile at line 3");
    assert_eq!(
        ed.apply(&runner, &fs),
        Applied::Rejected {
            stderr: "MalformedFile at line 3".into()
        }
    );
    // Draft kept, real file untouched.
    assert_eq!(ed.draft(), "bad");
    assert_eq!(fs.get(&cfg()), Some(b"A".to_vec()));
}

#[test]
fn apply_refuses_on_concurrent_edit_with_conflict() {
    let fs = FakeFs::seed(&cfg(), b"A");
    let mut ed = loaded(&fs);
    ed.set_draft("B".into());
    // Another writer changes the file after load.
    fs.map().insert(cfg(), b"C".to_vec());
    assert_eq!(ed.apply(&FakeRunner::ok(), &fs), Applied::Conflict);
    // The concurrent content is preserved.
    assert_eq!(fs.get(&cfg()), Some(b"C".to_vec()));
}

#[test]
fn apply_maps_a_filesystem_error_to_io() {
    let fs = FakeFs {
        fail_write: true,
        ..FakeFs::default()
    };
    let mut ed = loaded(&fs);
    ed.set_draft("B".into());
    assert!(matches!(
        ed.apply(&FakeRunner::ok(), &fs),
        Applied::Io { .. }
    ));
}

#[test]
fn reload_recovers_the_on_disk_content_and_hash() {
    let fs = FakeFs::seed(&cfg(), b"A");
    let mut ed = loaded(&fs);
    ed.set_draft("stale".into());
    fs.map().insert(cfg(), b"D".to_vec());
    ed.reload(&fs).unwrap();
    assert_eq!(ed.draft(), "D");
    // After reload the guard matches the new content, so Apply lands.
    ed.set_draft("E".into());
    assert_eq!(ed.apply(&FakeRunner::ok(), &fs), Applied::Ok);
}

#[test]
fn effective_runs_dump_config_against_real_env() {
    let ed = loaded(&FakeFs::default());
    let runner = FakeRunner::with(true, "merged config", "");
    let out = ed.effective(&runner);
    assert_eq!(out.stdout, "merged config");
    assert_eq!(*runner.log(), vec!["effective".to_string()]);
}

#[test]
fn credential_presence_scans_providers_and_stats_files() {
    let text = "[[providers]]\nname = \"openai\"\nurl = \"x\"\n\
                name = \"anthropic\"\nname = \"openai\"\nnotname = \"z\"\nname = bare\n";
    let fs = FakeFs::seed(&cfg(), text.as_bytes());
    // openai has a credential file; anthropic does not.
    fs.map()
        .insert(PathBuf::from("/creds/openai.json"), b"{}".to_vec());
    let ed = loaded(&fs);
    assert_eq!(
        ed.credential_presence(&fs),
        vec![
            ("openai".to_string(), true),
            ("anthropic".to_string(), false),
        ]
    );
}

#[test]
fn model_cache_reads_present_and_absent() {
    let fs = FakeFs::seed(
        &PathBuf::from("/cache/models/openai.json"),
        b"[{\"id\":\"gpt\"}]",
    );
    let ed = loaded(&fs);
    assert_eq!(
        ed.model_cache("openai", &fs).unwrap().as_deref(),
        Some("[{\"id\":\"gpt\"}]")
    );
    assert_eq!(ed.model_cache("gemini", &fs).unwrap(), None);
}

#[test]
fn refresh_models_dispatches_list_models() {
    let ed = loaded(&FakeFs::default());
    let runner = FakeRunner::ok();
    let _ = ed.refresh_models("openai", &runner);
    assert_eq!(*runner.log(), vec!["list:openai".to_string()]);
}

#[test]
fn built_in_rows_hint_is_a_static_line() {
    assert!(BUILT_IN_ROWS_HINT.contains("built-in"));
}

#[test]
fn brazen_paths_resolve_folds_config_creds_and_cache() {
    let env = Env::from_pairs([
        ("HOME", "/home/u"),
        ("XDG_CONFIG_HOME", "/home/u/.config"),
        ("XDG_DATA_HOME", "/home/u/.local/share"),
        ("XDG_CACHE_HOME", "/home/u/.cache"),
    ]);
    let p = BrazenPaths::resolve(&env, Os::Linux);
    assert_eq!(
        p.config,
        PathBuf::from("/home/u/.config/brazen/config.toml")
    );
    assert_eq!(
        p.credentials_dir,
        PathBuf::from("/home/u/.local/share/brazen/credentials")
    );
    assert_eq!(
        p.models_cache_dir,
        PathBuf::from("/home/u/.cache/brazen/models")
    );
}