hl-engine 0.1.13

Safe Rust lifecycle API for the standalone HL Linux guest engine
use hl_engine::{
    spec::{
        AccountingSpec, CachePolicy, CheckpointMode, DebugAuthorization, DebugOperation,
        EntropySpec, IncompatibleResourcePolicy, TimeRate, TracePrivacy, TraceSpec,
    },
    Engine, Guest, MachineSpec,
};

fn spec() -> MachineSpec {
    MachineSpec::new(Guest::Aarch64, "/bin/true")
}

#[test]
fn discovery_never_implies_unimplemented_control_planes() {
    let capabilities = Engine::new().capabilities();
    assert!(!capabilities.observability.structured_events);
    assert!(!capabilities.observability.metrics);
    assert!(capabilities.debugging.operations.is_empty());
    assert!(!capabilities.time.virtual_time);
    assert!(!capabilities.time.deterministic_entropy);
    assert!(capabilities.resources.live_updates.is_empty());
    assert_eq!(
        capabilities.control.operations,
        std::collections::BTreeSet::from([
            hl_engine::spec::ControlOperation::ProcessInventory,
            hl_engine::spec::ControlOperation::Signal,
            hl_engine::spec::ControlOperation::Pause,
            hl_engine::spec::ControlOperation::Attach,
        ])
    );
    assert_eq!(capabilities.limits.processes, 65_536);
}

#[test]
fn resource_shapes_fail_before_backend_support_checks() {
    let mut value = spec();
    value.resources.memory_reservation_bytes = Some(0);
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().field,
        "resources"
    );

    let mut value = spec();
    value.resources.memory_bytes = Some(1024);
    value.resources.memory_reservation_bytes = Some(2048);
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().field,
        "resources.memory_reservation_bytes"
    );

    let mut value = spec();
    value.resources.accounting = AccountingSpec::Machine;
    let error = Engine::new().validate(&value).unwrap_err();
    assert_eq!(error.field, "resources");
    assert_eq!(
        error.category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );
}

#[test]
fn time_and_entropy_distinguish_invalid_from_unsupported() {
    let mut value = spec();
    value.time.rate = TimeRate {
        numerator: 1,
        denominator: 0,
    };
    assert_eq!(Engine::new().validate(&value).unwrap_err().field, "time");

    let mut value = spec();
    value.time.rate = TimeRate {
        numerator: 2,
        denominator: 1,
    };
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );

    let mut value = spec();
    value.entropy = EntropySpec::Deterministic(Vec::new());
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Invalid
    );
    value.entropy = EntropySpec::Deterministic(vec![1, 2, 3]);
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );
}

#[test]
fn cache_policy_is_explicit_and_read_write_host_store_is_supported() {
    let mut value = spec();
    value.cache.directory = Some("relative".into());
    value.cache.policy = CachePolicy::ReadWrite;
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().field,
        "cache.directory"
    );

    value.cache.directory = Some("/tmp/hl-cache".into());
    Engine::new().validate(&value).unwrap();

    value.cache.policy = CachePolicy::ReadOnly;
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );
}

#[test]
fn checkpoint_observability_and_debug_require_valid_bounded_policy() {
    let mut value = spec();
    value.checkpoint.mode = CheckpointMode::Incremental;
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Conflict
    );
    value.checkpoint.enabled = true;
    value.checkpoint.maximum_pause_ms = Some(0);
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().field,
        "checkpoint.maximum_pause_ms"
    );
    value.checkpoint.maximum_pause_ms = Some(100);
    value.checkpoint.incompatible_resources = IncompatibleResourcePolicy::Refuse;
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );

    let mut value = spec();
    value.observability.trace = Some(TraceSpec {
        queue_capacity: 1,
        include_guest_time: false,
        include_host_time: false,
        privacy: TracePrivacy::MetadataOnly,
    });
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Invalid
    );
    value
        .observability
        .trace
        .as_mut()
        .unwrap()
        .include_host_time = true;
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );

    let mut value = spec();
    value.debug.operations.insert(DebugOperation::Memory);
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().field,
        "debug.authorization"
    );
    value.debug.authorization = Some(DebugAuthorization {
        capability: vec![1],
    });
    assert_eq!(
        Engine::new().validate(&value).unwrap_err().category,
        hl_engine::spec::SpecErrorCategory::Unsupported
    );
}