lernie 0.1.63

lernie: the operator seat — the window and wire client for a yog server
//! **Every way a word can fail to be one this binary answers** — a wrong
//! arity, a body that is not a gesture, and a word that is nothing at all.
//!
//! Split from [`super::decisions`] at the design-time budget on the seam that
//! module's own doc drew. Every refusal here is a value a test reads back, and
//! every one of them names what was refused and what could have been typed
//! instead — a bare non-zero exit teaches nobody anything.

use super::super::Stream;
use super::super::verdict::REFUSED;
use super::said;

/// **The enrollment's arity is exact too**, and a wrong one earns the row's own
/// usage rather than the arm's silence — the same grammar every other verb
/// teaches from its mistake.
#[test]
fn a_misspelled_enrollment_earns_the_row_s_usage() {
    let said = said(&["enroll", "home"]);
    assert_eq!(said.code, 2);
    assert!(
        said.text.contains(&crate::verbs::ENROLL.usage()),
        "{}",
        said.text
    );
}

/// **Its arity is exact, for the verb table's own reason**: argv quotes, so a
/// goal is one argument and an unquoted tail refuses rather than being silently
/// joined — which would make three typed words indistinguishable from one
/// quoted sentence.
#[test]
fn the_start_word_refuses_by_arity_and_says_what_it_takes() {
    for words in [vec!["start"], vec!["start", "home"]] {
        let v = said(&words);
        assert_eq!(v.code, REFUSED, "{words:?}");
        assert_eq!(v.stream, Stream::Err);
        // **One space after `usage:`, like every other refusal in the binary**
        // (bl-60e6). The hand-written sentence this used to earn carried
        // fourteen, a wrapping artifact from when it was laid out over two rows
        // in the source — on one of the two words an operator is likeliest to
        // get wrong. It is the door's own computed line now.
        let wanted = format!(
            "`lernie start` takes 2 to 5 argument(s) and got {} — usage: {}",
            words.len() - 1,
            crate::verbs::doors::START.usage()
        );
        assert!(v.text.contains(&wanted), "{}", v.text);
    }
}

/// **An unquoted goal is the tail this arm cannot read, and the sentence leads
/// with the fix.** The composite's grammar grew a written word at PROTOCOL 18
/// (`--role`), so a tail past the goal is read here rather than counted by the
/// door — and what an operator has almost always done is forget the quotes,
/// which is a different mistake from naming a work target wrongly and gets the
/// answer that fixes it.
#[test]
fn an_unquoted_goal_is_told_to_quote_it_and_shown_the_whole_grammar() {
    let v = said(&["start", "home", "do", "the", "thing"]);
    assert_eq!(v.code, REFUSED);
    assert_eq!(v.stream, Stream::Err);
    assert!(v.text.contains("\"the thing\""), "{}", v.text);
    assert!(v.text.contains("ONE argument"), "{}", v.text);
    assert!(v.text.contains("--role"), "{}", v.text);
    assert!(
        v.text.contains(&crate::verbs::doors::START.usage()),
        "{}",
        v.text
    );
}

/// **A verb's own refusal names the verb**, where a word that is no verb can
/// only be quoted back — two different mistakes, and the more specific one gets
/// the more specific answer.
#[test]
fn a_verb_with_the_wrong_arity_refuses_by_name_and_teaches_the_grammar() {
    let v = said(&["transcript", "home"]);
    assert_eq!(v.code, REFUSED);
    assert!(v.text.contains("`lernie transcript`"), "{}", v.text);
    assert!(
        v.text
            .contains("usage: lernie transcript <workspace> <agent>"),
        "{}",
        v.text
    );
}

/// **A body that is not a gesture is the CALLER's typo**, so it is decided
/// here — in the pure function, where the refusal is a value a test reads back
/// — and it earns the usage rather than a connection.
#[test]
fn a_body_that_is_not_a_gesture_is_refused_and_pointed_at_the_page() {
    for (body, said_what) in [
        ("not json at all", "not JSON"),
        ("[1,2]", "a gesture is a JSON object"),
        (r#"{"workspace":"home"}"#, "missing field"),
    ] {
        let v = said(&["ask", body]);
        assert_eq!(v.code, REFUSED, "{body}");
        assert_eq!(v.stream, Stream::Err);
        assert!(v.text.contains(said_what), "{body}: {}", v.text);
        // **`ask` is where this cost the most** (bl-b232): composing an
        // envelope by hand makes the malformed case the common one, and the
        // diagnosis names a byte offset that used to scroll away under 110
        // lines of help.
        assert!(v.text.contains("`lernie help`"), "{body}: {}", v.text);
        assert_eq!(v.text.lines().count(), 2, "{body}: {}", v.text);
    }
}

/// **A door with the wrong number of arguments refuses like a verb does**
/// (bl-6bda): the word is real and the usage lists it, so what it earns is its
/// own grammar rather than the sentence a typo gets.
#[test]
fn a_door_with_the_wrong_arity_refuses_by_name_and_teaches_the_grammar() {
    for (words, expected) in [
        (
            vec!["ask"],
            "`lernie ask` takes 1 argument(s) and got 0 — usage: lernie ask <envelope>",
        ),
        (
            vec!["ask", "{}", "twice"],
            "`lernie ask` takes 1 argument(s) and got 2 — usage: lernie ask <envelope>",
        ),
        (
            vec!["entries", "--now"],
            "`lernie entries` takes 0 argument(s) and got 1 — usage: lernie entries",
        ),
        (
            vec!["help", "a", "b"],
            "`lernie help` takes 0 to 1 argument(s) and got 2 — usage: lernie help [<verb>]",
        ),
    ] {
        let v = said(&words);
        assert_eq!(v.code, REFUSED, "{words:?}");
        assert!(v.text.contains(expected), "{words:?}:\n{}", v.text);
    }
}

#[test]
fn an_unrecognised_argument_refuses_and_quotes_every_word_of_it() {
    let v = said(&["seat", "--ws", "Example"]);
    assert_eq!(v.code, REFUSED);
    assert!(
        v.text.contains("unrecognised argument: seat --ws Example"),
        "{}",
        v.text
    );
}

/// The match is on the WHOLE argument list, not on a first word, so a word that
/// would succeed alone refuses when something rides behind it — rather than
/// silently ignoring the rest.
#[test]
fn a_recognised_word_with_extra_words_is_not_recognised() {
    let extra = ["--version", "--now"];
    let v = said(&extra);
    assert_eq!(v.code, REFUSED);
    assert!(
        v.text
            .contains(&format!("unrecognised argument: {}", extra.join(" "))),
        "{}",
        v.text
    );
}

/// **A mistyped grade is settled here, and costs no connection** (bl-07b9).
/// The set is closed, it is small, and this binary already holds it — the
/// engine's own `unknown grade "OPERATOR"` is true, arrives after a full round
/// trip, and names neither of the two words that would have worked.
#[test]
fn a_grade_that_is_not_one_of_the_two_refuses_before_anything_is_dialled() {
    for typo in ["notagrade", "OPERATOR", ""] {
        let v = said(&["enroll", "home", "phone-1", typo]);
        assert_eq!(v.code, REFUSED, "{typo:?}");
        assert!(
            v.text
                .contains(&format!("unknown grade {typo:?} — `lernie enroll` takes")),
            "{typo:?}: {}",
            v.text
        );
        for word in crate::ui::Grade::both() {
            assert!(
                v.text.contains(&format!("{:?}", word.word())),
                "it names both words that would have worked: {}",
                v.text
            );
        }
    }
}

/// **No sentence this binary composes carries a run of spaces**, which is the
/// class the defect above belongs to: `cargo fmt` joins a `\`-continued string
/// literal and keeps the indentation that was only ever the source's (bl-60e6).
///
/// It reads the composed sentences rather than the usage block, whose columns
/// are aligned on purpose — a run of spaces there is the layout, and everywhere
/// else it is the artifact.
#[test]
fn no_sentence_this_binary_composes_carries_a_run_of_spaces() {
    let mut said: Vec<String> = Vec::new();
    for verb in crate::verbs::table() {
        said.push(verb.usage());
        said.push(crate::verbs::help::page(verb.word).unwrap_or_default());
        if let Err(refusal) = verb.envelope(Vec::new()) {
            said.push(refusal);
        }
    }
    for door in crate::verbs::doors::table() {
        said.push(door.usage());
        said.push(door.refused(9));
        said.push(crate::verbs::help::page(door.word).unwrap_or_default());
    }
    let run: Vec<String> = said
        .iter()
        .flat_map(|text| text.lines())
        .filter(|line| line.contains("  "))
        .map(str::to_owned)
        .collect();
    assert_eq!(run, Vec::<String>::new());
}

/// **A tail that is not the flag names the flag** (bl-9fd1) rather than
/// counting: `lernie stop w a kids` is the right number of words and the wrong
/// one, and an arity sentence about "2 to 3 arguments" answers a question the
/// operator did not ask.
#[test]
fn a_tail_that_is_not_the_flag_is_told_which_word_it_takes() {
    let refused = said(&["stop", "home", "Pelican", "kids"]);
    assert_eq!(refused.code, REFUSED);
    assert!(
        refused.text.contains("no word \"kids\""),
        "{}",
        refused.text
    );
    assert!(refused.text.contains("\"children\""), "{}", refused.text);
}

/// **A depth that is not a number is the caller's typo**, so it is read here
/// rather than spent on a connection — the same division `ask`'s body and
/// `enroll`'s grade are read on (bl-28a4). The wire would refuse it too, in
/// its own words, a round trip later.
#[test]
fn a_trail_depth_that_is_not_a_number_earns_the_door_s_usage() {
    let refusal = said(&["ops", "deep"]);
    assert_eq!(refusal.code, REFUSED);
    assert!(refusal.text.contains(r#"got "deep""#), "{}", refusal.text);
    assert!(
        refusal.text.contains(&crate::verbs::doors::OPS.usage()),
        "{}",
        refusal.text
    );
}

/// **The spelling everybody tries first is the wrong one**, and the arity
/// refusal is where that is paid: `lernie workspaces --json` is what the
/// sibling tools take, and being told `workspaces` takes no argument answers a
/// question the operator did not ask.
#[test]
fn a_trailing_json_flag_earns_the_sentence_that_says_where_it_goes() {
    for words in [
        vec!["workspaces", "--json"],
        vec!["entries", "--json"],
        vec!["conversations", "home", "--json"],
        // …including the one door whose optional argument would otherwise
        // have swallowed it as a depth (bl-28a4).
        vec!["ops", "--json"],
    ] {
        let refusal = said(&words);
        assert_eq!(refusal.code, REFUSED, "{words:?}");
        assert!(
            refusal.text.contains("`--json` goes BEFORE the word"),
            "{words:?}: {}",
            refusal.text
        );
    }
    // …and an ordinary arity mistake earns the plain sentence. (The usage
    // below it says where the flag goes for everybody; what is asserted here
    // is that the DIAGNOSIS line does not, because nothing about this mistake
    // is that flag.)
    let plain = said(&["workspaces", "home"]);
    assert!(
        !plain.text.contains("`--json` goes BEFORE"),
        "{}",
        plain.text
    );
}