malvin 0.2.5

Non-interactive research and coding agent
#![cfg(all(test, unix))]

use std::collections::HashSet;
use std::os::unix::process::CommandExt;
use std::process::Command;

use super::super::hostile_orphan_test_util::{
    assert_sibling_monitored_and_blocks_spawn, spawn_agent_pg_and_malvin_sibling,
};
#[cfg(target_os = "linux")]
use super::super::hostile_orphan_test_util::{
    process_alive, read_orphan_pid, spawn_hostile_agent_acp_orphan, wait_for_init_reparent,
};
use super::super::process_group_terminate::{
    terminate_agent_process_group_blocking, terminate_agent_process_group_for_interrupt,
};
use super::super::unix_process_group_ps::ProcRow;
use super::{
    descendant_pids, kill_targets_for_teardown, terminate_agent_process_group,
    terminate_process_group,
};

#[test]
fn terminate_blocking_kills_note_affiliated_pid_without_pgid() {
    crate::test_utils::enable_test_fast_teardown();
    let baseline = crate::malvin_sandbox::malvin_spawn_baseline();
    let mut cmd = Command::new("sleep");
    cmd.arg("120").process_group(0);
    let mut child = cmd.spawn().expect("spawn sleep");
    super::super::unix_process_group_kill_targets::note_session_affiliated_pid(child.id());
    assert!(
        super::super::unix_process_group_kill_targets::is_session_affiliated_pid(child.id()),
        "note_session_affiliated_pid must record pid"
    );
    let targets = kill_targets_for_teardown(None, Some(&baseline));
    assert!(
        targets.contains(&child.id()),
        "affiliated pid must be a teardown target: {targets:?}"
    );
    terminate_agent_process_group_for_interrupt(None, &baseline);
    child
        .wait()
        .expect("affiliated child must exit after teardown");
}

#[tokio::test]
async fn async_teardown_kills_affiliated_pid_without_pgid() {
    crate::test_utils::enable_test_fast_teardown();
    let baseline = HashSet::new();
    let mut cmd = Command::new("sleep");
    cmd.arg("120").process_group(0);
    let mut child = cmd.spawn().expect("spawn sleep");
    super::super::unix_process_group_kill_targets::note_session_affiliated_pid(child.id());
    let targets = kill_targets_for_teardown(None, Some(&baseline));
    assert!(
        targets.contains(&child.id()),
        "affiliated pid must be a teardown target: {targets:?}"
    );
    terminate_agent_process_group(None, &baseline).await;
    assert!(
        child.try_wait().expect("wait").is_some(),
        "graceful async teardown must kill affiliated child when pgid is None"
    );
}

#[test]
fn descendant_pids_walks_child_chain() {
    let rows = vec![
        ProcRow {
            pid: 10,
            pgid: 10,
            ppid: 1,
        },
        ProcRow {
            pid: 11,
            pgid: 10,
            ppid: 10,
        },
        ProcRow {
            pid: 12,
            pgid: 10,
            ppid: 11,
        },
    ];
    let roots = HashSet::from([10]);
    let desc = descendant_pids(&roots, &rows);
    assert!(desc.contains(&10));
    assert!(desc.contains(&11));
    assert!(desc.contains(&12));
}

#[test]
fn terminate_agent_process_group_blocking_noop_without_targets() {
    let empty = HashSet::new();
    terminate_agent_process_group_blocking(None, &empty);
}

#[test]
fn kill_targets_empty_baseline_skips_orphan_scan() {
    let empty = HashSet::new();
    let targets = kill_targets_for_teardown(None, Some(&empty));
    assert!(
        targets.is_empty(),
        "empty baseline must not scan host orphans"
    );
}

#[test]
fn reap_baseline_amnestied_agent_orphans_blocking_noop_without_orphans() {
    super::reap_baseline_amnestied_agent_orphans_blocking();
}

#[tokio::test]
async fn signal_targets_noop_for_empty_set() {
    super::signal_targets(&HashSet::new(), None, 15).await;
}

#[tokio::test]
async fn terminate_process_group_kills_sleep_child() {
    crate::test_utils::clear_test_no_real_agent_env();
    let mut cmd = Command::new("sleep");
    cmd.arg("120").process_group(0);
    let mut child = cmd.spawn().expect("spawn sleep");
    let pgid = child.id();
    terminate_process_group(Some(pgid)).await;
    assert_ne!(child.try_wait().expect("wait"), None);
}

#[tokio::test]
async fn terminate_agent_process_group_kills_sleep_child() {
    crate::test_utils::clear_test_no_real_agent_env();
    let baseline = super::super::unix_process_group_ps::snapshot_pids();
    let mut cmd = Command::new("sleep");
    cmd.arg("120").process_group(0);
    let mut child = cmd.spawn().expect("spawn sleep");
    let pgid = child.id();
    terminate_agent_process_group(Some(pgid), &baseline).await;
    assert_ne!(child.try_wait().expect("wait"), None);
}

#[cfg(target_os = "linux")]
#[tokio::test]
async fn baseline_amnestied_agent_acp_orphan_killed_on_teardown() {
    crate::test_utils::enable_test_fast_teardown();
    let tmp = tempfile::tempdir().expect("tempdir");
    let orphan_pid_file = tmp.path().join("orphan.pid");
    let (mut agent, pgid) = spawn_hostile_agent_acp_orphan(tmp.path(), &orphan_pid_file);
    let orphan_pid = read_orphan_pid(&orphan_pid_file, Some(pgid)).await;
    wait_for_init_reparent(orphan_pid).await;
    let mut baseline = super::super::unix_process_group_ps::snapshot_pids();
    baseline.insert(orphan_pid);
    terminate_agent_process_group(Some(pgid), &baseline).await;
    let _ = agent.wait();
    drop(agent);
    assert!(
        crate::test_utils::test_wait_until_async(|| !process_alive(orphan_pid)).await,
        "teardown must kill baseline-amnestied agent acp orphan (pid={orphan_pid})"
    );
}

#[tokio::test]
async fn malvin_sibling_outside_agent_pg_killed_on_teardown() {
    use crate::malvin_sandbox::{assert_dead_before_next_spawn, clear_active_sandbox_session};

    crate::test_utils::enable_test_fast_teardown();
    clear_active_sandbox_session();
    let baseline = super::super::unix_process_group_ps::snapshot_pids();
    let (agent_pgid, sibling_pid, mut agent_child, mut sibling_child) =
        spawn_agent_pg_and_malvin_sibling();
    assert_sibling_monitored_and_blocks_spawn(agent_pgid, sibling_pid, &baseline);
    terminate_agent_process_group(Some(agent_pgid), &baseline).await;
    clear_active_sandbox_session();
    assert!(
        crate::test_utils::test_wait_until_async(|| {
            sibling_child.try_wait().expect("wait sibling").is_some()
        })
        .await,
        "teardown must terminate malvin sibling outside agent PG (pid={sibling_pid})"
    );
    assert_dead_before_next_spawn().expect("dead-before-next after clean teardown");
    let _ = agent_child.wait();
    let _ = sibling_child.wait();
}

#[cfg(test)]
#[allow(unused_imports)]
mod kiss_cov_gate_refs {
    use super::*;
    #[test]
    fn kiss_cov_unit_names() {
        let _ = terminate_blocking_kills_note_affiliated_pid_without_pgid;
        let _ = async_teardown_kills_affiliated_pid_without_pgid;
        let _ = descendant_pids_walks_child_chain;
        let _ = terminate_agent_process_group_blocking_noop_without_targets;
        let _ = kill_targets_empty_baseline_skips_orphan_scan;
        let _ = reap_baseline_amnestied_agent_orphans_blocking_noop_without_orphans;
        let _ = signal_targets_noop_for_empty_set;
        let _ = terminate_process_group_kills_sleep_child;
        let _ = terminate_agent_process_group_kills_sleep_child;
        let _ = malvin_sibling_outside_agent_pg_killed_on_teardown;
        #[cfg(target_os = "linux")]
        let _ = baseline_amnestied_agent_acp_orphan_killed_on_teardown;
        let _ = crate::acp::hostile_orphan_test_util::spawn_agent_pg_and_malvin_sibling;
        let _ = crate::acp::hostile_orphan_test_util::assert_sibling_monitored_and_blocks_spawn;
        let _ = crate::acp::hostile_orphan_test_util::spawn_hostile_agent_acp_orphan;
    }
}

#[cfg(test)]
mod kiss_cov_auto {
    use super::*;

    #[test]
    fn kiss_cov_signal_targets_noop_for_empty_set() {
        let _ = signal_targets_noop_for_empty_set;
    }
    #[test]
    fn kiss_cov_terminate_process_group_kills_sleep_child() {
        let _ = terminate_process_group_kills_sleep_child;
    }
    #[test]
    fn kiss_cov_terminate_agent_process_group_kills_sleep_child() {
        let _ = terminate_agent_process_group_kills_sleep_child;
    }
}