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 as a member of the ecosystem: the language, the theme and the reduced motion of
//! `quvyta.conf` are read before the first frame and followed while qcli runs, the switch of the
//! ecosystem still stops the update question, and the settings of qcli itself are still the
//! application's own business.
//!
//! Every test starts the way a person's first start is: from the runtime `run()` starts, with
//! qcli's own language files and keys, in the folder of the test's own [`Place`], and every change
//! is a real file written the way another Quvyta application writes it. A member start reads the
//! files again only when the harness is asked to, which is what the runtime's watch does when it
//! hears the folder change; nothing here restarts the application.

use qframe::runtime::Harness;
use qframe::storage::{Ecosystem, Scope, Shared};

use super::QCli;
use super::tests::{Place, say};
use crate::config;
use crate::provider::fake::{FakeServer, says};

/// The ecosystem qcli is a member of.
const QUVYTA: Ecosystem = Ecosystem::QUVYTA;

/// The shared file, with `text` in it, as qcli's folder of settings.
fn shared(place: &Place, text: &str) {
    let folder = place.base.join("config");
    std::fs::write(folder.join("quvyta.conf"), text).expect("the ecosystem's shared file");
}

/// qcli in its own folder, opened from the runtime `run()` starts, at the size the other chat
/// tests use.
fn member(place: &Place, app: QCli) -> Harness<QCli> {
    let folder = place.base.join("config");
    super::tests::opened(app, &folder)
}

/// What another Quvyta application writes into the shared file, for every member of the
/// ecosystem.
fn another_app_writes(place: &Place, key: Shared, value: &str) {
    let folder = place.base.join("config");
    QUVYTA.set_in(&folder, "qexp", key, value, Scope::Ecosystem).expect("the shared file is written");
}

#[test]
fn the_language_in_the_shared_file_is_used_from_the_first_frame() {
    let place = Place::new("member-language");
    shared(&place, "language = \"tr\"\n");
    let harness = member(&place, place.app(None));

    assert_eq!(harness.env().i18n().active(), "tr");
    assert!(harness.screen().contains("çık"), "the first frame is already Turkish:\n{}", harness.screen());
}

#[test]
fn a_language_changed_by_another_application_is_followed_while_qcli_runs() {
    let place = Place::new("member-follow-language");
    shared(&place, "language = \"en\"\n");
    let mut harness = member(&place, place.app(None));
    assert!(harness.screen().contains("quit"), "it starts in English:\n{}", harness.screen());

    // Another Quvyta application switches the language for everybody.
    another_app_writes(&place, Shared::Language, "tr");
    harness.poll_preferences();

    assert_eq!(harness.env().i18n().active(), "tr");
    assert!(harness.screen().contains("çık"), "the running screen is Turkish without a restart:\n{}", harness.screen());
}

#[test]
fn a_theme_changed_by_another_application_is_followed_while_qcli_runs() {
    let place = Place::new("member-follow-theme");
    shared(&place, "language = \"en\"\ntheme = \"nordic\"\n");
    let mut harness = member(&place, place.app(None));
    // The name in the corner is drawn from the language file that is active, so the test asks the
    // environment which word that is instead of naming one: a member start carries the framework's
    // built-in files only, and what is looked at here is the colour of that cell.
    let name = harness.env().i18n().translate("app.name", &[]);
    let (x, y) = harness.find(&name).expect("the name in the header");
    let (x, y) = (x as u16, y as u16);
    let before = harness.fg(x, y).expect("the name is drawn in the theme's accent");

    another_app_writes(&place, Shared::Theme, "amber");
    harness.poll_preferences();

    assert_eq!(harness.env().theme().id(), "amber");
    let after = harness.fg(x, y).expect("the name is still drawn in the theme's accent");
    assert_ne!(before, after, "the accent of the name follows the theme the shared file names");
}

#[test]
fn reduced_motion_from_the_shared_file_is_followed_the_same_way() {
    let place = Place::new("member-follow-motion");
    shared(&place, "language = \"en\"\n");
    let mut harness = member(&place, place.app(None));
    assert!(!harness.env().reduced_motion(), "motion is what the shared file does not name");

    another_app_writes(&place, Shared::ReducedMotion, "true");
    harness.poll_preferences();
    assert!(harness.env().reduced_motion(), "still, without a restart");

    another_app_writes(&place, Shared::ReducedMotion, "false");
    harness.poll_preferences();
    assert!(!harness.env().reduced_motion(), "and back, the file is read again");
}

#[test]
fn the_ecosystems_update_switch_still_stops_the_question() {
    let asked = Place::new("member-update-asked");
    let harness = member(&asked, asked.app(None).update_notice(Some(folders(&asked))));
    assert_eq!(harness.update_checks().len(), 1, "a member start asks for a newer version as before");

    let quiet = Place::new("member-update-quiet");
    shared(&quiet, "update-notice = false\n");
    let harness = member(&quiet, quiet.app(None).update_notice(Some(folders(&quiet))));
    assert!(harness.update_checks().is_empty(), "the switch in the shared file stops the question");
}

/// Where the update notice looks under a test's own place, never the person's.
fn folders(place: &Place) -> config::UpdateFolders {
    config::UpdateFolders { config: place.base.join("config"), state: place.base.join("state") }
}

#[test]
fn the_apps_own_settings_are_still_read_under_a_member_start() {
    let place = Place::new("member-own-settings");
    let server = FakeServer::start(vec![(200, says("Hello from the stand-in."))]);
    let key = "not-a-real-key-0000-member";
    let key_file = place.base.join("member-key");
    std::fs::write(&key_file, format!("{key}\n")).expect("the key file");
    std::fs::write(
        place.base.join("config/cli.conf"),
        format!(
            "address = \"{}\"\nmodel = \"own-model\"\nkey-file = \"{}\"\nkey-header = \"api-key\"\n",
            server.address,
            key_file.display()
        ),
    )
    .expect("qcli's own settings");
    let mut harness = member(&place, place.app(None));
    assert!(
        harness.screen().contains("own-model"),
        "the header names the model of the settings:\n{}",
        harness.screen()
    );

    say(&mut harness, "hello there");
    assert!(harness.screen().contains("Hello from the stand-in."), "the stand-in answered:\n{}", harness.screen());
    let request = &server.received()[0];
    assert_eq!(request.body["model"], "own-model", "the model comes from qcli's own file");
    assert_eq!(request.header("api-key"), Some(key), "and so does the key it names");
}