malvin 0.2.5

Non-interactive research and coding agent
use crate::herdr::lifecycle_io_support::{
    agent_state_of, assert_bind_shape, assert_idle_then_clear_metadata, collect_until_deadline,
    collect_until_teardown_clear, herdr_test_env_lock, install_test_herdr_env, method_of,
    restore_test_herdr_env, spawn_request_collector, with_herdr_fixture,
};
use crate::herdr::{notify_reclaim, notify_run_end, notify_run_start, notify_working};
use crate::herdr::{reset_session_for_test, session_active_for_test, session_has_binding_for_test};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::time::Duration;

#[test]
fn lifecycle_reports_session_working_idle_clear_over_socket() {
    let _g = herdr_test_env_lock()
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    reset_session_for_test();
    with_herdr_fixture(|run_dir, rx| {
        notify_run_start(run_dir);
        assert!(session_active_for_test());
        notify_reclaim();
        notify_working();
        notify_run_end();
        assert!(!session_active_for_test());
        assert!(!session_has_binding_for_test());
        let reqs = collect_until_teardown_clear(rx);
        assert_bind_shape(&reqs);
        assert_idle_then_clear_metadata(&reqs);
    });
    reset_session_for_test();
}

#[test]
fn notify_working_pulses_working_without_clearing_authority() {
    let _g = herdr_test_env_lock()
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    reset_session_for_test();
    with_herdr_fixture(|run_dir, rx| {
        notify_run_start(run_dir);
        let _ = collect_until_deadline(rx, Duration::from_millis(400));
        notify_working();
        let pulsed = collect_until_deadline(rx, Duration::from_millis(400));
        assert!(pulsed.iter().any(|v| {
            method_of(v) == "pane.report_agent" && agent_state_of(v) == Some("working")
        }));
        assert!(
            pulsed
                .iter()
                .all(|v| method_of(v) != "pane.clear_agent_authority")
        );
        notify_run_end();
        let _ = collect_until_teardown_clear(rx);
    });
    reset_session_for_test();
}

fn bind_test_socket_or_skip(sock: &Path) -> Option<UnixListener> {
    match UnixListener::bind(sock) {
        Ok(listener) => Some(listener),
        Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
            eprintln!("skipping Unix-socket lifecycle test: bind denied: {error}");
            None
        }
        Err(error) => panic!("bind: {error}"),
    }
}

fn retry_teardown_after_socket_loss(sock: &Path, run_dir: &Path) -> bool {
    let Some(listener) = bind_test_socket_or_skip(sock) else {
        return false;
    };
    let rx = spawn_request_collector(listener);
    notify_run_start(run_dir);
    let _ = collect_until_deadline(&rx, Duration::from_millis(500));
    let _ = std::fs::remove_file(sock);
    notify_run_end();
    assert!(session_has_binding_for_test());
    assert!(!session_active_for_test());
    let Some(listener) = bind_test_socket_or_skip(sock) else {
        return false;
    };
    let rx2 = spawn_request_collector(listener);
    notify_run_end();
    assert_idle_then_clear_metadata(&collect_until_teardown_clear(&rx2));
    assert!(!session_has_binding_for_test());
    true
}

#[test]
fn failed_teardown_retains_binding_for_retry_then_clears() {
    let _g = herdr_test_env_lock()
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    reset_session_for_test();
    let dir = tempfile::tempdir().expect("tempdir");
    let sock = dir.path().join("herdr.sock");
    let old = install_test_herdr_env(&sock);
    let run_dir = dir.path().join("retry_run");
    std::fs::create_dir_all(&run_dir).expect("mkdir");
    let _ = retry_teardown_after_socket_loss(&sock, &run_dir);
    restore_test_herdr_env(old);
    reset_session_for_test();
}