met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Edge-case tests for `lib_keys.rs`: arrow aliases, the filter box, unbound keys and pane-specific letters.
//!
//! Owner: server tui.
//! Invariants: none beyond the types.

use super::*;
use crate::recipe::Recipe;
use crate::tui::data::library::LibraryEntry;

fn key(code: KeyCode) -> KeyEvent {
    KeyEvent::from(code)
}

fn typed(s: &mut LibState, text: &str) {
    for c in text.chars() {
        s.on_key(key(KeyCode::Char(c)));
    }
}

fn recipe() -> Recipe {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/recipes/qwen3.6/qwen3.6-35b-a3b-fp8-mtp.yaml");
    Recipe::parse(
        "qwen3.6/flagship",
        &std::fs::read_to_string(path).expect("fixture"),
    )
    .expect("parses")
}

fn local(id: &str) -> LibraryEntry {
    LibraryEntry {
        id: id.into(),
        snapshot_dir: Default::default(),
        size_bytes: 1024,
        has_weights: true,
        model_type: "qwen3_6_moe".into(),
        quant: "fp8".into(),
        layers: 40,
        hidden: 4096,
        heads: 32,
        experts: 128,
        context: 65536,
        optimized: true,
    }
}

fn state() -> LibState {
    let r = recipe();
    let weights = vec![local(&r.model), local("org/orphan")];
    let mut s = LibState::default();
    s.index = crate::recipe::fetch::Index {
        recipes: vec![r],
        ..Default::default()
    };
    s.rebuild(&weights);
    s
}

#[test]
fn the_arrow_keys_do_what_hjkl_do_in_every_pane() {
    let mut s = state();
    s.on_key(key(KeyCode::Down));
    assert_eq!(s.selected, 1);
    s.on_key(key(KeyCode::Up));
    assert_eq!(s.selected, 0);

    s.on_key(key(KeyCode::Right));
    assert_eq!(s.view, View::Cards, "Right descends like Enter");
    s.on_key(key(KeyCode::Left));
    assert_eq!(s.view, View::List, "Left steps back like Esc");

    s.on_key(key(KeyCode::Char('l')));
    assert_eq!(s.view, View::Cards);
    s.on_key(key(KeyCode::Char('h')));
    assert_eq!(s.view, View::List);
}

#[test]
fn a_key_the_library_does_not_bind_changes_nothing() {
    let mut s = state();
    for code in [
        KeyCode::Tab,
        KeyCode::PageDown,
        KeyCode::Home,
        KeyCode::F(1),
    ] {
        assert_eq!(s.on_key(key(code)), Outcome::None, "{code:?}");
        assert_eq!(s.view, View::List, "{code:?} must not navigate");
        assert_eq!(s.selected, 0, "{code:?} must not move the cursor");
    }
}

#[test]
fn backspace_shortens_the_search_and_enter_keeps_what_was_typed() {
    let mut s = state();
    s.on_key(key(KeyCode::Char('/')));
    typed(&mut s, "orphanx");
    s.on_key(key(KeyCode::Backspace));
    assert_eq!(s.filter, "orphan");

    s.on_key(key(KeyCode::Enter));
    assert!(!s.filter_editing, "the field gives up the keyboard");
    assert_eq!(s.filter, "orphan", "and keeps the filter");
    assert!(!s.is_editing(), "global bindings resume");
    assert_eq!(s.visible().len(), 1);
}

#[test]
fn backspace_on_an_empty_search_is_not_an_error() {
    let mut s = state();
    s.on_key(key(KeyCode::Char('/')));
    s.on_key(key(KeyCode::Backspace));
    assert!(s.filter.is_empty());
    assert!(s.filter_editing, "and the field is still open");
}

#[test]
fn every_search_keystroke_puts_the_cursor_back_on_the_first_match() {
    let mut s = state();
    s.selected = 1;
    s.on_key(key(KeyCode::Char('/')));
    typed(&mut s, "o");
    assert_eq!(s.selected, 0);
    s.on_key(key(KeyCode::Backspace));
    assert_eq!(s.selected, 0, "and on the way back out too");
}

#[test]
fn a_key_the_search_field_does_not_use_leaves_the_filter_alone() {
    let mut s = state();
    s.on_key(key(KeyCode::Char('/')));
    typed(&mut s, "orph");
    s.selected = 0;
    assert_eq!(s.on_key(key(KeyCode::Down)), Outcome::None);
    assert_eq!(s.filter, "orph", "unchanged");
    assert!(s.filter_editing, "and the field keeps the keyboard");
}

#[test]
fn r_does_not_start_a_second_fetch_while_one_is_running() {
    let mut s = state();
    s.fetching = true;
    assert_eq!(s.on_key(key(KeyCode::Char('r'))), Outcome::None);
    assert!(!s.mark_dirty, "and it does not queue a rescan either");
}

#[test]
fn r_asks_for_a_local_rescan_as_well_as_a_fetch() {
    let mut s = state();
    s.on_key(key(KeyCode::Char('r')));
    assert!(s.mark_dirty, "the event loop drains this into a rescan");
}

#[test]
fn d_means_restore_defaults_in_the_form_and_download_everywhere_else() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    assert_eq!(s.view, View::Config);

    match s.on_key(key(KeyCode::Char('d'))) {
        Outcome::Toast { text, error } => {
            assert!(!error);
            assert!(text.contains("restored"), "{text}");
        }
        other => panic!("d in the form must restore defaults, got {other:?}"),
    }
    assert_eq!(s.view, View::Config, "and it does not navigate");
}

#[test]
fn s_launches_only_from_the_form() {
    let mut s = state();
    assert_eq!(s.on_key(key(KeyCode::Char('s'))), Outcome::None);
    s.view = View::Cards;
    assert_eq!(s.on_key(key(KeyCode::Char('s'))), Outcome::None);
    s.view = View::Config;
    assert_eq!(s.on_key(key(KeyCode::Char('s'))), Outcome::Launch);
}

#[test]
fn leaving_the_form_clears_the_error_it_was_showing() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    // 2026-09-26: A free-text row; enumerated rows open a picker that offers only valid values.
    s.row = s
        .config_rows()
        .iter()
        .position(|r| r.key == "port")
        .expect("port");
    s.on_key(key(KeyCode::Enter));
    for _ in 0..40 {
        s.on_key(key(KeyCode::Backspace));
    }
    typed(&mut s, "nonsense");
    s.on_key(key(KeyCode::Enter));
    assert!(s.error.is_some(), "the commit was rejected");

    s.on_key(key(KeyCode::Esc));
    assert_eq!(s.view, View::Cards);
    assert!(s.error.is_none(), "and the complaint goes with the form");
}

#[test]
fn cancelling_an_edit_keeps_an_error_the_previous_commit_earned() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    s.error = Some("earlier complaint".into());
    // 2026-09-26: A free-text row; the first row (`enable_prefix_caching`) is a boolean with a picker.
    s.row = s
        .config_rows()
        .iter()
        .position(|r| r.key == "port")
        .expect("port");
    s.on_key(key(KeyCode::Enter));
    assert!(s.editing);
    typed(&mut s, "x");
    s.on_key(key(KeyCode::Esc));

    assert!(!s.editing);
    assert!(s.edit_buffer.is_empty(), "the buffer is discarded");
    assert_eq!(s.error.as_deref(), Some("earlier complaint"));
}

#[test]
fn a_field_being_edited_owns_the_keyboard() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    assert!(!s.is_editing(), "not until a row is opened");
    // 2026-09-26: A free-text row; the first row (`enable_prefix_caching`) is a boolean with a picker.
    s.row = s
        .config_rows()
        .iter()
        .position(|r| r.key == "port")
        .expect("port");
    s.on_key(key(KeyCode::Enter));
    assert!(s.is_editing(), "global bindings must stand down");

    let before = s.row;
    typed(&mut s, "j");
    assert_eq!(s.row, before, "j is a character, not a move");
    assert!(s.edit_buffer.ends_with('j'));
}

#[test]
fn backspace_on_an_empty_edit_buffer_is_not_an_error() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    s.row = s
        .config_rows()
        .iter()
        .position(|r| r.key == "port")
        .expect("port");
    s.on_key(key(KeyCode::Enter));
    for _ in 0..40 {
        s.on_key(key(KeyCode::Backspace));
    }
    assert!(s.edit_buffer.is_empty());
    assert!(s.editing, "still editing, just empty");
}

#[test]
fn moving_between_form_rows_clamps_at_both_ends() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    s.on_key(key(KeyCode::Enter));
    let rows = s.config_rows().len();
    assert!(rows > 1, "the fixture has several settings");

    s.on_key(key(KeyCode::Char('k')));
    assert_eq!(s.row, 0, "clamped at the first row");
    for _ in 0..rows + 5 {
        s.on_key(key(KeyCode::Char('j')));
    }
    assert_eq!(s.row, rows - 1, "and at the last");
}

#[test]
fn the_card_keys_do_not_move_the_model_selection() {
    let mut s = state();
    s.on_key(key(KeyCode::Enter));
    let selected = s.selected;
    s.on_key(key(KeyCode::Char('j')));
    s.on_key(key(KeyCode::Char('k')));
    assert_eq!(s.selected, selected);
    assert_eq!(s.view, View::Cards);
}

#[test]
fn moving_in_an_empty_card_list_is_a_no_op_rather_than_a_panic() {
    let mut s = LibState::default();
    s.view = View::Cards;
    assert!(s.cards().is_empty());
    s.on_key(key(KeyCode::Char('j')));
    s.on_key(key(KeyCode::Char('k')));
    assert_eq!(s.card, 0);
    match s.on_key(key(KeyCode::Enter)) {
        Outcome::Toast { error, .. } => assert!(error, "it explains rather than opening"),
        other => panic!("expected a toast, got {other:?}"),
    }
}

#[test]
fn moving_in_an_empty_form_is_a_no_op_rather_than_a_panic() {
    let mut s = LibState::default();
    s.view = View::Config;
    assert!(s.config_rows().is_empty());
    s.on_key(key(KeyCode::Char('j')));
    assert_eq!(s.row, 0);
    s.on_key(key(KeyCode::Enter));
    assert!(!s.editing, "there is no row to edit");
}

#[test]
fn enter_on_a_non_metrale_card_explains_instead_of_opening_the_form() {
    let mut r = recipe();
    r.runtime = Some("vllm".into());
    let weights = vec![local(&r.model)];
    let mut s = LibState::default();
    s.index = crate::recipe::fetch::Index {
        recipes: vec![r],
        ..Default::default()
    };
    s.rebuild(&weights);

    s.on_key(key(KeyCode::Enter));
    assert_eq!(s.view, View::Cards, "the card still opens");
    match s.on_key(key(KeyCode::Enter)) {
        Outcome::Toast { text, error } => {
            assert!(error);
            assert!(text.contains("vllm"), "names the runtime: {text}");
        }
        other => panic!("expected a toast, got {other:?}"),
    }
    assert_eq!(s.view, View::Cards, "and it stays put");
}