quvyta-cli 0.1.0-alpha.5

A small coding agent in the terminal: chat with a model that reads, searches, edits and runs in one folder, and asks before it changes anything (alpha)
Documentation
//! qcli at every size a terminal can be: small enough to hold nothing, a laptop's, a wide
//! screen's. Nothing panics, a card waiting for an answer keeps both its buttons in view whenever
//! the terminal has room for them, and the top bar never shows half a name.

use serde_json::json;

use super::tests::{Place, harness, read_card, say};
use super::*;
use crate::provider::fake::{FakeServer, calls, says};

/// The sizes every state is drawn at.
const SIZES: [(u16, u16); 4] = [(10, 3), (40, 10), (80, 24), (200, 60)];

/// The top bar's row.
fn top(screen: &str) -> &str {
    screen.lines().next().unwrap_or_default()
}

/// Panics if a name of the top bar is on it cut short: each of `names` is there whole or not at
/// all.
fn no_half_names(screen: &str, names: &[&str]) {
    let bar = top(screen);
    for name in names {
        if bar.contains(name) {
            continue;
        }
        let cut = (3..name.chars().count()).map(|n| name.chars().take(n).collect::<String>()).rev();
        for start in cut {
            assert!(!bar.contains(&format!("{start}…")), "`{name}` is cut on the top bar: {bar:?}");
        }
    }
}

/// qcli at `width` × `height` with a card waiting for a command, in the folder `place`.
fn with_a_card(place: &Place, width: u16, height: u16, command: &str) -> Harness<QCli> {
    let server =
        FakeServer::start(vec![(200, calls("r1", "run_command", &json!({"command": command}))), (200, says("ok"))]);
    let mut harness = harness(place.app(Some(&server.address)));
    say(&mut harness, "run it");
    read_card(&mut harness);
    harness.resize(width, height);
    // The server's thread ends with the test; the card is what is looked at.
    std::mem::forget(server);
    harness
}

#[test]
fn every_state_is_drawn_at_every_size_without_a_panic() {
    for (width, height) in SIZES {
        let place = Place::new(&format!("sizes-empty-{width}x{height}"));
        let mut empty = harness(place.app(None));
        empty.resize(width, height);
        let _ = empty.screen();

        let long = format!("printf '{}'", "x".repeat(400));
        let _ = with_a_card(&place, width, height, &long).screen();

        let refused = FakeServer::start(vec![(401, json!({"error": {"message": "no"}}).to_string())]);
        let mut notice = harness(place.app(Some(&refused.address)));
        say(&mut notice, "hi");
        notice.resize(width, height);
        let _ = notice.screen();
    }
}

#[test]
fn a_card_keeps_both_its_buttons_in_view_wherever_they_fit() {
    for (width, height) in SIZES.into_iter().filter(|(width, height)| *width >= 40 && *height >= 10) {
        let place = Place::new(&format!("sizes-card-{width}x{height}"));
        // A command of many lines: the card is taller than a small terminal.
        let long: String = (1..=40).map(|n| format!("echo line {n}\n")).collect();
        let harness = with_a_card(&place, width, height, &long);
        let screen = harness.screen();
        assert!(screen.contains("Allow"), "{width}×{height}: Allow is in view:\n{screen}");
        assert!(screen.contains("Decline"), "{width}×{height}: Decline is in view:\n{screen}");
    }
}

#[test]
fn the_top_bar_never_shows_half_a_name() {
    for (width, height) in SIZES {
        let place = Place::new(&format!("sizes-bar-{width}x{height}"));
        let mut harness = harness(place.app(None));
        harness.resize(width, height);
        let screen = harness.screen();
        no_half_names(&screen, &["qcli", "work"]);
    }
}

/// A folder with a long name at every width: the model goes first and whole, the folder is
/// shortened in its middle so its start and its end stay, and qcli's name is never cut.
#[test]
fn a_narrow_top_bar_gives_up_the_model_first_and_shortens_the_folder_in_its_middle() {
    let long = "a-project-folder-with-a-very-long-name";
    let bar = |width: u16| {
        let place = Place::new(&format!("sizes-long-folder-{width}"));
        let folder = place.work().join(long);
        std::fs::create_dir_all(&folder).expect("the folder");
        let app = QCli::new(
            Workspace::new(&folder).expect("the workspace"),
            place.store(),
            place.settings(Some("http://127.0.0.1:1")),
        );
        let mut harness = harness(app);
        harness.resize(width, 24);
        top(&harness.screen()).trim().to_owned()
    };
    assert_eq!(bar(80), format!("qcli  {long}    test-model"), "all of it fits");
    let forty = bar(40);
    assert!(!forty.contains("test"), "the model goes first, whole: {forty:?}");
    assert!(forty.starts_with("qcli  a-project"), "qcli's name and the folder's start stay: {forty:?}");
    assert!(forty.ends_with("long-name"), "and the folder's end: {forty:?}");
    assert!(forty.contains('…'), "the middle is left out: {forty:?}");
    let fifteen = bar(15);
    assert!(!fifteen.contains("qc"), "qcli's name goes before it would be cut: {fifteen:?}");
    assert!(fifteen.ends_with("name"), "the folder keeps its end: {fifteen:?}");
    no_half_names(&bar(10), &["qcli", "test-model"]);
}

/// The provider form at a small terminal: its fields are there to reach and its buttons to press.
#[test]
fn the_provider_form_can_be_saved_at_a_small_size() {
    for (width, height) in [(40, 10), (80, 24)] {
        let place = Place::new(&format!("sizes-form-{width}x{height}"));
        let mut harness = harness(place.app(None));
        harness.resize(width, height);
        let screen = harness.screen();
        assert!(screen.contains("Save"), "{width}×{height}: Save is in view:\n{screen}");
        assert!(screen.contains("Cancel"), "{width}×{height}: Cancel is in view:\n{screen}");
    }
}