malvin 0.2.4

Non-interactive research and coding agent
use super::models_cmd::test_hooks::*;

fn trim_trailing_tips_drops_banner() {
    let t = "a\nb\nTip: upgrade\n";
    assert_eq!(trim_trailing_tip_lines(t).lines().count(), 2);
}

fn trim_trailing_tips_drops_tip_space_banner_without_colon() {
    let t = "a\nb\ntip use TLS in prod\n";
    assert_eq!(trim_trailing_tip_lines(t).lines().count(), 2);
}

fn trim_trailing_tips_keeps_last_line_that_mentions_tip_mid_sentence() {
    let t = "composer-2 — Fast\nSee tip: use TLS in prod\n";
    assert_eq!(
        trim_trailing_tip_lines(t),
        "composer-2 — Fast\nSee tip: use TLS in prod"
    );
}

fn trim_trailing_tips_keeps_line_starting_with_tip_of_english_phrase() {
    let t = "composer-2 — Fast\nTip of the iceberg — latency matters\n";
    assert_eq!(
        trim_trailing_tip_lines(t),
        "composer-2 — Fast\nTip of the iceberg — latency matters"
    );
}

fn parse_model_line_splits_em_dash() {
    let (n, d) = parse_model_line("composer-2 — Fast").expect("parse");
    assert_eq!(n, "composer-2");
    assert_eq!(d, "Fast");
}

fn parse_model_line_splits_ascii_hyphen_when_name_has_many_words() {
    let line = "my production inference tier one model id - Claude via API";
    let (n, d) = parse_model_line(line).expect("parse");
    assert_eq!(n, "my production inference tier one model id");
    assert_eq!(d, "Claude via API");
}

fn models_display_lines_keeps_unparsed_single_token_between_parsed_rows() {
    let text = "composer-2 — Fast\nHEADERS\ngpt-4.1 — Stable";
    let lines = models_display_lines(text).expect("non-empty");
    assert_eq!(
        lines,
        vec![
            "composer-2\tFast".to_string(),
            "HEADERS".to_string(),
            "gpt-4.1\tStable".to_string(),
        ]
    );
}

fn models_subcommand_parse_invokes_cli_helpers() {
    use crate::cli::{Cli, Commands};
    use clap::Parser;
    let cli = Cli::try_parse_from(["malvin", "models"]).expect("parse");
    assert!(matches!(cli.command, Some(Commands::Models(_))));
    let refresh = Cli::try_parse_from(["malvin", "models", "--refresh", "pi:"]).expect("parse");
    match refresh.command {
        Some(Commands::Models(args)) => assert!(args.refresh),
        _ => panic!("expected Models"),
    }
}

#[cfg(unix)]
fn run_models_reads_fake_agent_models_output() {
    use std::os::unix::fs::PermissionsExt;

    use crate::repo_checks::set_fake_command_dir;

    let tmp = tempfile::tempdir().expect("tempdir");
    let agent = tmp.path().join("agent");
    std::fs::write(
        &agent,
        "#!/bin/sh\nif [ \"$1\" = models ]; then printf 'composer-2 — Fast\\nTip: upgrade\\n'; exit 0; fi\nexit 1\n",
    )
    .expect("write fake agent");
    let mut perms = std::fs::metadata(&agent).expect("metadata").permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&agent, perms).expect("chmod fake agent");
    let _guard = set_fake_command_dir(tmp.path());
    print_cursor_models_via_cli_for_test(Some("cursor:")).expect("fake agent models");
    let path = resolve_models_cli().expect("fake agent on fake PATH");
    assert_eq!(path, agent);
}

fn current_model_label_reads_config_or_default() {
    use super::models_cmd::test_hooks::{current_model_label, print_current_footer};
    use crate::output::{enable_stdout_capture, take_captured_stdout};

    crate::test_utils::with_isolated_home(|work| {
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(work).expect("chdir");
        crate::malvin_config_file::open_malvin_config(work).expect("seed");
        let label = current_model_label();
        assert!(label.starts_with("cursor:"), "{label}");
        enable_stdout_capture();
        print_current_footer();
        let out = take_captured_stdout();
        assert!(out.contains("Current:"), "{out}");
        std::env::set_current_dir(cwd).expect("restore");
    });
}

fn cursor_list_models_timeout_honors_env() {
    use super::models_cmd::test_hooks::cursor_list_models_timeout;
    use crate::test_utils::test_env_lock;

    let _lock = test_env_lock();
    let prior = std::env::var_os("MALVIN_CURSOR_LIST_MODELS_TIMEOUT_MS");
    #[allow(unsafe_code)]
    unsafe {
        std::env::set_var("MALVIN_CURSOR_LIST_MODELS_TIMEOUT_MS", "123");
    }
    assert_eq!(
        cursor_list_models_timeout(),
        std::time::Duration::from_millis(123)
    );
    #[allow(unsafe_code)]
    unsafe {
        match prior {
            Some(v) => std::env::set_var("MALVIN_CURSOR_LIST_MODELS_TIMEOUT_MS", v),
            None => std::env::remove_var("MALVIN_CURSOR_LIST_MODELS_TIMEOUT_MS"),
        }
    }
}

fn sdk_catalog_empty_is_detected_even_when_auto_would_be_injected() {
    assert!(!sdk_catalog_has_model_rows(""));
    assert!(!sdk_catalog_has_model_rows("\n\n"));
    assert!(sdk_catalog_has_model_rows("cursor:composer-2\tFast\n"));
    let injected = sdk_model_rows_from_stdout("");
    assert_eq!(injected, vec!["cursor:auto".to_string()]);
}

#[test]
fn kiss_bundled_cli_models_cmd_tests() {
    trim_trailing_tips_drops_banner();
    trim_trailing_tips_drops_tip_space_banner_without_colon();
    trim_trailing_tips_keeps_last_line_that_mentions_tip_mid_sentence();
    trim_trailing_tips_keeps_line_starting_with_tip_of_english_phrase();
    parse_model_line_splits_em_dash();
    parse_model_line_splits_ascii_hyphen_when_name_has_many_words();
    models_display_lines_keeps_unparsed_single_token_between_parsed_rows();
    models_subcommand_parse_invokes_cli_helpers();
    run_models_reads_fake_agent_models_output();
    current_model_label_reads_config_or_default();
    cursor_list_models_timeout_honors_env();
    sdk_catalog_empty_is_detected_even_when_auto_would_be_injected();
}