frame-host 0.4.0

Frame host server and embedding seam — boots an application's frame-core component tree with an embedded liminal bus, announces the host's real application events on the bus, and serves the built frame page
Documentation
//! The host genuinely boots frame-core machinery through the embedding
//! seam's install path: registration, capability table, supervised start,
//! liveness, lifecycle events, and ordered removal.

use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::time::Duration;

use frame_core::event::{LifecycleEventKind, LifecycleState};
use frame_core::runtime::RuntimePolicy;
use frame_host::manifest;
use frame_host::runtime::HostRuntime;
use frame_host::spec::ComponentInstall;

const EVENT_DEADLINE: Duration = Duration::from_secs(10);
const TEST_EVENT_BUFFER: usize = 64;

/// Policy the test application states (frame supplies no defaults).
const fn test_policy() -> RuntimePolicy {
    RuntimePolicy {
        scheduler_threads: 2,
        operation_timeout: Duration::from_secs(3),
        max_fragment_bytes: None,
    }
}

#[test]
fn boot_emits_lifecycle_events_and_shutdown_removes_cleanly()
-> Result<(), Box<dyn std::error::Error>> {
    let mut runtime = HostRuntime::new(test_policy())?;
    let subscription = runtime
        .registry()
        .subscribe(NonZeroUsize::new(TEST_EVENT_BUFFER).ok_or("zero buffer")?)?;
    let logger = runtime.spawn_event_logger(HashSet::from([manifest::component_id()]))?;

    let statuses = runtime.install(vec![ComponentInstall {
        meta: manifest::component_meta(),
        bytecode: manifest::PRESENCE_BYTECODE.to_vec(),
        support_modules: Vec::new(),
    }])?;
    assert_eq!(statuses.len(), 1);
    let status = &statuses[0];
    assert_eq!(status.state, LifecycleState::Running);
    assert_eq!(status.children.len(), 1);
    assert_eq!(status.children[0].name, manifest::PRESENCE_CHILD);
    assert!(status.failure.is_none());
    assert_eq!(status.id, manifest::component_id());

    // The registered manifest is retrievable and carries zero grants through
    // the host's sole capability facade.
    let grants = runtime
        .registry()
        .host_capabilities()
        .grants_for(manifest::component_id())?;
    assert!(grants.is_empty());

    // The presence child answers its liveness probe; sending news changes
    // the observed marker, proving a real mailbox round-trip.
    let id = manifest::component_id();
    assert_eq!(
        runtime
            .registry()
            .probe_child(id, manifest::PRESENCE_CHILD)?,
        0
    );
    runtime
        .registry()
        .send_child(id, manifest::PRESENCE_CHILD, manifest::NEWS_MESSAGE)?;
    assert_eq!(
        runtime
            .registry()
            .probe_child(id, manifest::PRESENCE_CHILD)?,
        1
    );

    let boot_transitions = drain_transitions(&subscription, LifecycleState::Running)?;
    assert_eq!(
        boot_transitions,
        vec![
            (None, LifecycleState::Registered),
            (Some(LifecycleState::Registered), LifecycleState::Starting),
            (Some(LifecycleState::Starting), LifecycleState::Running),
        ]
    );

    runtime.shutdown(logger)?;
    let teardown_transitions = drain_transitions(&subscription, LifecycleState::Removed)?;
    assert_eq!(
        teardown_transitions,
        vec![
            (Some(LifecycleState::Running), LifecycleState::Stopping),
            (Some(LifecycleState::Stopping), LifecycleState::Stopped),
            (Some(LifecycleState::Stopped), LifecycleState::Removed),
        ]
    );
    Ok(())
}

/// One observed `(from, to)` lifecycle transition.
type Transition = (Option<LifecycleState>, LifecycleState);

/// Collects `(from, to)` transitions for the bundled component until `until`
/// is observed, failing on deadline instead of hanging.
fn drain_transitions(
    subscription: &frame_core::event::LifecycleSubscription,
    until: LifecycleState,
) -> Result<Vec<Transition>, Box<dyn std::error::Error>> {
    let mut transitions = Vec::new();
    loop {
        let event = subscription.recv_timeout(EVENT_DEADLINE)?;
        if event.component_id != manifest::component_id() {
            continue;
        }
        if let LifecycleEventKind::Transition { from, to } = event.kind {
            transitions.push((from, to));
            if to == until {
                return Ok(transitions);
            }
        }
    }
}