lean-rs-worker 0.1.1

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

use std::path::{Path, PathBuf};

use lean_rs::LeanBuiltCapability;
use lean_rs_worker::{
    LeanWorkerCapabilityBuilder, LeanWorkerChild, LeanWorkerCommandMetadata, LeanWorkerError, LeanWorkerRestartPolicy,
};
use serde_json::json;

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 interop_root() -> PathBuf {
    workspace_root().join("fixtures").join("interop-shims")
}

fn builder() -> LeanWorkerCapabilityBuilder {
    LeanWorkerCapabilityBuilder::new(
        interop_root(),
        "lean_rs_interop_consumer",
        "LeanRsInteropConsumer",
        ["LeanRsInteropConsumer.Callback"],
    )
    .worker_executable(worker_binary())
}

#[test]
fn built_capability_builder_infers_lake_root_from_dylib_path() {
    let dylib = interop_root()
        .join(".lake")
        .join("build")
        .join("lib")
        .join(if cfg!(target_os = "macos") {
            "liblean__rs__interop__consumer_LeanRsInteropConsumer.dylib"
        } else {
            "liblean__rs__interop__consumer_LeanRsInteropConsumer.so"
        });
    let spec = LeanBuiltCapability::path(&dylib)
        .package("lean_rs_interop_consumer")
        .module("LeanRsInteropConsumer");

    let builder = LeanWorkerCapabilityBuilder::from_built_capability(&spec, ["LeanRsInteropConsumer.Callback"])
        .expect("standard Lake dylib path is accepted")
        .worker_executable(worker_binary());

    assert_eq!(
        builder.session_key(),
        LeanWorkerCapabilityBuilder::new(
            interop_root(),
            "lean_rs_interop_consumer",
            "LeanRsInteropConsumer",
            ["LeanRsInteropConsumer.Callback"],
        )
        .worker_executable(worker_binary())
        .session_key(),
    );
}

#[test]
fn app_owned_worker_child_locator_accepts_explicit_binary() {
    let child = LeanWorkerChild::path(worker_binary());
    let mut capability = LeanWorkerCapabilityBuilder::new(
        interop_root(),
        "lean_rs_interop_consumer",
        "LeanRsInteropConsumer",
        ["LeanRsInteropConsumer.Callback"],
    )
    .worker_child(child)
    .open()
    .expect("explicit app-owned worker child opens");

    capability.open_session(None, None).expect("session opens");
}

#[test]
fn builder_opens_worker_capability_and_validates_metadata() {
    let mut capability = builder()
        .validate_metadata(
            "lean_rs_interop_consumer_worker_metadata",
            json!({"caller": "builder-test"}),
        )
        .open()
        .expect("builder opens capability");

    assert!(
        capability.dylib_path().is_file(),
        "builder should return the built Lake dylib path"
    );
    assert_eq!(
        capability.validated_metadata().map(|metadata| &metadata.commands),
        Some(&vec![
            LeanWorkerCommandMetadata {
                name: "version".to_owned(),
                version: "fixture-1".to_owned(),
            },
            LeanWorkerCommandMetadata {
                name: "scan".to_owned(),
                version: "fixture-2".to_owned(),
            },
        ]),
    );
    assert_eq!(capability.runtime_metadata().worker_version, env!("CARGO_PKG_VERSION"));

    let mut session = capability
        .open_session(None, None)
        .expect("session opens after builder");
    let metadata = session
        .capability_metadata(
            "lean_rs_interop_consumer_worker_metadata",
            &json!({"caller": "builder-test-second-session"}),
            None,
            None,
        )
        .expect("metadata call succeeds through reopened session");
    assert_eq!(metadata.commands.len(), 2);
}

#[test]
fn missing_lake_target_is_a_build_error() {
    let err = LeanWorkerCapabilityBuilder::new(
        interop_root(),
        "lean_rs_interop_consumer",
        "MissingTarget",
        ["LeanRsInteropConsumer.Callback"],
    )
    .worker_executable(worker_binary())
    .open()
    .expect_err("missing Lake target should fail before worker startup");

    match err {
        LeanWorkerError::CapabilityBuild { diagnostic } => {
            let rendered = diagnostic.to_string();
            assert!(
                rendered.contains("MissingTarget"),
                "diagnostic should name the missing target: {rendered}"
            );
        }
        other => panic!("expected capability build error, got {other:?}"),
    }
}

#[test]
fn missing_worker_child_is_a_spawn_error() {
    let missing = workspace_root().join("target").join("definitely-not-a-worker-child");
    let err = builder()
        .worker_executable(missing.clone())
        .open()
        .expect_err("missing worker child should fail at spawn");

    match err {
        LeanWorkerError::Spawn { executable, .. } => assert_eq!(executable, missing),
        other => panic!("expected spawn error, got {other:?}"),
    }
}

#[test]
fn metadata_validation_failure_is_typed() {
    let err = builder()
        .validate_metadata(
            "lean_rs_interop_consumer_worker_metadata_missing",
            json!({"caller": "builder-test"}),
        )
        .open()
        .expect_err("metadata validation should fail when export is missing");

    match err {
        LeanWorkerError::Worker { code, message } => {
            assert_eq!(code, "lean_rs.symbol_lookup");
            assert!(
                message.contains("lean_rs_interop_consumer_worker_metadata_missing"),
                "message should name missing export, got {message}",
            );
        }
        other => panic!("expected worker symbol lookup error, got {other:?}"),
    }
}

#[test]
fn restart_policy_override_is_applied_during_builder_startup() {
    let capability = builder()
        .restart_policy(LeanWorkerRestartPolicy::default().max_requests(1))
        .open()
        .expect("builder opens capability with restart policy");

    let stats = capability.worker().stats();
    assert!(
        stats.max_request_restarts >= 1,
        "health then session-open should trigger the max-request restart policy, stats={stats:?}",
    );
}