lernie 0.1.63

lernie: the operator seat — the window and wire client for a yog server
//! The conversations under a wall: the two empty states, the headline, the
//! age, the indent, and the click that selects.

/// What hangs under a conversation, on the glass.
mod subtree;
/// The rule, the tint and the connectors a threaded row wears.
mod threading;
/// What the pane's own width does to the panels beside it.
mod width;

use super::{NO_CONVERSATIONS, NOT_ANSWERED, age, headline};
use crate::paint_probe::frame::Window;
use crate::reply::convs::{AgentState, Tone};
use crate::test_support::window::{click, conv, pane, seated, seen};
use crate::ui::Model;

/// **The rows as the roster paints them** — [`super::under`] against the
/// fixture's own aim, which is what `crate::ui::roster::wall` hands it.
pub(super) fn beneath(ui: &mut egui::Ui, model: &mut Model) {
    let aim = model.aim.clone().expect("the fixture is aimed at a wall");
    super::under(ui, model, &aim, false);
}

/// **Two empty states and they are two different facts** (bl-f780): a wall
/// nobody has been answered about yet, and a wall that answered nothing.
///
/// The second used to be said about both, which states a definite fact about a
/// wall nobody looked at — the same thing `super::UNCERTAIN` refuses to do one
/// level down. **The other two went with the pane** (DESIGN §4.39): *pick a
/// workspace* had no subject to name once the rows stand under their own
/// wall's row, and an aim naming a channel this seat does not hold has no row
/// to stand under at all.
#[test]
fn the_empty_states_say_two_different_things() {
    let mut waiting = seated();
    waiting.convs.clear();
    waiting.answered = None;
    let painted = pane(|ui| beneath(ui, &mut waiting));
    assert!(painted.contains(NOT_ANSWERED), "{painted}");
    assert!(!painted.contains(NO_CONVERSATIONS), "{painted}");

    let mut empty = seated();
    empty.convs.clear();
    empty.answered = empty.aim.clone();
    let painted = pane(|ui| beneath(ui, &mut empty));
    assert!(painted.contains(NO_CONVERSATIONS), "{painted}");
}

/// **Aiming somewhere else retires the ANSWER with the rows.** The transient
/// face of the same defect: between the keypress and the reply the list stood
/// under the new wall's row and said that wall held nothing.
#[test]
fn aiming_elsewhere_leaves_the_pane_waiting_rather_than_reporting_nothing() {
    let mut model = seated();
    assert_eq!(model.answered, None, "seated() has not been answered");
    model.absorb(
        &model.roster[0].channel.clone(),
        crate::reply::Read::Answer(crate::reply::Reply::Conversations(vec![conv("id", "one")])),
    );
    assert_eq!(model.answered, model.aim, "the answer is about the aim");
    model.aim_at("(this box's own engine)", "elsewhere");
    assert_eq!(model.answered, None, "and it is retired with the rows");
    let painted = pane(|ui| beneath(ui, &mut model));
    assert!(painted.contains(NOT_ANSWERED), "{painted}");
}

/// A headline is a glance: the label, what it is doing, how long since it moved
/// — and the two counts only where there is something to count.
#[test]
fn a_headline_says_the_state_the_age_and_only_the_counts_that_matter() {
    let quiet = headline(&conv("id", "port the probe"));
    assert_eq!(quiet, "port the probe  [quiescent]  42s");
    let busy = headline(&crate::reply::convs::ConvRow {
        state: AgentState::Live,
        attention: 2,
        members: 4,
        age_secs: 7200,
        ..conv("id", "port the probe")
    });
    assert!(quiet.len() < busy.len());
    assert!(busy.contains("[live]"), "{busy}");
    assert!(busy.contains("2h"), "{busy}");
    assert!(busy.contains("2 waiting"), "{busy}");
    assert!(busy.contains("4 members"), "{busy}");
}

/// **An age in the future is not a thing to paint.** Two machines' clocks
/// disagreeing is a fact about a seat that dials somewhere else, so a negative
/// age clamps rather than refusing or showing a minus sign.
#[test]
fn an_age_is_compact_and_never_negative() {
    for (secs, said) in [
        (-3, "0s"),
        (0, "0s"),
        (42, "42s"),
        (59, "59s"),
        (60, "1m"),
        (3599, "59m"),
        (3600, "1h"),
        (86_399, "23h"),
        (86_400, "1d"),
        (172_800, "2d"),
    ] {
        assert_eq!(age(secs), said, "{secs}");
    }
}

/// **A member hangs under its root, and its preview is a line after the
/// first** — so it is weak ink (`docs/STYLE.md` §5), and it starts exactly
/// where the row's own words do rather than at a second left edge.
///
/// **The fact this test used to hold moved with the restyle** (bl-d1ae): the
/// preview carried the row's TONE, which put a second full-strength colour on
/// a row whose one coloured thing is meant to be its rule. The tone is still
/// the ink of the headline above it and of the failure clause beside it — both
/// asserted below — and the preview is now the weak ink every line after the
/// first is painted in.
#[test]
fn a_member_is_indented_and_its_preview_is_weak_ink_at_the_row_s_own_edge() {
    let mut model = seated();
    model.convs = vec![crate::reply::convs::ConvRow {
        depth: 2,
        tone: Tone::Weak,
        preview: "the galley lies about elision".to_owned(),
        ..conv("child", "a member")
    }];
    let window = Window::sized(900.0, 600.0);
    let runs = seen(&window, |ctx| {
        egui::CentralPanel::default().show(ctx, |ui| beneath(ui, &mut model));
    });
    let at = |needle: &str| {
        runs.iter()
            .find(|run| run.text.contains(needle))
            .unwrap_or_else(|| panic!("{needle:?} is not on the glass"))
            .clone()
    };
    let words = at("a member");
    let preview = at("the galley lies about elision");
    assert_eq!(preview.ink, crate::ui::theme::INK_WEAK);
    assert!(
        (preview.laid.min.x - words.laid.min.x).abs() < 0.5,
        "the preview starts under the words: {:?} vs {:?}",
        preview.laid,
        words.laid
    );
    assert!(
        words.laid.min.y < preview.laid.min.y,
        "and under them, not beside"
    );
}

/// **A red row says what is wrong with it** (REMOTE §9.10). A conversation
/// whose latest model call failed used to paint a `bad` tone and no words, so
/// a wall whose provider row holds no credential was a list of red rows an
/// operator opened one by one to learn the one thing all of them said.
///
/// Three assertions, and the ordering is the one that would rot silently: the
/// clause is on the glass, it is painted in the row's own tone rather than in
/// an ink this pane decided for it, and it stands ABOVE the preview — the row
/// reads label, then why nothing more happened, then what was last said.
#[test]
fn a_failed_row_paints_its_clause_above_its_preview_and_in_its_own_tone() {
    let mut model = seated();
    model.convs = vec![crate::reply::convs::ConvRow {
        tone: Tone::Bad,
        failure: "no credential for provider row \"work\"".to_owned().into(),
        preview: "the last thing it managed to say".to_owned(),
        ..conv("id", "a refusing conversation")
    }];
    let window = Window::sized(900.0, 600.0);
    let runs = seen(&window, |ctx| {
        egui::CentralPanel::default().show(ctx, |ui| beneath(ui, &mut model));
    });
    let at = |needle: &str| {
        runs.iter()
            .find(|run| run.text.contains(needle))
            .unwrap_or_else(|| panic!("{needle:?} is not on the glass"))
            .clone()
    };
    let clause = at("no credential for provider row");
    assert_eq!(clause.ink, crate::ui::theme::tone_ink(&Tone::Bad));
    assert!(
        clause.laid.min.y < at("the last thing it managed to say").laid.min.y,
        "the clause stands above the preview: {:?}",
        clause.laid
    );
}

/// **Selecting a conversation drops the last one's transcript**, for the reason
/// the roster drops the last wall's list: content under a new header reads as
/// current, and a transcript is the one place that lies worst.
#[test]
fn a_click_selects_the_conversation_and_drops_the_last_one_s_transcript() {
    let mut model = seated();
    model.conversation = None;
    let window = Window::new();
    click(&window, &headline(&model.convs[0].clone()), |ctx| {
        egui::CentralPanel::default().show(ctx, |ui| beneath(ui, &mut model));
    });
    assert_eq!(model.conversation.as_deref(), Some("20260830T051200Z-a1b2"));
    assert!(model.transcript.entries.is_empty());
    assert_eq!(model.live, None);
}

/// **A state nothing observed wears a `?`**, inside the badge it qualifies. The
/// engine answers a reading and whether it could take one, and a badge that
/// painted only the first would state a definite fact about a conversation
/// nobody looked at.
#[test]
fn an_unobserved_state_is_marked_rather_than_painted_as_definite() {
    let unsure = headline(&crate::reply::convs::ConvRow {
        uncertain: true,
        ..conv("id", "port the probe")
    });
    assert_eq!(unsure, "port the probe  [quiescent?]  42s");
}

/// **The conversation this window just started stands in the list**, before the
/// engine has anything to answer about it: the minted name, the operator's own
/// goal beneath it, and a badge that says nothing observed any of it.
#[test]
fn a_started_conversation_stands_in_the_list_before_the_engine_can_answer() {
    let mut model = Model {
        conversation: Some("brisk-otter".to_owned()),
        start: Some(crate::ui::model::Start {
            address: "home".to_owned(),
            goal: "port the paint probe".to_owned(),
            phase: crate::ui::model::Phase::Started("brisk-otter".to_owned()),
            spread: None,
        }),
        ..seated()
    };
    let painted = pane(|ui| beneath(ui, &mut model));
    assert!(painted.contains("brisk-otter  [quiescent?]"), "{painted}");
    assert!(painted.contains("port the paint probe"), "{painted}");
    assert!(
        !painted.contains("[live]"),
        "no driver this seat has seen: {painted}"
    );
}