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: Tests of the Ops REPL: completion, dispatch, the output each
//! command appends, and the Ops input keys. `/gpu` is not run here: it queries
//! the device.
//!
//! Owner: server tui.
//! Invariants: none beyond the types.

use super::*;

fn app() -> App {
    App::new(clap::Parser::parse_from(["met", "org/m"]))
}

/// 2026-09-26: Everything the pane printed, as one blob to search.
fn out(a: &App) -> String {
    a.ops.output.join("\n")
}

#[test]
fn completion_only_fires_on_an_unambiguous_prefix() {
    assert_eq!(complete("/q"), Some("/quit"));
    assert_eq!(complete("/de"), Some("/detach"));
    assert_eq!(complete("/"), Some("/help"), "the first, not nothing");
    assert_eq!(complete("/zzz"), None, "no such command");
    assert_eq!(complete(""), None);
    assert_eq!(complete("/status"), None, "already complete");
}

#[test]
fn completion_stops_once_an_argument_is_being_typed() {
    assert_eq!(complete("/metrics "), None);
    assert_eq!(complete("/metrics dec"), None);
}

#[test]
fn completion_offers_the_bare_name_of_a_command_that_takes_an_argument() {
    assert_eq!(complete("/met"), Some("/metrics"));
    assert_eq!(complete("/ker"), Some("/kernels"));
}

#[test]
fn every_command_echoes_the_line_it_ran() {
    let mut a = app();
    execute("/cache", &mut a);
    assert_eq!(a.ops.output[0], "❯ /cache");
}

#[test]
fn a_line_is_trimmed_before_it_is_dispatched() {
    let mut a = app();
    execute("  /help  ", &mut a);
    assert_eq!(a.ops.output[0], "❯ /help");
    assert!(out(&a).contains("/quit"), "and it really ran");
}

#[test]
fn help_lists_every_command_it_can_run() {
    // 2026-09-26: `/help` prints the `COMMANDS` table. The dispatcher is a
    // separate `match`, so this checks the table side only.
    let mut a = app();
    execute("/help", &mut a);
    let printed = out(&a);
    for (name, description) in COMMANDS {
        assert!(printed.contains(name), "{name} missing from /help");
        assert!(printed.contains(description), "{description} missing");
    }
}

#[test]
fn bare_text_is_pointed_at_the_chat_tab_rather_than_guessed_at() {
    let mut a = app();
    execute("what is the capital of France", &mut a);
    assert!(out(&a).contains("Chat tab"), "got {:?}", a.ops.output);
    assert!(!a.should_quit);
}

#[test]
fn an_unknown_command_names_itself_and_points_at_help() {
    let mut a = app();
    execute("/nope", &mut a);
    let printed = out(&a);
    assert!(printed.contains("/nope"), "got {printed:?}");
    assert!(printed.contains("/help"));
}

#[test]
fn the_watchdog_needs_on_or_off_and_says_so() {
    for line in ["/watchdog", "/watchdog maybe", "/watchdog ON"] {
        let mut a = app();
        execute(line, &mut a);
        assert!(
            out(&a).contains("usage: /watchdog on|off"),
            "{line} should have been refused"
        );
    }
}

#[test]
fn status_says_so_when_the_scheduler_has_not_published_yet() {
    // 2026-09-26: The request counters print even with no run attached.
    let mut a = app();
    execute("/status", &mut a);
    let printed = out(&a);
    assert!(printed.contains("snapshot not yet published"));
    assert!(printed.contains("requests:"), "the counters still print");
}

#[test]
fn a_filter_that_matches_nothing_says_nothing_matched() {
    for (line, expected) in [
        ("/metrics zzzz-no-such-metric", "(no metrics matched)"),
        (
            "/kernels zzzz-no-such-kernel",
            "(no kernel lookups matched)",
        ),
    ] {
        let mut a = app();
        execute(line, &mut a);
        assert!(out(&a).contains(expected), "{line} → {:?}", a.ops.output);
    }
}

#[test]
fn the_cache_command_reports_even_before_anything_is_cached() {
    let mut a = app();
    execute("/cache", &mut a);
    assert!(out(&a).contains("prefix cache:"));
}

#[test]
fn detach_leaves_the_tui_without_shutting_the_server_down() {
    let mut a = app();
    execute("/detach", &mut a);
    assert!(a.detach);
    assert!(!a.should_quit, "the process stays up");
}

#[test]
fn quit_asks_for_a_clean_shutdown() {
    // 2026-09-26: `/quit` calls `shutdown::request`, which sets a process-wide
    // flag, so this test leaves shutdown requested for the rest of the test
    // binary.
    let mut a = app();
    execute("/quit", &mut a);
    assert!(a.should_quit);
    assert!(!a.detach);
}

fn focused_ops(a: &mut App) {
    a.on_key(crossterm::event::KeyEvent::from(
        crossterm::event::KeyCode::Char('6'),
    ));
    a.on_key(crossterm::event::KeyEvent::from(
        crossterm::event::KeyCode::Char('i'),
    ));
}

fn tap(a: &mut App, code: crossterm::event::KeyCode) {
    a.on_key(crossterm::event::KeyEvent::from(code));
}

fn type_line(a: &mut App, s: &str) {
    for c in s.chars() {
        tap(a, crossterm::event::KeyCode::Char(c));
    }
}

/// 2026-09-26: In the Ops input, Tab accepts the ghost completion and is
/// otherwise inert; it never reaches the section switch.
#[test]
fn tab_accepts_the_ghost_completion_and_never_leaves_the_section() {
    let mut a = app();
    focused_ops(&mut a);
    type_line(&mut a, "/de");
    tap(&mut a, crossterm::event::KeyCode::Tab);
    assert_eq!(a.ops.input, "/detach", "the ghost became the line");
    assert_eq!(a.section, crate::tui::app::Section::Terminal);

    type_line(&mut a, "zzz");
    let before = a.ops.input.clone();
    tap(&mut a, crossterm::event::KeyCode::Tab);
    assert_eq!(a.ops.input, before, "nothing to complete, nothing changed");
    assert_eq!(a.section, crate::tui::app::Section::Terminal, "still here");
    assert!(a.term_sub == crate::tui::app::TermSub::Ops);
}

#[test]
fn history_walks_both_ways_and_falls_off_the_newest_end_empty() {
    let mut a = app();
    focused_ops(&mut a);
    type_line(&mut a, "/help");
    tap(&mut a, crossterm::event::KeyCode::Enter);
    type_line(&mut a, "/status");
    tap(&mut a, crossterm::event::KeyCode::Enter);

    tap(&mut a, crossterm::event::KeyCode::Up);
    assert_eq!(a.ops.input, "/status");
    tap(&mut a, crossterm::event::KeyCode::Up);
    assert_eq!(a.ops.input, "/help");
    tap(&mut a, crossterm::event::KeyCode::Down);
    assert_eq!(a.ops.input, "/status");
    tap(&mut a, crossterm::event::KeyCode::Down);
    assert_eq!(a.ops.input, "", "past the newest entry is a fresh line");
    assert!(a.ops.history_pos.is_none());
    tap(&mut a, crossterm::event::KeyCode::Down);
    assert_eq!(a.ops.input, "", "and Down there stays put");
}

#[test]
fn ops_output_scrolls_with_keys_and_wheel_against_the_published_ceiling() {
    let mut a = app();
    a.on_key(crossterm::event::KeyEvent::from(
        crossterm::event::KeyCode::Char('6'),
    ));
    a.ops.output = (0..100).map(|i| format!("line {i}")).collect();
    a.ops.scroll_max.set(50);

    tap(&mut a, crossterm::event::KeyCode::Up);
    assert_eq!(a.ops.scroll_up, 1, "content-focus arrows move the output");
    a.scroll(-3);
    assert_eq!(a.ops.scroll_up, 4);
    assert_eq!(
        a.log_scroll, None,
        "Main's log no longer moves as a side effect"
    );

    tap(&mut a, crossterm::event::KeyCode::Home);
    assert_eq!(a.ops.scroll_up, 50, "g/Home parks at the oldest");
    tap(&mut a, crossterm::event::KeyCode::Up);
    assert_eq!(a.ops.scroll_up, 50, "clamped at the ceiling, never banked");
    tap(&mut a, crossterm::event::KeyCode::End);
    assert_eq!(a.ops.scroll_up, 0, "G/End resumes following");

    // 2026-09-26: With the input line focused, Up and Down walk history and
    // PageUp/PageDown scroll.
    tap(&mut a, crossterm::event::KeyCode::Char('i'));
    tap(&mut a, crossterm::event::KeyCode::PageUp);
    assert_eq!(a.ops.scroll_up, 10);
}

#[test]
fn running_a_command_snaps_the_scrollback_to_its_output() {
    let mut a = app();
    a.ops.scroll_up = 30;
    execute("/help", &mut a);
    assert_eq!(a.ops.scroll_up, 0, "Enter means: show me the result");
}

#[test]
fn the_output_buffer_is_capped_oldest_first() {
    let mut a = app();
    a.ops.output = (0..1_500).map(|i| format!("old {i}")).collect();
    execute("/help", &mut a);
    assert_eq!(a.ops.output.len(), 1_000, "trimmed to the cap");
    assert!(
        a.ops.output.last().unwrap().contains("/quit"),
        "the newest output — the command just run — is what is kept"
    );
    assert!(
        !a.ops.output.iter().any(|l| l == "old 0"),
        "the oldest lines are what went"
    );
}