lean-rs-worker 0.1.2

Worker-process boundary for lean-rs host workloads.
Documentation
#![allow(clippy::expect_used, clippy::panic, clippy::wildcard_enum_match_arm)]

use std::path::{Path, PathBuf};
use std::thread;
use std::time::Duration;

use lean_rs_worker::{
    LeanWorker, LeanWorkerConfig, LeanWorkerError, LeanWorkerRestartPolicy, LeanWorkerRestartReason, LeanWorkerStatus,
};

fn worker_binary() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_lean-rs-worker-child"))
}

fn workspace_root() -> PathBuf {
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    manifest_dir
        .parent()
        .and_then(Path::parent)
        .expect("crates/<name> lives two directories below the workspace root")
        .to_path_buf()
}

fn fixture_root() -> PathBuf {
    workspace_root().join("fixtures").join("lean")
}

fn ensure_fixture_built() {
    let fixture = fixture_root();
    lean_toolchain::build_lake_target_quiet(&fixture, "LeanRsFixture").expect("fixture Lake target builds");
}

fn worker_config() -> LeanWorkerConfig {
    LeanWorkerConfig::new(worker_binary())
}

#[test]
fn public_health_check_succeeds() {
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker.health().expect("health check succeeds");
    assert_eq!(worker.status().expect("status succeeds"), LeanWorkerStatus::Running);
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly");
}

#[test]
fn public_fixture_export_call_succeeds() {
    ensure_fixture_built();
    let fixture = fixture_root();
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker
        .load_fixture_capability(&fixture)
        .expect("fixture capability loads in worker");
    let value = worker
        .call_fixture_mul(&fixture, 9, 8)
        .expect("worker calls fixture export");
    assert_eq!(value, 72);
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly");
}

#[test]
fn explicit_restart_replaces_live_child() {
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker.health().expect("health check succeeds before restart");
    worker.restart().expect("worker restarts");
    worker.health().expect("health check succeeds after restart");
    let stats = worker.stats();
    assert_eq!(stats.restarts, 1);
    assert_eq!(stats.explicit_cycles, 1);
    assert_eq!(stats.last_restart_reason, Some(LeanWorkerRestartReason::Explicit));
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after restart");
}

#[test]
fn explicit_cycle_replaces_child_and_records_reason() {
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker.health().expect("health check succeeds before cycle");
    worker.cycle().expect("worker cycles");
    worker.health().expect("health check succeeds after cycle");
    let stats = worker.stats();
    assert_eq!(stats.restarts, 1);
    assert_eq!(stats.explicit_cycles, 1);
    assert_eq!(stats.exits, 1);
    assert_eq!(stats.last_restart_reason, Some(LeanWorkerRestartReason::Explicit));
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after cycle");
}

#[test]
fn max_request_policy_restarts_before_next_request() {
    let config = worker_config().restart_policy(LeanWorkerRestartPolicy::default().max_requests(1));
    let mut worker = LeanWorker::spawn(&config).expect("worker starts");
    worker.health().expect("first request succeeds");
    assert_eq!(worker.stats().restarts, 0);

    worker.health().expect("second request succeeds after policy restart");
    let stats = worker.stats();
    assert_eq!(stats.requests, 2);
    assert_eq!(stats.restarts, 1);
    assert_eq!(stats.max_request_restarts, 1);
    assert_eq!(
        stats.last_restart_reason,
        Some(LeanWorkerRestartReason::MaxRequests { limit: 1 })
    );
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after policy restart");
}

#[test]
fn max_import_policy_restarts_before_next_import() {
    ensure_fixture_built();
    let fixture = fixture_root();
    let config = worker_config().restart_policy(LeanWorkerRestartPolicy::default().max_imports(1));
    let mut worker = LeanWorker::spawn(&config).expect("worker starts");
    worker
        .load_fixture_capability(&fixture)
        .expect("first import-like request succeeds");
    assert_eq!(worker.stats().restarts, 0);

    worker
        .load_fixture_capability(&fixture)
        .expect("second import-like request succeeds after policy restart");
    let stats = worker.stats();
    assert_eq!(stats.imports, 2);
    assert_eq!(stats.restarts, 1);
    assert_eq!(stats.max_import_restarts, 1);
    assert_eq!(
        stats.last_restart_reason,
        Some(LeanWorkerRestartReason::MaxImports { limit: 1 })
    );
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after import policy restart");
}

#[test]
fn idle_restart_policy_restarts_before_next_request() {
    let idle_limit = Duration::from_millis(100);
    let config = worker_config().restart_policy(LeanWorkerRestartPolicy::default().idle_restart_after(idle_limit));
    let mut worker = LeanWorker::spawn(&config).expect("worker starts");
    worker.health().expect("first request succeeds");
    thread::sleep(Duration::from_millis(150));
    worker.health().expect("second request succeeds after idle restart");
    let stats = worker.stats();
    assert_eq!(stats.restarts, 1);
    assert_eq!(stats.idle_restarts, 1);
    match stats.last_restart_reason {
        Some(LeanWorkerRestartReason::Idle { limit, .. }) => assert_eq!(limit, idle_limit),
        other => panic!("expected idle restart reason, got {other:?}"),
    }
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after idle restart");
}

#[test]
fn rss_policy_restarts_or_records_unavailable_sample() {
    let config = worker_config().restart_policy(LeanWorkerRestartPolicy::default().max_rss_kib(1));
    let mut worker = LeanWorker::spawn(&config).expect("worker starts");
    worker.health().expect("request succeeds with RSS policy");
    let stats = worker.stats();
    if stats.rss_samples_unavailable > 0 {
        assert_eq!(stats.rss_restarts, 0);
    } else {
        assert_eq!(stats.restarts, 1);
        assert_eq!(stats.rss_restarts, 1);
        match stats.last_restart_reason {
            Some(LeanWorkerRestartReason::RssCeiling { current_kib, limit_kib }) => {
                assert!(current_kib >= limit_kib);
                assert_eq!(limit_kib, 1);
            }
            other => panic!("expected RSS restart reason, got {other:?}"),
        }
    }
    let exit = worker.terminate().expect("worker terminates");
    assert!(exit.success, "worker should exit cleanly after RSS policy check");
}

#[test]
fn dead_worker_use_is_typed() {
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker.health().expect("health check succeeds");
    worker.__kill_for_test().expect("worker kill succeeds");
    let err = worker.health().expect_err("dead worker use should fail");
    match err {
        LeanWorkerError::ChildPanicOrAbort { exit } => {
            assert!(!exit.success, "killed child should be a fatal worker exit");
        }
        other => panic!("expected child panic/abort error, got {other:?}"),
    }
}

#[test]
fn child_crash_is_typed_and_parent_survives() {
    ensure_fixture_built();
    let fixture = fixture_root();
    let worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    let exit = worker
        .__trigger_lean_panic_fixture(&fixture)
        .expect("parent observes child fatal exit");
    assert!(!exit.success, "panic fixture should terminate the child");
    if !exit.diagnostics.is_empty() {
        assert!(
            exit.diagnostics.contains("lean_rs_fixture: deliberate Lean panic"),
            "child diagnostics should contain Lean panic message, got:\n{}",
            exit.diagnostics,
        );
    }
}

#[test]
fn child_crash_and_policy_restart_are_distinguishable() {
    let config = worker_config().restart_policy(LeanWorkerRestartPolicy::default().max_requests(1));
    let mut worker = LeanWorker::spawn(&config).expect("worker starts");
    worker.health().expect("first request succeeds");
    worker.health().expect("policy restart happens before second request");
    assert_eq!(worker.stats().max_request_restarts, 1);
    let exit = worker.terminate().expect("policy worker terminates");
    assert!(exit.success, "policy worker should exit cleanly");

    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    worker.__kill_for_test().expect("worker kill succeeds");
    let err = worker.health().expect_err("dead worker use should fail");
    match err {
        LeanWorkerError::ChildPanicOrAbort { .. } => {}
        other => panic!("expected child panic/abort error, got {other:?}"),
    }
    assert_eq!(worker.stats().restarts, 0);
}

#[test]
fn missing_fixture_path_maps_to_worker_error() {
    let missing = workspace_root()
        .join("fixtures")
        .join("definitely-missing-worker-fixture");
    let mut worker = LeanWorker::spawn(&worker_config()).expect("worker starts");
    let err = worker
        .load_fixture_capability(&missing)
        .expect_err("missing fixture path should be a typed worker error");
    match err {
        LeanWorkerError::Worker { code, message } => {
            assert_eq!(code, "lean_rs.module_init");
            assert!(
                message.contains("definitely-missing-worker-fixture"),
                "message should identify missing fixture path, got {message}",
            );
        }
        other => panic!("expected worker error, got {other:?}"),
    }
    let exit = worker.terminate().expect("worker terminates after typed error");
    assert!(exit.success, "worker should stay alive after typed load error");
}

#[test]
fn startup_failure_is_typed() {
    let missing = workspace_root().join("target").join("definitely-missing-worker-child");
    let err = LeanWorker::spawn(&LeanWorkerConfig::new(&missing)).expect_err("spawn should fail");
    match err {
        LeanWorkerError::Spawn { executable, .. } => assert_eq!(executable, missing),
        other => panic!("expected spawn error, got {other:?}"),
    }
}

#[test]
fn startup_timeout_is_typed() {
    let err = LeanWorker::spawn(
        &LeanWorkerConfig::new("/bin/cat")
            .env("unused", "unused")
            .startup_timeout(Duration::from_millis(20)),
    )
    .expect_err("non-worker child should not handshake");
    match err {
        LeanWorkerError::Timeout { operation, .. } => assert_eq!(operation, "startup"),
        other => panic!("expected startup timeout, got {other:?}"),
    }
}