yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
use super::*;
use crate::git_tree::{Agent, AgentState};
use crate::projects::join::JoinState;

fn branch(name: &str, state: AgentState) -> Agent {
    Agent {
        branch_name: format!("agents/{name}"),
        agent_id: name.to_string(),
        tip_oid: "0".repeat(40),
        tip_short_oid: "0000000".to_string(),
        tip_timestamp_unix: 0,
        steps: vec![],
        preview: None,
        streaming_text: None,
        tool_calls: vec![],
        state,
        state_uncertain: false,
        pending_messages: 0,
        conflicted_oid: None,
        budget_oid: None,
        abandoned_oid: None,
        notify_oid: None,
        goal_ball: None,
    }
}

#[test]
fn new_prompt_enabled_when_text_present() {
    assert!(new_prompt_enabled("hi"));
    assert!(new_prompt_enabled("  surrounded by spaces  "));
}

#[test]
fn new_prompt_disabled_for_empty_input() {
    assert!(!new_prompt_enabled(""));
}

#[test]
fn new_prompt_disabled_for_whitespace_only() {
    assert!(!new_prompt_enabled("   \t\n"));
}

#[test]
fn create_ball_enabled_requires_a_nonblank_title() {
    assert!(create_ball_enabled("Fix the bug"));
    assert!(!create_ball_enabled(""));
    assert!(!create_ball_enabled("  \t "));
}

#[test]
fn draft_clears_only_on_a_clean_send() {
    use crate::actions::verbs::Outcome;
    let clean = Ok(Outcome {
        exit: 0,
        stdout: String::new(),
        stderr: String::new(),
    });
    let ran_nonzero = Ok(Outcome {
        exit: 3,
        stdout: String::new(),
        stderr: "boom".into(),
    });
    let never_launched: std::io::Result<Outcome> = Err(std::io::Error::other("no binary"));
    assert!(draft_clears(&clean), "a clean send clears the draft");
    assert!(
        !draft_clears(&ran_nonzero),
        "a ran-but-failed verb keeps the draft"
    );
    assert!(
        !draft_clears(&never_launched),
        "a spawn failure keeps the draft"
    );
}

#[test]
fn stop_disabled_when_selection_is_none() {
    let bs = vec![branch("foo", AgentState::InFlight)];
    assert!(!stop_enabled(None, &bs));
}

#[test]
fn stop_disabled_when_selection_not_in_agents() {
    let bs = vec![branch("foo", AgentState::InFlight)];
    assert!(!stop_enabled(Some("bar"), &bs));
}

#[test]
fn stop_disabled_when_selected_agent_stopped() {
    let bs = vec![branch("foo", AgentState::Stopped)];
    assert!(!stop_enabled(Some("foo"), &bs));
}

#[test]
fn stop_disabled_when_selected_agent_quiescent() {
    // A finished-for-now agent has no executor to signal (§2.9).
    let bs = vec![branch("foo", AgentState::Quiescent)];
    assert!(!stop_enabled(Some("foo"), &bs));
}

#[test]
fn stop_enabled_when_selection_is_in_flight() {
    let bs = vec![branch("foo", AgentState::InFlight)];
    assert!(stop_enabled(Some("foo"), &bs));
}

#[test]
fn stop_enabled_when_selection_is_live() {
    // A driver between model calls (running a tool) is stoppable — the
    // very case §2.9's lock-fd discovery exists for.
    let bs = vec![branch("foo", AgentState::Live)];
    assert!(stop_enabled(Some("foo"), &bs));
}

#[test]
fn stop_disabled_when_agents_empty() {
    let bs: Vec<Agent> = vec![];
    assert!(!stop_enabled(Some("foo"), &bs));
}

#[test]
fn stop_picks_correct_agent_among_several() {
    let bs = vec![
        branch("a", AgentState::Stopped),
        branch("b", AgentState::InFlight),
        branch("c", AgentState::Quiescent),
        branch("d", AgentState::Live),
    ];
    assert!(stop_enabled(Some("b"), &bs));
    assert!(stop_enabled(Some("d"), &bs));
    assert!(!stop_enabled(Some("a"), &bs));
    assert!(!stop_enabled(Some("c"), &bs));
}

#[test]
fn message_enabled_only_for_a_present_selection_with_text() {
    let bs = vec![branch("foo", AgentState::Stopped)];
    // Any state — Message is the resume gesture (contrast stop_enabled).
    assert!(message_enabled(Some("foo"), "continue please", &bs));
    assert!(!message_enabled(Some("foo"), "   ", &bs), "blank text");
    assert!(!message_enabled(None, "hi", &bs), "no selection");
    assert!(!message_enabled(Some("bar"), "hi", &bs), "absent id");
}

#[test]
fn stop_children_offered_only_with_a_descendant() {
    let bs = vec![
        branch("root-x", AgentState::Live),
        branch("root-x-c1", AgentState::Stopped),
        branch("root-y", AgentState::Live),
    ];
    assert!(stop_children_offered("root-x", &bs), "has a child");
    assert!(!stop_children_offered("root-y", &bs), "leaf agent");
    // A hyphen-boundary miss: root-xx is not a descendant of root-x.
    let bs2 = vec![
        branch("root-x", AgentState::Live),
        branch("root-xx", AgentState::Live),
    ];
    assert!(!stop_children_offered("root-x", &bs2));
}

#[test]
fn close_enabled_only_for_bound() {
    assert!(close_enabled(JoinState::Bound));
    assert!(!close_enabled(JoinState::ClaimedElsewhere));
    assert!(!close_enabled(JoinState::ReadyStartable));
    assert!(!close_enabled(JoinState::Delivered));
}

#[test]
fn unclaim_enabled_only_for_bound() {
    assert!(unclaim_enabled(JoinState::Bound));
    assert!(!unclaim_enabled(JoinState::ClaimedElsewhere));
    assert!(!unclaim_enabled(JoinState::ReadyStartable));
    assert!(!unclaim_enabled(JoinState::UnassignedWorkspace));
}

#[test]
fn assign_enabled_only_for_a_ready_ball() {
    // Assign binds an unbound ball → only ReadyStartable (what `bl claim` allows).
    assert!(assign_enabled(JoinState::ReadyStartable));
    assert!(!assign_enabled(JoinState::Bound), "already bound");
    assert!(!assign_enabled(JoinState::Blocked));
    assert!(!assign_enabled(JoinState::ClaimedElsewhere));
    assert!(!assign_enabled(JoinState::Delivered));
    assert!(!assign_enabled(JoinState::UnassignedWorkspace));
    assert!(!assign_enabled(JoinState::OrphanedProject));
}

#[test]
fn move_enabled_only_for_bound() {
    // Move = release + assign elsewhere → only a ball this yog owns (Bound).
    assert!(move_enabled(JoinState::Bound));
    assert!(!move_enabled(JoinState::ReadyStartable));
    assert!(!move_enabled(JoinState::ClaimedElsewhere));
    assert!(!move_enabled(JoinState::Delivered));
}

#[test]
fn actions_state_default_is_empty() {
    let s = ActionsState::default();
    assert!(s.new_prompt_input.is_empty());
    assert!(s.path_dir.is_empty());
    assert!(s.selected_branch.is_none());
    assert!(!s.stop_children);
    assert!(s.last_failure.is_none());
}

#[test]
fn actions_state_clone_eq() {
    let s = ActionsState {
        new_prompt_input: "hi".to_string(),
        selected_branch: Some("b".to_string()),
        stop_children: true,
        ..Default::default()
    };
    let s2 = s.clone();
    assert_eq!(s, s2);
}