yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! Goal composition + the pre-mint preview (§3.3): the identity lines, the
//! per-rung prefills and driver cwds, [`compose_prepared`], the target-name
//! resolution, and [`preview`].

use crate::binding::{work_worktree_path, workspace_path};
use crate::names::MintError;
use crate::projects::join::JoinState;
use crate::start::goal::{compose_prepared, driver_cwd, mint_name_of, prefill};
use crate::start::{
    BallSpec, Payload, PlanInputs, Target, identity_preamble, identity_preview, parse_ball_stamp,
    preview,
};
use std::path::{Path, PathBuf};

const YOG: &str = "/yog";
const BALLS: &str = "/balls";
const HOME: &str = "/home/op";
const PROJ: &str = "/proj";

fn existing_ball() -> Payload {
    Payload::Ball {
        project: PathBuf::from(PROJ),
        ball: BallSpec::Existing {
            id: "bl-1".to_owned(),
            title: "T".to_owned(),
            body: "B".to_owned(),
            join: JoinState::ReadyStartable,
        },
    }
}
fn new_ball() -> Payload {
    Payload::Ball {
        project: PathBuf::from(PROJ),
        ball: BallSpec::New {
            title: "Fresh".to_owned(),
            body: "do".to_owned(),
        },
    }
}

#[test]
fn identity_lines_are_the_preview_and_the_phase1_stamp() {
    assert_eq!(identity_preview("cobalt-gecko"), "You are cobalt-gecko.");
    assert_eq!(
        identity_preamble("cobalt-gecko"),
        "You are cobalt-gecko. Stamp every bl verb you run with --as cobalt-gecko.",
    );
}

#[test]
fn prefill_is_empty_for_bare() {
    assert_eq!(prefill(&Payload::Bare, None), "");
}

#[test]
fn prefill_names_the_path_verbatim() {
    let g = prefill(
        &Payload::Path {
            dir: PathBuf::from("/work/here"),
        },
        None,
    );
    assert!(g.contains("/work/here"));
    assert!(g.contains("working directory"));
    assert!(!g.contains("You are"));
}

#[test]
fn prefill_is_the_ball_worktree_preamble() {
    let wt = work_worktree_path(Path::new(BALLS), Path::new(PROJ), "bl-1", None);
    let g = prefill(&existing_ball(), Some(&wt));
    assert_eq!(
        g,
        format!(
            "Ball bl-1: T\n\nB\n\nThe project repository checkout for this work is the git worktree at:\n{}   (branch work/bl-1 of {PROJ})\nDo all repository work there, by absolute path. Do not rely on the current directory.",
            wt.display(),
        )
    );
}

#[test]
fn prefill_for_a_new_ball_is_title_and_body_only() {
    // Pre-create the id/worktree are unknown; the worktree preamble joins on the
    // re-plan once `bl create` mints the id (§8.1).
    assert_eq!(prefill(&new_ball(), None), "Ball (new): Fresh\n\ndo");
}

#[test]
fn parse_ball_stamp_is_the_inverse_of_the_composed_header() {
    // The fired goal.md is the identity preamble stamped above the prefill (§3.3);
    // the parse reads the id back off the `Ball <id>:` line regardless of what
    // precedes it. Round-trips the exact compose the executor writes.
    let wt = work_worktree_path(Path::new(BALLS), Path::new(PROJ), "bl-1", None);
    let goal = format!(
        "{}\n\n{}",
        identity_preamble("cobalt-gecko"),
        prefill(&existing_ball(), Some(&wt)),
    );
    assert_eq!(parse_ball_stamp(&goal).as_deref(), Some("bl-1"));
}

#[test]
fn parse_ball_stamp_rejects_non_ball_goals_and_malformed_headers() {
    // A bare/path conversation has no header.
    assert_eq!(parse_ball_stamp("You are x.\n\nfree prose"), None);
    // The word "Ball" opening a prose line is not a header: an empty id and a
    // multi-word id are both rejected (a real id is one token).
    assert_eq!(parse_ball_stamp("Ball : no id"), None);
    assert_eq!(parse_ball_stamp("Ball two words: nope"), None);
    // A header buried after the preamble still parses (line-wise scan).
    assert_eq!(
        parse_ball_stamp("You are y.\n\nBall bz-9: T\n\nbody").as_deref(),
        Some("bz-9"),
    );
}

#[test]
fn driver_cwd_is_the_per_rung_directory() {
    let home = Path::new(HOME);
    let wt = work_worktree_path(Path::new(BALLS), Path::new(PROJ), "bl-1", None);
    assert_eq!(driver_cwd(&Payload::Bare, home, None), PathBuf::from(HOME));
    assert_eq!(
        driver_cwd(
            &Payload::Path {
                dir: PathBuf::from("/d")
            },
            home,
            None,
        ),
        PathBuf::from("/d"),
    );
    assert_eq!(driver_cwd(&existing_ball(), home, Some(&wt)), wt);
    // A not-yet-created ball has no worktree, so it falls back to `~`.
    assert_eq!(driver_cwd(&new_ball(), home, None), PathBuf::from(HOME));
}

#[test]
fn compose_prepared_derives_the_fire_params() {
    let inputs = PlanInputs {
        name: "cobalt-gecko".to_owned(),
        workspace: workspace_path(Path::new(YOG), "cobalt-gecko"),
        payload: Payload::Bare,
        home: PathBuf::from(HOME),
        yog_data_root: PathBuf::from(YOG),
        balls_state_root: PathBuf::from(BALLS),
    };
    let p = compose_prepared(&inputs, None);
    assert_eq!(p.name, "cobalt-gecko");
    assert_eq!(p.workspace, workspace_path(Path::new(YOG), "cobalt-gecko"));
    assert_eq!(p.cwd, PathBuf::from(HOME));
    assert_eq!(p.goal, "");
}

#[test]
fn mint_name_of_returns_existing_verbatim() {
    let name = mint_name_of(
        &Target::Existing {
            workspace: PathBuf::from("/lernie/workspaces/alpha"),
        },
        Path::new(YOG),
        &[],
        &mut super::rng(),
    )
    .unwrap();
    assert_eq!(
        name, "alpha",
        "the name is the focused workspace's path leaf"
    );
}

#[test]
fn mint_name_of_mints_a_hyphenated_pair() {
    // An empty world (no names root, no claimants) → a fresh two-word name.
    let name = mint_name_of(&Target::Mint, Path::new(YOG), &[], &mut super::rng()).unwrap();
    let (a, b) = name.split_once('-').expect("two words hyphenated");
    assert!(!a.is_empty() && !b.is_empty());
    assert!(!matches!(
        mint_name_of(&Target::Mint, Path::new(YOG), &[], &mut super::rng()),
        Err(MintError::Exhausted(_))
    ));
}

#[test]
fn preview_pairs_the_identity_line_with_the_prefill() {
    let w = super::World::new();
    let inputs = w.inputs(
        Target::Existing {
            workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
        },
        Payload::Path {
            dir: PathBuf::from("/d"),
        },
    );
    let c = preview(&inputs, &mut super::rng());
    assert_eq!(c.preview, "You are cobalt-gecko.");
    assert!(c.prefill.contains("/d"));
}

#[test]
fn preview_predicts_a_minted_name() {
    let w = super::World::new();
    let inputs = w.inputs(Target::Mint, Payload::Bare);
    let c = preview(&inputs, &mut super::rng());
    assert!(c.preview.starts_with("You are "));
    assert!(c.preview.ends_with('.'));
    assert_eq!(c.prefill, "", "bare has no prefill");
}