malvin 0.2.7

Non-interactive research and coding agent
use super::{
    apply_shared_config_defaults, apply_workspace_config_defaults, global_flag_from_command_line,
    parse_cli_with_config_defaults,
};
use crate::cli::{Cli, SharedOpts};
use clap::{CommandFactory, FromArgMatches};
use malvin::malvin_config_file::AgentConfig;

pub(super) fn write_agent_config(work_dir: &std::path::Path) {
    let path = malvin::malvin_config_path(work_dir);
    let mut text = std::fs::read_to_string(&path).expect("read");
    if text.contains("[agent]") {
        text = text
            .lines()
            .filter(|line| {
                !line.starts_with("model =")
                    && !line.starts_with("max_loops =")
                    && !line.starts_with("max_loops_code =")
                    && !line.starts_with("max_acp_retries =")
                    && *line != "[agent]"
            })
            .collect::<Vec<_>>()
            .join("\n");
    }
    text.push_str("\n[agent]\nmodel = \"cursor:cfg-model\"\nmax_acp_retries = 8\n");
    std::fs::write(&path, text).expect("write");
}

pub(super) fn with_seeded_agent_config(f: impl FnOnce()) {
    malvin::test_utils::with_isolated_home(|work| {
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(work).expect("chdir");
        malvin::malvin_config_file::open_malvin_config(work).expect("seed");
        write_agent_config(work);
        f();
        std::env::set_current_dir(cwd).expect("restore cwd");
    });
}

#[test]
fn write_agent_config_adds_agent_section_to_partial_file() {
    malvin::test_utils::with_isolated_home(|work| {
        let path = malvin::malvin_config_path(work);
        std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
        std::fs::write(&path, "mem_limit_gb = 2\n").expect("write");
        write_agent_config(work);
        let text = std::fs::read_to_string(&path).expect("read");
        assert!(text.contains("[agent]"));
        assert!(text.contains("model = \"cursor:cfg-model\""));
    });
}

#[test]
fn flag_and_shared_helpers_detect_and_apply_defaults() {
    let matches = Cli::command().get_matches_from(["malvin", "admin", "models"]);
    assert!(!global_flag_from_command_line(&matches, "model"));

    let agent = AgentConfig {
        model: malvin::model_id::parse_model_id("cursor:cfg").expect("model"),
        max_hypotheses: malvin::malvin_config_file::DEFAULT_MAX_HYPOTHESES,
        max_acp_retries: 6,
    };
    let mut shared = SharedOpts {
        model: malvin::model_id::parse_model_id("cursor:old").expect("model"),
        verbose: false,
        max_acp_retries: 1,
        doc: false,
        iml: false,
    };
    apply_shared_config_defaults(&matches, &mut shared, &agent);
    assert_eq!(shared.model.canonical(), "cursor:cfg");
    assert_eq!(shared.max_acp_retries, 6);
}

#[test]
fn apply_workspace_config_defaults_overrides_unset_flags_for_gates_only() {
    with_seeded_agent_config(|| {
        let matches = Cli::command().get_matches_from(["malvin", "-g"]);
        let mut cli = Cli::from_arg_matches(&matches).expect("cli");
        apply_workspace_config_defaults(&matches, &mut cli).expect("apply");
        assert_eq!(cli.shared.model.canonical(), "cursor:cfg-model");
        assert_eq!(cli.shared.max_acp_retries, 8);
        assert!(cli.command.is_none());
        assert_eq!(
            cli.router.max_loops,
            malvin::malvin_config_file::DEFAULT_MAX_LOOPS
        );
    });
}

#[test]
fn apply_workspace_config_defaults_respects_explicit_cli_flags_for_gates_only() {
    with_seeded_agent_config(|| {
        let matches = Cli::command().get_matches_from([
            "malvin",
            "--model",
            "cursor:cli-model",
            "--max-acp-retries",
            "2",
            "-g",
            "--max-loops",
            "3",
        ]);
        let mut cli = Cli::from_arg_matches(&matches).expect("cli");
        apply_workspace_config_defaults(&matches, &mut cli).expect("apply");
        assert_eq!(cli.shared.model.canonical(), "cursor:cli-model");
        assert_eq!(cli.shared.max_acp_retries, 2);
        assert_eq!(cli.router.max_loops, 3);
    });
}

#[test]
fn apply_workspace_config_defaults_for_admin_command() {
    with_seeded_agent_config(|| {
        let matches = Cli::command().get_matches_from(["malvin", "admin", "models"]);
        let mut cli = Cli::from_arg_matches(&matches).expect("cli");
        apply_workspace_config_defaults(&matches, &mut cli).expect("apply");
        assert_eq!(cli.shared.model.canonical(), "cursor:cfg-model");
        assert!(matches!(cli.command, Some(crate::cli::Commands::Admin(_))));
    });
}

#[test]
fn apply_workspace_config_defaults_skips_do() {
    malvin::test_utils::with_isolated_home(|work| {
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(work).expect("chdir");
        let config_path = malvin::malvin_config_path(work);
        assert!(!config_path.exists());
        let do_matches = Cli::command().get_matches_from(["malvin", "--do", "hello"]);
        let mut do_cli = Cli::from_arg_matches(&do_matches).expect("cli");
        apply_workspace_config_defaults(&do_matches, &mut do_cli).expect("apply");
        assert!(!config_path.exists());
        std::env::set_current_dir(cwd).expect("restore cwd");
    });
}

#[test]
fn parse_cli_with_config_defaults_gates_only() {
    malvin::test_utils::with_isolated_home(|work| {
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(work).expect("chdir");
        let (cli, _) = parse_cli_with_config_defaults(["malvin", "-g"]).expect("parse");
        assert!(cli.command.is_none());
        assert!(!cli.has_request());
        assert!(cli.router.gates);
        assert!(cli.router.max_loops >= 1);
        std::env::set_current_dir(cwd).expect("restore cwd");
    });
}