yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! The detached `lernie prompt` (§8.1, §3.3): the fire-time identity stamp, the
//! `YOG_NAME` layer, the per-rung cwd, and the spawn-only ops line. A fifo
//! rendezvous proves the detached child ran and captures its argv + env.

use super::{World, write_exec};
use crate::cli_outbound::Cli;
use crate::start::{DETACHED_EXIT, execute_prompt, identity_preamble};
use crate::test_support::spawn_guard;
use std::path::Path;

/// A blocking fifo — reading it rendezvous with the detached child's write.
fn make_fifo(path: &Path) {
    let status = std::process::Command::new("mkfifo")
        .args(["-m", "600"])
        .arg(path)
        .status()
        .expect("spawn mkfifo");
    assert!(status.success(), "mkfifo");
}

#[test]
fn prompt_stamps_identity_layers_yog_name_and_logs_the_sentinel() {
    let _g = spawn_guard();
    let w = World::new();
    let fifo = w.bin.path().join("report");
    make_fifo(&fifo);
    // The fake records the argv (verb/ws/goal) and the layered YOG_NAME.
    let body = format!(
        "#!/bin/sh\nprintf '%s\\037%s\\037%s\\037%s' \"$1\" \"$2\" \"$3\" \"$YOG_NAME\" > '{}'\n",
        fifo.display()
    );
    let lernie = Cli::new(write_exec(w.bin.path(), "lernie", &body));
    let ws = w.yog.path().join("ws");
    let cwd = w.balls.path(); // an existing dir → a valid detached cwd
    execute_prompt(
        &lernie,
        w.state.path(),
        "TS",
        "cobalt-gecko",
        cwd,
        &ws,
        "do it",
    )
    .unwrap();

    let recorded = std::fs::read_to_string(&fifo).unwrap();
    let fields: Vec<&str> = recorded.split('\u{1f}').collect();
    assert_eq!(fields[0], "prompt");
    assert_eq!(fields[1], ws.to_string_lossy());
    // The identity preamble is stamped at fire, ahead of the typed payload (§3.3).
    assert_eq!(
        fields[2],
        format!("{}\n\ndo it", identity_preamble("cobalt-gecko"))
    );
    assert_eq!(fields[3], "cobalt-gecko", "YOG_NAME layered (§8)");

    let e = &w.ops()[0];
    assert_eq!(e.argv[1], "prompt");
    assert_eq!(e.exit, DETACHED_EXIT);
    assert_eq!(e.cwd, cwd.display().to_string());
    assert!(e.stderr.is_empty(), "a clean launch logs no error");
    // The logged goal carries the stamp (small goal → unclipped).
    assert!(e.argv[3].contains("You are cobalt-gecko."));
}

#[test]
fn prompt_routes_the_drivers_stderr_to_its_per_spawn_sink() {
    let _g = spawn_guard();
    let w = World::new();
    let fifo = w.bin.path().join("report");
    make_fifo(&fifo);
    // A driver that refuses and dies the moment it launches — the §13.3 hole:
    // the spawn itself succeeds, so the ops line is a clean `-2`.
    let body = format!(
        "#!/bin/sh\nprintf 'refusing: version skew\\n' >&2\nprintf done > '{}'\n",
        fifo.display()
    );
    let lernie = Cli::new(write_exec(w.bin.path(), "lernie", &body));
    let ws = w.yog.path().join("ws");
    execute_prompt(&lernie, w.state.path(), "TS", "n", w.balls.path(), &ws, "g").unwrap();
    assert_eq!(std::fs::read_to_string(&fifo).unwrap(), "done");

    let e = &w.ops()[0];
    assert!(
        e.stderr.is_empty(),
        "the *logged* line records only the launch"
    );
    // The evidence lives in the sink, keyed by the same ts + workspace, and the
    // read-time fold turns the row into the failure the operator must see.
    let folded = crate::opslog::detached::fold(w.state.path(), e);
    assert_eq!(folded.stderr, "refusing: version skew\n");
    assert!(crate::opslog::OpRow::from(&folded).failed());
}

#[test]
fn prompt_clips_a_large_logged_goal() {
    let _g = spawn_guard();
    let w = World::new();
    let lernie = Cli::new(super::fake_lernie(w.bin.path()));
    let ws = w.yog.path().join("ws");
    // Each byte JSON-escapes to `\u00XX` (6 bytes): a raw-byte clip would let this
    // serialize to ~54 KB. The clip must hold POST-escape or the `ops.jsonl` line
    // blows past CAP/PIPE_BUF and loses its two-instance atomic append (§4.2).
    let big = "\u{1}".repeat(9000);
    execute_prompt(
        &lernie,
        w.state.path(),
        "TS",
        "n",
        w.balls.path(),
        &ws,
        &big,
    )
    .unwrap();
    let entry = &w.ops()[0];
    assert!(
        entry.argv[3].contains("bytes elided"),
        "the logged goal is clipped (§4.2)"
    );
    assert!(
        crate::opslog::build_line(entry).len() <= crate::opslog::CAP,
        "the serialized ops line stays ≤ CAP after JSON escaping"
    );
}

#[test]
fn prompt_logs_the_spawn_failure_and_returns_err() {
    let _g = spawn_guard();
    let w = World::new();
    let lernie = Cli::new("/definitely/not/a/real/lernie-prompt");
    let ws = w.yog.path().join("ws");
    let err = execute_prompt(&lernie, w.state.path(), "TS", "n", w.balls.path(), &ws, "g").is_err();
    assert!(err, "a missing binary is surfaced");
    let e = &w.ops()[0];
    assert_eq!(e.exit, DETACHED_EXIT);
    assert!(!e.stderr.is_empty(), "the spawn error rides in stderr");
}