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
//! What the screen says besides the conversation: the verb of a call as it streams in, the
//! indicator that waits, the mark every tone carries, and the warnings of the provider form.

use std::os::unix::fs::PermissionsExt as _;
use std::time::Duration;

use serde_json::json;

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

#[test]
fn a_call_as_it_streams_in_is_named_in_the_screens_language() {
    let place = Place::new("view-live-verb");
    let mut app = place.app(None);
    app.phase = Phase::Replying;
    app.live = vec![Live::Call("read_file".to_owned())];
    let mut harness = harness(app);
    harness.set_locale("tr");
    let screen = harness.screen();
    let verb = harness.env().i18n().translate("action.read", &[]);
    assert_ne!(verb, "read_file", "Turkish has a word of its own for it");
    assert!(screen.contains(&verb), "the verb in Turkish: {screen}");
    assert!(!screen.contains("read_file"), "not the tool's name: {screen}");
}

#[test]
fn the_waiting_indicator_shows_only_for_a_wait_long_enough_to_notice() {
    let place = Place::new("view-delayed");
    let mut app = place.app(None);
    app.phase = Phase::Replying;
    let mut harness = harness(app);
    assert!(!harness.screen().contains("waiting for the reply"), "not at once: {}", harness.screen());
    harness.advance(Duration::from_millis(400));
    assert!(harness.screen().contains("waiting for the reply"), "after a noticeable wait: {}", harness.screen());
}

#[test]
fn a_refusal_and_a_failed_action_carry_a_mark_and_not_only_a_colour() {
    let place = Place::new("view-marks");
    let server = FakeServer::start(vec![
        (200, calls("c1", "read_file", &json!({"path": "missing.txt"}))),
        (200, says("It is not there.")),
        (401, json!({"error": {"message": "invalid api key"}}).to_string()),
    ]);
    let mut harness = harness(place.app(Some(&server.address)));
    let error = harness.env().icons().glyph("error").into_owned();
    say(&mut harness, "read the missing file");
    assert!(
        harness.screen().contains(&format!("{error} `missing.txt` does not exist.")),
        "the failed result has the error mark: {}",
        harness.screen()
    );
    say(&mut harness, "again");
    assert!(
        harness.screen().contains(&format!("{error} The provider did not answer")),
        "and so has the refusal: {}",
        harness.screen()
    );
}

#[test]
fn the_form_warns_of_a_key_sent_unencrypted_to_another_machine() {
    let place = Place::new("view-plain-http");
    let mut harness = harness(place.app(Some("http://models.example.test")));
    harness.press("ctrl+o");
    assert!(harness.screen().contains("sends the key unencrypted"), "{}", harness.screen());

    // The same over https, and a server on this machine: nothing to warn of.
    for address in ["https://models.example.test", "http://127.0.0.1:8080", "http://localhost:8080"] {
        let place = Place::new("view-plain-http-fine");
        let mut fine = super::tests::harness(place.app(Some(address)));
        fine.press("ctrl+o");
        assert!(!fine.screen().contains("unencrypted"), "{address}: {}", fine.screen());
    }
}

#[test]
fn the_form_warns_of_a_key_file_others_can_read_and_still_saves() {
    let place = Place::new("view-open-key");
    let key = place.base().join("key");
    let mut harness = harness(place.app(Some("https://models.example.test")));
    harness.press("ctrl+o");
    assert!(!harness.screen().contains("Other users"), "a private key file is fine: {}", harness.screen());
    harness.press("esc");

    std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o644)).expect("readable by all");
    harness.press("ctrl+o");
    assert!(harness.screen().contains("Other users of this machine can read"), "{}", harness.screen());
    harness.click_text("Save");
    assert!(harness.app().form.is_none(), "a warning does not keep the form from saving");
}

/// The warning comes with the fix, done here with a click rather than a command to copy.
#[test]
fn the_key_file_is_made_private_from_the_form() {
    let place = Place::new("view-make-private");
    let key = place.base().join("key");
    std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o644)).expect("readable by all");
    let mut harness = harness(place.app(Some("https://models.example.test")));
    harness.press("ctrl+o");
    harness.click_text("Make it private");
    let mode = std::fs::metadata(&key).expect("the key file").permissions().mode() & 0o777;
    assert_eq!(mode, 0o600, "the file is its owner's alone");
    assert!(!harness.screen().contains("Other users"), "and the warning is gone: {}", harness.screen());
    assert!(!harness.screen().contains("Make it private"), "with its button: {}", harness.screen());
}

/// Every saved answer is parsed once and kept while it is on screen, not parsed again on every
/// frame: twelve answers are twelve documents kept, and a new conversation lets them all go.
#[test]
fn the_answers_of_a_long_conversation_are_parsed_once_and_let_go_with_it() {
    let place = Place::new("view-answers");
    let server = FakeServer::start(vec![]);
    let mut app = place.app(Some(&server.address));
    for i in 0..12 {
        app.conversation.say(&format!("question {i}"));
        app.conversation.reply(vec![json!({"type": "text", "text": format!("## Answer {i}\n\n- one\n- two")})]);
    }
    let mut harness = harness(app);
    harness.render().render();
    assert_eq!(harness.app().answers.held(), 12, "every answer drawn is kept");

    harness.press("ctrl+n");
    assert_eq!(harness.app().answers.held(), 0, "a new conversation lets them go");
}

#[test]
fn a_running_commands_last_lines_show_under_its_indicator_and_a_stopped_rounds_do_not() {
    let place = Place::new("view-tail");
    let server = FakeServer::start(vec![]);
    let mut app = place.app(Some(&server.address));
    let action = Action::Run { command: "cargo test".to_owned() };
    let call = crate::conversation::Call { id: "r1".to_owned(), name: "run_command".to_owned(), input: json!({}) };
    app.phase = Phase::Acting { call, action };
    let round = app.round;
    let mut harness = harness(app);
    let lines: String = (1..=8).map(|n| format!("test number {n} ... ok\n")).collect();
    harness.send(Msg::Tail(round, lines));
    let screen = harness.screen();
    assert!(screen.contains("test number 8 ... ok"), "the newest line is shown: {screen}");
    assert!(screen.contains("test number 4 ... ok"), "and the four before it: {screen}");
    assert!(!screen.contains("test number 3 ... ok"), "but no more than five: {screen}");

    harness.send(Msg::Tail(round + 1, "from a stopped round".to_owned()));
    assert!(!harness.screen().contains("from a stopped round"), "a round that is over is not shown");
}