yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Ops-tail / surface-failure view-model tests (§4.2, §7.3), against the shared
//! cloned-project world in [`super`].

use super::{append_op, model, world};
use crate::AppModel;
use crate::opslog::{self, DETACHED_EXIT};
use std::fs;
use std::path::Path;

/// Append one `lernie prime` line for `cwd` with the given exit.
fn prime(m: &AppModel, cwd: &str, exit: i32) {
    opslog::append(
        m.state_root(),
        &opslog::OpEntry {
            ts: "TS".into(),
            argv: vec!["lernie".into(), "prime".into()],
            cwd: cwd.into(),
            exit,
            stdout: String::new(),
            stderr: if exit == 0 {
                String::new()
            } else {
                "unrecognized subcommand\n".into()
            },
        },
    )
    .unwrap();
}

#[test]
fn last_failure_projects_only_a_failed_last_op() {
    let w = world();
    let (_c, mut m) = model(&w);
    // No ops → no banner; a clean last op → still no banner (§7.3).
    assert!(m.last_failure().is_none());
    append_op(&m);
    m.after_lernie_verb();
    assert!(m.last_failure().is_none());
    // A failed last op → the surface failure view-model projects argv + tail.
    opslog::append(
        m.state_root(),
        &opslog::OpEntry {
            ts: "TS".into(),
            argv: vec!["lernie".into(), "prime".into()],
            cwd: "/proj".into(),
            exit: 2,
            stdout: String::new(),
            stderr: "prime failed\n".into(),
        },
    )
    .unwrap();
    m.after_lernie_verb();
    let f = m.last_failure().unwrap();
    assert_eq!(f.argv, "lernie prime");
    assert_eq!(f.stderr_tail, "prime failed");
}

#[test]
fn a_re_run_green_verb_retires_its_stale_failure_from_the_chip() {
    let w = world();
    let (_c, mut m) = model(&w);
    // The three-day-old wound: `lernie prime` failed, and stayed the ambient
    // error long after it was fixed (§6 retirement).
    prime(&m, "/w", 2);
    m.after_lernie_verb();
    assert_eq!(m.activity().errors, 1, "the fresh failure is live");
    // Re-run green: the failure retires from the chip but stays in the log.
    prime(&m, "/w", 0);
    m.after_lernie_verb();
    assert_eq!(
        m.activity(),
        opslog::Activity {
            total: 2,
            errors: 0
        }
    );
    assert_eq!(m.ops_rows().len(), 2, "the log keeps both lines");
    // An unrelated later failure is the one the operator sees.
    append_op(&m); // clean `bl close bl-work`
    prime(&m, "/other", 3);
    m.after_lernie_verb();
    assert_eq!(m.activity().errors, 1);
    assert_eq!(m.last_failure().unwrap().argv, "lernie prime");
}

#[test]
fn a_detached_prompt_that_died_after_launch_surfaces_on_the_next_sweep() {
    let w = world();
    let (_c, mut m) = model(&w);
    let ws = Path::new("/ws/cobalt-gecko");
    opslog::append(
        m.state_root(),
        &opslog::OpEntry {
            ts: "17".into(),
            argv: vec![
                "lernie".into(),
                "prompt".into(),
                ws.to_string_lossy().into_owned(),
                "goal".into(),
            ],
            cwd: "/ws".into(),
            exit: DETACHED_EXIT,
            stdout: String::new(),
            stderr: String::new(),
        },
    )
    .unwrap();
    // The launch alone: a clean `-2` row, nothing to see — the §13.3 hole.
    m.after_lernie_verb();
    assert!(m.last_failure().is_none());
    assert!(!m.ops_rows().last().unwrap().failed());
    assert!(!m.ops_rows().last().unwrap().has_output());

    // The driver then refuses and dies, leaving its cry in the sink file.
    let sink = opslog::detached::sink(m.state_root(), "17", ws);
    fs::create_dir_all(sink.parent().unwrap()).unwrap();
    fs::write(&sink, "brazen 0.0.2 refuses 0.0.3\n").unwrap();

    // The next sweep folds it in: the row becomes a rendered failure and the
    // originating surface gets its ichor-red banner (§7.3) — no new ops line.
    m.after_lernie_verb();
    let row = m.ops_rows().last().unwrap();
    assert!(
        row.failed(),
        "the dead driver's stderr makes the row a failure"
    );
    assert!(row.has_output(), "and the pane offers the expansion");
    assert_eq!(
        m.last_failure().unwrap().stderr_tail,
        "brazen 0.0.2 refuses 0.0.3"
    );
    assert_eq!(
        opslog::tail(m.state_root(), 8).len(),
        1,
        "no line rewritten"
    );
}