malvin 0.2.4

Non-interactive research and coding agent
use super::{
    assemble_user_identity, format_current_state, format_local_datetime, format_retry_line,
    format_sandbox_memory_line, format_user_identity,
};

fn format_user_identity_includes_name() {
    let id = format_user_identity();
    assert!(!id.is_empty());
    #[cfg(unix)]
    {
        assert!(id.contains("uid "));
        assert!(super::effective_user_id().is_some());
    }
}

fn assemble_user_identity_with_full_name() {
    assert_eq!(
        assemble_user_identity("dsweet", Some(1000), Some("David Sweet")),
        "dsweet (uid 1000, David Sweet)"
    );
}

fn assemble_user_identity_omits_redundant_full_name() {
    assert_eq!(
        assemble_user_identity("dsweet", Some(1000), Some("dsweet")),
        "dsweet (uid 1000)"
    );
}

fn assemble_user_identity_omits_empty_full_name() {
    assert_eq!(
        assemble_user_identity("dsweet", Some(1000), Some("")),
        "dsweet (uid 1000)"
    );
}

fn assemble_user_identity_without_uid() {
    assert_eq!(
        assemble_user_identity("unknown", None, Some("Bob")),
        "unknown"
    );
}

#[cfg(unix)]
fn format_user_identity_includes_gecos_full_name_when_distinct() {
    let id = format_user_identity();
    let uid = super::effective_user_id().expect("uid");
    if let Some(full_name) = super::passwd_gecos_full_name(uid)
        && full_name != std::env::var("USER").unwrap_or_default() {
            assert!(
                id.contains(&full_name),
                "expected full name {full_name:?} in {id:?}"
            );
        }
}

#[cfg(unix)]
fn current_sandbox_rss_bytes_when_agent_registered() {
    crate::active_agent_heartbeat::clear_active_agent_process_groups_for_test();
    let pgid = std::process::id();
    let baseline = crate::acp::snapshot_pids();
    crate::active_agent_heartbeat::register_active_agent_process_group(Some(pgid), baseline);
    let line = format_sandbox_memory_line(std::path::Path::new("."));
    assert!(line.contains("in use"));
    crate::active_agent_heartbeat::unregister_active_agent_process_group(Some(pgid));
    crate::active_agent_heartbeat::clear_active_agent_process_groups_for_test();
}

fn infer_gate_retry_reasons_empty_without_artifacts() {
    assert!(super::infer_gate_retry_reasons(None, 2).is_empty());
}

fn infer_gate_retry_reasons_empty_for_first_iteration() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    assert!(super::infer_gate_retry_reasons(Some(&artifacts), 1).is_empty());
}

fn gate_iteration_oom_killed_false_when_marker_missing() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    assert!(!crate::sandbox_oom::gate_iteration_oom_killed(
        &artifacts, 1
    ));
}

fn format_local_datetime_is_non_empty() {
    assert!(!format_local_datetime().is_empty());
}

fn format_sandbox_memory_line_includes_limit_and_available() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let line = format_sandbox_memory_line(tmp.path());
    assert!(line.starts_with("Sandbox memory:"));
    assert!(line.contains("limit"));
    assert!(line.contains("available"));
}

fn format_retry_line_first_run_is_not_retry() {
    let line = format_retry_line(None, None);
    assert!(line.contains("not a retry"));
}

fn format_retry_line_first_gate_iteration_is_not_retry() {
    let line = format_retry_line(Some(1), None);
    assert!(line.contains("first outer gate-loop"));
}

fn format_retry_line_second_iteration_is_retry_without_done() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    crate::artifacts::ensure_gate_exp_log_file(&artifacts, 1).expect("exp log");
    let line = format_retry_line(Some(2), Some(&artifacts));
    assert!(line.contains("retry #1"));
    assert!(line.contains("quality gates"));
}

fn format_retry_line_detects_oom_from_sandbox_marker() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    crate::sandbox_oom::record_sandbox_oom_kill(
        &artifacts.run_dir,
        crate::sandbox_oom::SandboxOomKillRecord::from_facts(
            1,
            crate::sandbox_oom::SandboxOomKillFacts {
                reason: crate::sandbox_oom::OOM_REASON_MEMORY_LIMIT,
                rss_bytes: Some(999),
                limit_bytes: 512,
                pgid: 42,
            },
        ),
    )
    .expect("write");
    let line = format_retry_line(Some(2), Some(&artifacts));
    assert!(line.contains("OOM"));
}

fn format_retry_line_gates_failure_after_done() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    let prev = artifacts.gate_exp_log_path(1);
    std::fs::create_dir_all(prev.parent().expect("parent")).expect("mkdir");
    std::fs::write(&prev, "## Step 1 — router mock\n").expect("write");
    let line = format_retry_line(Some(2), Some(&artifacts));
    assert!(line.contains("quality gates"));
}

fn format_current_state_joins_all_sections() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let text = format_current_state(tmp.path(), None, None);
    assert!(text.contains("User:"));
    assert!(text.contains("Date/time:"));
    assert!(text.contains("Sandbox memory:"));
    assert!(text.contains("Retry:"));
}

fn kiss_cov_current_state_non_unix_branch() {
    crate::test_utils::with_isolated_home(|_| {
        let tmp = tempfile::tempdir().expect("tempdir");
        let _artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
            .expect("artifacts");
        let _ = super::current_sandbox_rss_bytes;
        let _ = super::effective_user_id;
        let _ = super::append_unsolved_reason;
        let _ = super::append_oom_reason;
        let _ = super::append_gates_reason;
    });
}

fn append_unsolved_reason_records_missing_marker() {
    crate::test_utils::with_isolated_home(|_| {
        let tmp = tempfile::tempdir().expect("tempdir");
        let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
            .expect("artifacts");
        let prev = artifacts.gate_exp_log_path(1);
        std::fs::create_dir_all(prev.parent().expect("parent")).expect("mkdir");
        std::fs::write(&prev, "no marker\n").expect("write");
        let mut reasons = Vec::new();
        super::append_unsolved_reason(&mut reasons, &artifacts, 1);
        assert_eq!(reasons.len(), 1);
    });
}

fn append_oom_reason_records_memory_kill() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
        .expect("artifacts");
    crate::sandbox_oom::record_sandbox_oom_kill(
        &artifacts.run_dir,
        crate::sandbox_oom::SandboxOomKillRecord::from_facts(
            1,
            crate::sandbox_oom::SandboxOomKillFacts {
                reason: crate::sandbox_oom::OOM_REASON_MEMORY_LIMIT,
                rss_bytes: Some(999),
                limit_bytes: 512,
                pgid: 42,
            },
        ),
    )
    .expect("write");
    let mut reasons = Vec::new();
    super::append_oom_reason(&mut reasons, &artifacts, 1);
    assert!(reasons.iter().any(|r| r.contains("OOM")));
}

fn append_gates_reason_after_done_session() {
    crate::test_utils::with_isolated_home(|_| {
        let tmp = tempfile::tempdir().expect("tempdir");
        let artifacts = crate::artifacts::create_run_artifacts_from_text("code", Some(tmp.path()))
            .expect("artifacts");
        let prev = artifacts.gate_exp_log_path(1);
        std::fs::create_dir_all(prev.parent().expect("parent")).expect("mkdir");
        std::fs::write(&prev, "## Step 1 — router mock\n").expect("write");
        let mut reasons = Vec::new();
        super::append_unsolved_reason(&mut reasons, &artifacts, 1);
        assert!(reasons.iter().any(|r| r.contains("quality gates")));
    });
}

#[test]
fn kiss_bundled_current_state_tests() {
    format_user_identity_includes_name();
    assemble_user_identity_with_full_name();
    assemble_user_identity_omits_redundant_full_name();
    assemble_user_identity_omits_empty_full_name();
    assemble_user_identity_without_uid();
    format_user_identity_includes_gecos_full_name_when_distinct();
    current_sandbox_rss_bytes_when_agent_registered();
    infer_gate_retry_reasons_empty_without_artifacts();
    infer_gate_retry_reasons_empty_for_first_iteration();
    gate_iteration_oom_killed_false_when_marker_missing();
    format_local_datetime_is_non_empty();
    format_sandbox_memory_line_includes_limit_and_available();
    format_retry_line_first_run_is_not_retry();
    format_retry_line_first_gate_iteration_is_not_retry();
    format_retry_line_second_iteration_is_retry_without_done();
    format_retry_line_detects_oom_from_sandbox_marker();
    format_retry_line_gates_failure_after_done();
    format_current_state_joins_all_sections();
    kiss_cov_current_state_non_unix_branch();
    append_unsolved_reason_records_missing_marker();
    append_oom_reason_records_memory_kill();
    append_gates_reason_after_done_session();
}