yog 0.0.66

yog: the standalone server for litany loops — the world, the balls and the conversations, behind one wire
Documentation
//! The `bl`-facing executors (§4.2, Z5): `bl create`, `bl claim` and the
//! claim cross-check — each leaving a durable ops row, and a
//! `["yog-step",…]` line for the non-spawn failures. The workspace-and-name
//! executors are [`super::ensure`]'s concern.

use super::{World, fake_bl, fake_fail};
use crate::binding::work_worktree_path;
use crate::cli_outbound::Cli;

use crate::start::{StartError, cross_check_claim, execute_claim, execute_create};

#[test]
fn create_stamps_the_workspace_name_and_captures_the_id() {
    let w = World::new();
    let bl = Cli::new(fake_bl(w.bin.path(), "bl-new9", w.balls.path()));
    let id = execute_create(
        &bl,
        w.state.path(),
        "TS",
        w.project.path(),
        "Wire",
        "the body",
        "cobalt-gecko",
    )
    .unwrap();
    assert_eq!(id, "bl-new9");
    assert_eq!(
        &w.ops()[0].argv[1..],
        &[
            "create",
            "Wire",
            "--as",
            "cobalt-gecko",
            "--body",
            "the body"
        ]
    );
}

#[test]
fn create_elides_an_empty_body() {
    let w = World::new();
    let bl = Cli::new(fake_bl(w.bin.path(), "bl-1", w.balls.path()));
    execute_create(&bl, w.state.path(), "TS", w.project.path(), "T", "", "n").unwrap();
    assert_eq!(&w.ops()[0].argv[1..], &["create", "T", "--as", "n"]);
}

#[test]
fn create_errors_on_a_nonzero_exit() {
    let w = World::new();
    let bl = Cli::new(fake_fail(w.bin.path(), "bl", "boom"));
    let err =
        execute_create(&bl, w.state.path(), "TS", w.project.path(), "T", "", "n").unwrap_err();
    assert!(matches!(err, StartError::VerbFailed { verb: "create", .. }));
}

#[test]
fn claim_stamps_the_name_and_cross_checks_the_canonical_worktree() {
    let w = World::new();
    let canonical = work_worktree_path(w.balls.path(), w.project.path(), "bl-7", None);
    let bl = Cli::new(fake_bl(w.bin.path(), "x", &canonical));
    let r = execute_claim(
        &bl,
        w.state.path(),
        "TS",
        w.project.path(),
        "bl-7",
        "cobalt-gecko",
        w.balls.path(),
    )
    .unwrap();
    assert_eq!(r.worktree, canonical);
    assert!(!r.suffixed);
    assert_eq!(
        &w.ops()[0].argv[1..],
        &["claim", "bl-7", "--as", "cobalt-gecko"]
    );
}

#[test]
fn claim_errors_on_a_nonzero_exit() {
    let w = World::new();
    let bl = Cli::new(fake_fail(w.bin.path(), "bl", "blocked"));
    let err = execute_claim(
        &bl,
        w.state.path(),
        "TS",
        w.project.path(),
        "bl-7",
        "n",
        w.balls.path(),
    )
    .unwrap_err();
    let StartError::VerbFailed { verb, outcome } = err else {
        panic!("expected VerbFailed, got {err:?}");
    };
    assert_eq!(verb, "claim");
    assert_eq!(outcome.stderr, "blocked\n");
}

#[test]
fn cross_check_accepts_the_claimant_suffix_variant() {
    let w = World::new();
    let suffixed = work_worktree_path(w.balls.path(), w.project.path(), "bl-9", Some("n"));
    let r = cross_check_claim(
        &suffixed.display().to_string(),
        w.balls.path(),
        w.project.path(),
        "bl-9",
        "n",
    )
    .unwrap();
    assert!(r.suffixed);
    assert_eq!(r.worktree, suffixed);
    assert!(w.ops().is_empty(), "a matched claim logs no step-failure");
}

#[test]
fn cross_check_drift_is_surfaced_loudly() {
    let w = World::new();
    let err = cross_check_claim(
        "/somewhere/unexpected",
        w.balls.path(),
        w.project.path(),
        "bl-9",
        "n",
    )
    .unwrap_err();
    assert!(matches!(err, StartError::Drift { .. }));
}

/// **The drift row is the caller's, and it is still written** (bl-e59e). The
/// cross-check became pure when the author moved onto the line — only the
/// caller holds the spawn handle that knows which seat asked — so the
/// `["yog-step","cross-check"]` row (Z5) has to be proven on the path that
/// writes it now, which is the executor rather than the judgement.
#[test]
fn a_drifted_claim_leaves_the_step_row_the_caller_is_named_on() {
    let w = World::new();
    let bl = Cli::new(fake_bl(
        w.bin.path(),
        "x",
        std::path::Path::new("/somewhere/else"),
    ));
    let seat = crate::registry::Client::parse("seat1").expect("a leaf name");
    let err = execute_claim(
        &bl.by(seat.clone()),
        w.state.path(),
        "TS",
        w.project.path(),
        "bl-7",
        "cobalt-gecko",
        w.balls.path(),
    )
    .unwrap_err();
    assert!(matches!(err, StartError::Drift { .. }));
    let drift = w
        .ops()
        .into_iter()
        .find(|e| e.argv.first().is_some_and(|a| a == crate::opslog::YOG_STEP))
        .expect("the cross-check row");
    assert_eq!(drift.argv[1], "cross-check");
    assert_eq!(drift.exit, crate::opslog::SYNTHETIC_EXIT);
    assert!(drift.stderr.contains("drift"));
    assert_eq!(drift.client, seat, "and it names who asked for the start");
}