malvin 0.2.5

Non-interactive research and coding agent
use std::collections::HashMap;
use std::path::Path;

use crate::cli::{SharedOpts, WorkflowCliOptions};
use crate::prompts::{PromptError, PromptStore};
use crate::router_flow::{RouterArgs, run_router};

#[must_use]
pub(crate) fn effective_init_max_loops(max_loops: usize) -> usize {
    crate::cli::workflow_router_shared::effective_max_loops(max_loops)
}

#[derive(Debug, Clone)]
pub struct InitWorkflowOpts {
    pub max_loops: usize,
    pub max_hypotheses: usize,
}

pub(crate) fn malvin_gates_file_missing() -> Result<bool, String> {
    let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
    if crate::malvin_checks_path(&cwd).is_file() {
        return Ok(false);
    }
    Ok(!cwd.join(crate::MALVIN_CHECKS_REL).is_file())
}

pub(crate) fn should_bootstrap_gates(shared: &SharedOpts) -> Result<bool, String> {
    Ok(shared.gates && malvin_gates_file_missing()?)
}

#[must_use]
pub(crate) fn shared_for_init_bootstrap(shared: &SharedOpts) -> SharedOpts {
    let mut bootstrap = shared.clone();
    bootstrap.gates = false;
    bootstrap
}

pub(crate) fn render_init_router_request(repo_root: &Path) -> Result<String, String> {
    let store = PromptStore::default_store();
    store.ensure_defaults().map_err(|e: PromptError| e.0)?;
    store
        .validate_exists("init_constraints.md")
        .map_err(|e: PromptError| e.0)?;
    let mut ctx = HashMap::new();
    ctx.insert(
        "repo_root_path".to_string(),
        repo_root.display().to_string(),
    );
    store
        .render_prompt_only("init_constraints.md", &ctx)
        .map(|s| s.trim().to_string())
        .map_err(|e: PromptError| e.0)
}

pub async fn maybe_run_init_bootstrap(
    init: InitWorkflowOpts,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    if should_bootstrap_gates(shared)? {
        let bootstrap_shared = shared_for_init_bootstrap(shared);
        run_init(init, &bootstrap_shared, workflow).await?;
    }
    Ok(())
}

pub async fn run_init(
    init: InitWorkflowOpts,
    shared: &SharedOpts,
    workflow: WorkflowCliOptions,
) -> Result<(), String> {
    let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
    let request = render_init_router_request(&cwd)?;
    run_router(
        RouterArgs {
            request: Some(request),
            max_loops: effective_init_max_loops(init.max_loops),
            max_hypotheses: init.max_hypotheses,
        },
        shared,
        workflow,
    )
    .await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cli::SharedOpts;
    use crate::cli::args::Cli;
    use clap::{CommandFactory, FromArgMatches};

    #[test]
    fn init_run_entry_is_covered() {
        let _ = run_init;
        let _ = maybe_run_init_bootstrap;
        let _ = render_init_router_request;
        let _ = effective_init_max_loops;
        let _ = malvin_gates_file_missing;
        let _ = should_bootstrap_gates;
        let _ = shared_for_init_bootstrap;
    }

    #[test]
    fn init_effective_max_loops_is_at_least_one() {
        assert_eq!(effective_init_max_loops(0), 1);
    }

    #[test]
    fn render_init_router_request_expands_cwd() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let text = render_init_router_request(tmp.path()).expect("render");
        let root = tmp.path().display().to_string();
        assert!(
            text.contains(&root),
            "expected cwd in rendered request: {text:?}"
        );
        assert!(
            text.contains(".malvin/gates"),
            "expected gates path constraint: {text:?}"
        );
    }

    #[test]
    fn init_is_not_a_subcommand_and_parses_as_bare_request() {
        let matches = Cli::command()
            .try_get_matches_from(["malvin", "init"])
            .expect("parse");
        let cli = Cli::from_arg_matches(&matches).expect("cli");
        assert!(cli.command.is_none());
        assert_eq!(cli.request.as_deref(), Some("init"));
    }

    #[test]
    fn should_bootstrap_gates_when_gates_flag_on_and_file_missing() {
        crate::test_utils::with_isolated_home(|work| {
            let cwd = std::env::current_dir().expect("cwd");
            std::env::set_current_dir(work).expect("chdir");
            let mut shared = SharedOpts::test_defaults();
            shared.gates = true;
            assert!(malvin_gates_file_missing().expect("probe"));
            assert!(should_bootstrap_gates(&shared).expect("bootstrap"));
            shared.gates = false;
            assert!(!should_bootstrap_gates(&shared).expect("bootstrap off"));
            std::env::set_current_dir(cwd).expect("restore cwd");
        });
    }

    #[test]
    fn should_bootstrap_gates_when_legacy_checks_only_and_gates_missing() {
        crate::test_utils::with_isolated_home(|work| {
            let cwd = std::env::current_dir().expect("cwd");
            std::env::set_current_dir(work).expect("chdir");
            assert!(
                std::process::Command::new("git")
                    .args(["init"])
                    .current_dir(work)
                    .status()
                    .expect("git init")
                    .success()
            );
            std::fs::create_dir_all(work.join(".malvin")).expect("mkdir");
            std::fs::write(work.join(".malvin/checks"), "true\n").expect("legacy checks");
            let mut shared = SharedOpts::test_defaults();
            shared.gates = true;
            assert!(
                should_bootstrap_gates(&shared).expect("bootstrap"),
                "legacy checks must not block bootstrap when .malvin/gates is missing"
            );
            std::env::set_current_dir(cwd).expect("restore cwd");
        });
    }

    #[test]
    fn shared_for_init_bootstrap_clears_gates_flag() {
        let mut shared = SharedOpts::test_defaults();
        shared.gates = true;
        let bootstrap = shared_for_init_bootstrap(&shared);
        assert!(!bootstrap.gates);
        assert!(shared.gates);
    }

    #[test]
    fn init_workflow_opts_clone_preserves_fields() {
        let init = InitWorkflowOpts {
            max_loops: 4,
            max_hypotheses: 6,
        };
        let cloned = init.clone();
        assert_eq!(cloned.max_loops, 4);
        assert_eq!(cloned.max_hypotheses, 6);
    }

    #[test]
    fn help_omits_init_subcommand() {
        let help = Cli::command().render_help().to_string();
        assert!(!help.contains("  init "));
    }

    #[test]
    fn run_init_calls_run_router_not_checks_discovery() {
        let src = include_str!("init_flow.rs");
        assert!(src.contains("run_router"), "run_init must call run_router");
        let discovery = ["ensure_malvin_checks", "_discovered"].concat();
        assert!(
            !src.contains(&discovery),
            "run_init must not call checks discovery"
        );
        let postcondition = ["finish_checks", "_discovery"].concat();
        assert!(
            !src.contains(&postcondition),
            "run_init must not postcondition on .malvin/gates"
        );
        let default_route = include_str!("entrypoint.rs");
        assert!(
            !default_route.contains("return run_async_cli(|| {\n            run_init"),
            "default route must not return after init bootstrap"
        );
        assert!(
            default_route.contains("maybe_run_init_bootstrap"),
            "default route must call maybe_run_init_bootstrap"
        );
        let gates_only = include_str!("entrypoint_gates_only.rs");
        assert!(
            !gates_only.contains("return run_async_cli(|| {\n            run_init"),
            "gates-only route must not return after init bootstrap"
        );
        assert!(
            gates_only.contains("maybe_run_init_bootstrap"),
            "gates-only route must call maybe_run_init_bootstrap"
        );
    }
}

#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
    use super::*;

    #[test]
    fn kiss_cov_unit_names() {
        let _ = stringify!(InitWorkflowOpts);
        let _ = stringify!(run_init);
        let _ = stringify!(maybe_run_init_bootstrap);
        let _ = stringify!(render_init_router_request);
        let _ = stringify!(effective_init_max_loops);
        let init = InitWorkflowOpts {
            max_loops: 2,
            max_hypotheses: 4,
        };
        let _ = init.max_loops;
        let _ = init.max_hypotheses;
        let _: Option<InitWorkflowOpts> = None;
        let _ = effective_init_max_loops;
        let _ = render_init_router_request;
        let _ = run_init;
        let _ = maybe_run_init_bootstrap;
        let _ = malvin_gates_file_missing;
        let _ = should_bootstrap_gates;
        let _ = shared_for_init_bootstrap;
    }
}