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 for `BenchState`: the form, the start gates, run recording and the followed target.
//!
//! Owner: server tui.
//! Invariants: none beyond the types.

use crossterm::event::{KeyCode, KeyEvent};

use super::*;
use crate::tui::app::BenchSub;

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

fn typed(state: &mut BenchState, text: &str) {
    for c in text.chars() {
        state.on_key(key(KeyCode::Char(c)), BenchSub::Suite);
    }
}

/// 2026-09-26: A state with the first benchmark selected and no executor, so nothing can start.
fn state() -> BenchState {
    let mut s = BenchState {
        target: TargetEndpoint::local(8888, "test-model"),
        ..Default::default()
    };
    s.select(0);
    s
}

#[test]
fn selecting_loads_the_schema_and_its_defaults() {
    let s = state();
    assert!(
        !s.specs.is_empty(),
        "the first benchmark declares parameters"
    );
    assert_eq!(s.edit.len(), s.specs.len() + 2);
    assert_eq!(s.row_count(), s.specs.len() + 2);
    for (i, spec) in s.specs.iter().enumerate() {
        assert_eq!(s.edit[i], spec.default.to_edit_string());
    }
}

#[test]
fn every_registered_benchmark_can_be_selected() {
    let mut s = state();
    for i in 0..metrale_bench::registry::all().len() {
        s.select(i);
        assert!(s.descriptor().is_some());
        assert!(!s.plugin_metadata().description.is_empty());
    }
}

#[test]
fn the_last_two_rows_are_always_the_endpoint() {
    let s = state();
    let (url_label, _, _) = s.row_meta(s.specs.len());
    let (model_label, _, _) = s.row_meta(s.specs.len() + 1);
    assert_eq!(url_label, "Endpoint URL");
    assert_eq!(model_label, "Model");
}

#[test]
fn committing_a_valid_edit_updates_the_value_and_clears_the_error() {
    let mut s = state();
    let key_name = s.specs[0].key;
    s.edit[0] = "nonsense".into();
    s.commit_row(0);
    assert!(s.row_error(0).is_some(), "a bad value must be reported");
    s.edit[0] = s.specs[0].default.to_edit_string();
    s.commit_row(0);
    assert!(s.row_error(0).is_none());
    assert_eq!(s.values.get(key_name), Some(&s.specs[0].default));
}

#[test]
fn editing_the_endpoint_normalises_the_trailing_slash() {
    let mut s = state();
    let row = s.specs.len();
    s.edit[row] = "http://10.10.10.3:8888/".into();
    s.commit_row(row);
    assert_eq!(s.target.base_url, "http://10.10.10.3:8888");
    assert_eq!(s.edit[row], "http://10.10.10.3:8888");
    assert!(s.row_error(row).is_none());
}

#[test]
fn an_empty_endpoint_is_rejected_rather_than_silently_kept() {
    let mut s = state();
    let row = s.specs.len();
    s.edit[row] = "   ".into();
    s.commit_row(row);
    assert!(s.row_error(row).is_some());
    assert_eq!(s.target.base_url, "http://127.0.0.1:8888", "unchanged");
}

#[test]
fn a_cancelled_edit_restores_the_committed_value() {
    let mut s = state();
    s.view = View::Params;
    let original = s.edit[0].clone();
    s.on_key(key(KeyCode::Enter), BenchSub::Suite);
    assert!(s.is_editing());
    typed(&mut s, "9");
    assert_ne!(s.edit[0], original);
    s.on_key(key(KeyCode::Esc), BenchSub::Suite);
    assert!(!s.is_editing());
    assert_eq!(s.edit[0], original, "Esc must not leave a half-typed value");
}

#[test]
fn digits_typed_into_a_field_do_not_escape_to_the_section_keys() {
    let mut s = state();
    s.view = View::Params;
    s.on_key(key(KeyCode::Enter), BenchSub::Suite);
    assert!(
        s.is_editing(),
        "the app checks is_editing() before treating digits as section jumps"
    );
}

#[test]
fn the_form_can_be_reset_to_the_schema_defaults() {
    let mut s = state();
    s.view = View::Params;
    s.edit[0] = "nonsense".into();
    s.commit_row(0);
    assert!(!s.errors.is_empty());
    s.on_key(key(KeyCode::Char('d')), BenchSub::Suite);
    assert!(s.errors.is_empty());
    assert_eq!(s.edit[0], s.specs[0].default.to_edit_string());
}

#[test]
fn starting_with_an_invalid_field_is_refused_and_says_why() {
    let mut s = state();
    s.edit[0] = "nonsense".into();
    s.commit_row(0);
    let err = s.start().unwrap_err();
    assert!(err.contains("need fixing"), "{err}");
}

#[test]
fn starting_without_an_executor_is_refused_rather_than_panicking() {
    let mut s = state();
    let err = s.start().unwrap_err();
    assert!(err.contains("executor"), "{err}");
}

#[test]
fn the_shell_running_benchmark_asks_for_confirmation_first() {
    let mut s = state();
    let index = metrale_bench::registry::all()
        .iter()
        .position(|d| d.needs_confirmation)
        .expect("the agentic benchmark requires confirmation");
    s.select(index);
    s.view = View::Params;
    s.on_key(key(KeyCode::Char('s')), BenchSub::Suite);
    assert!(s.confirm_open, "s must open the consent gate, not start");
    assert_ne!(s.view, View::Run);
    s.on_key(key(KeyCode::Char('n')), BenchSub::Suite);
    assert!(!s.confirm_open);
    assert_ne!(s.view, View::Run);
}

#[test]
fn a_benchmark_without_side_effects_does_not_ask() {
    let mut s = state();
    let index = metrale_bench::registry::all()
        .iter()
        .position(|d| !d.needs_confirmation)
        .expect("most benchmarks need no confirmation");
    s.select(index);
    s.view = View::Params;
    // 2026-09-26: No executor, so the start is refused, and without opening the confirmation prompt.
    s.on_key(key(KeyCode::Char('s')), BenchSub::Suite);
    assert!(!s.confirm_open);
}

#[test]
fn list_navigation_is_clamped_to_the_registry() {
    let mut s = state();
    let n = metrale_bench::registry::all().len();
    for _ in 0..n + 5 {
        s.on_key(key(KeyCode::Char('j')), BenchSub::Suite);
    }
    assert_eq!(s.selected, n - 1);
    for _ in 0..n + 5 {
        s.on_key(key(KeyCode::Char('k')), BenchSub::Suite);
    }
    assert_eq!(s.selected, 0);
}

#[test]
fn enter_and_esc_walk_the_three_views() {
    let mut s = state();
    assert_eq!(s.view, View::List);
    s.on_key(key(KeyCode::Enter), BenchSub::Suite);
    assert_eq!(s.view, View::Params);
    s.on_key(key(KeyCode::Esc), BenchSub::Suite);
    assert_eq!(s.view, View::List);
}

#[test]
fn the_glow_is_off_until_a_run_turns_it_on() {
    let s = state();
    assert!(!s.glow);
    assert!(!s.is_running());
}

/// 2026-09-26: A run the dashboard records is found by `history::load_all`, the reader the CLI's
/// history command uses, with its parameters, its target and `source: Tui`.
#[test]
fn a_run_persisted_by_the_dashboard_is_readable_by_the_cli() {
    let dir = tempfile::tempdir().expect("scratch dir");
    let runtime = tokio::runtime::Runtime::new().expect("runtime");
    let store = metrale_bench::ArtifactStore::with_root(dir.path());
    let executor = metrale_bench::BenchmarkExecutor::new(runtime.handle().clone(), store.clone());

    let mut s = state();
    let descriptor = metrale_bench::registry::find("concurrency-sweep").expect("registered");
    s.select(0);
    s.attach(executor, TargetEndpoint::local(9001, "cross-path-model"));
    s.running_descriptor = Some(descriptor);
    s.values = ParamValues::defaults(&descriptor.build().parameters());

    let frame = BenchmarkResult {
        status: RunStatus::Completed,
        phase: "done".into(),
        progress: None,
        summary: Vec::new(),
        table: None,
        verdict: Some(metrale_bench::Verdict::pass("fine")),
        metrics: std::collections::BTreeMap::new(),
        log: Vec::new(),
        dataset_fingerprint: None,
        elapsed: std::time::Duration::from_secs(3),
        hardware_state: None,
    };
    s.persist(&frame);

    let found = metrale_bench::history::load_all(&store);
    assert_eq!(found.len(), 1, "the dashboard's run is in the store");
    let r = &found[0];
    assert_eq!(r.benchmark_id, "concurrency-sweep");
    assert_eq!(
        r.source,
        metrale_bench::RunSource::Tui,
        "tagged as the TUI's"
    );
    assert_eq!(r.metrale_version, crate::cli::METRALE_VERSION);
    assert_eq!(r.target(), TargetEndpoint::local(9001, "cross-path-model"));
    assert!(!r.is_legacy(), "written in the current schema");
    // 2026-09-26: Every parameter, not just the ones a user touched.
    assert_eq!(
        r.params.len(),
        descriptor.build().parameters().len(),
        "params are complete: {:?}",
        r.params
    );
    let values = r
        .values(&descriptor.build().parameters())
        .expect("round-trips");
    assert_eq!(values, s.values);
}

/// 2026-09-26: A second terminal frame does not add a second record.
#[test]
fn a_run_is_recorded_once_even_if_the_terminal_frame_repeats() {
    let dir = tempfile::tempdir().expect("scratch dir");
    let runtime = tokio::runtime::Runtime::new().expect("runtime");
    let store = metrale_bench::ArtifactStore::with_root(dir.path());
    let executor = metrale_bench::BenchmarkExecutor::new(runtime.handle().clone(), store.clone());

    let mut s = state();
    let descriptor = metrale_bench::registry::find("concurrency-sweep").expect("registered");
    s.attach(executor, TargetEndpoint::local(9001, "m"));
    s.running_descriptor = Some(descriptor);

    let frame = BenchmarkResult {
        status: RunStatus::Completed,
        phase: "done".into(),
        progress: None,
        summary: Vec::new(),
        table: None,
        verdict: None,
        metrics: std::collections::BTreeMap::new(),
        log: Vec::new(),
        dataset_fingerprint: None,
        elapsed: std::time::Duration::from_secs(1),
        hardware_state: None,
    };
    s.persist(&frame);
    s.persist(&frame);
    assert_eq!(metrale_bench::history::load_all(&store).len(), 1);
}

#[test]
fn the_target_follows_the_model_that_is_actually_serving() {
    let mut s = BenchState {
        target: metrale_bench::TargetEndpoint::local(8888, String::new()),
        ..BenchState::default()
    };
    assert_eq!(s.target.model, "");

    s.follow_live_model("org/loaded");
    assert_eq!(s.target.model, "org/loaded", "picks up the first model");

    s.follow_live_model("org/swapped-in");
    assert_eq!(s.target.model, "org/swapped-in", "and follows a swap");
}

#[test]
fn a_target_the_operator_typed_is_left_alone() {
    let mut s = BenchState {
        target: metrale_bench::TargetEndpoint::local(8888, "org/mine".to_string()),
        target_model_pinned: true,
        ..BenchState::default()
    };
    s.follow_live_model("org/something-else");
    assert_eq!(s.target.model, "org/mine");
}

#[test]
fn an_empty_live_model_does_not_blank_the_target() {
    let mut s = BenchState {
        target: metrale_bench::TargetEndpoint::local(8888, "org/a".to_string()),
        ..BenchState::default()
    };
    s.follow_live_model("");
    assert_eq!(s.target.model, "org/a");
}