frame-core 0.3.0

Component model, lifecycle, process isolation — hosts components as supervised BEAM process trees
Documentation
//! Regression wall for the `gc_bif` `{tr,..}` composition defect
//! (`crates/frame-host/attribution/gcbif-wedge/`, Artemis's 2026-07-18 probe
//! report Round 2).
//!
//! Root cause: bare `Scheduler::with_services` installs an EMPTY BIF
//! registry, so `erlang:'+'/2` resolves Deferred at load and guard-BIF
//! dispatch kills the child with `InvalidOperand("guard bif native import")`
//! at first arithmetic. The fix is `frame_core::composition::compose_scheduler`
//! (gate-1 BIF population + `with_services_and_code_server`), plus a typed
//! refusal in `ComponentRegistry::start` for any committed module that still
//! defers an `erlang:*` import.
//!
//! This wall lives beside the fix: it was written in frame-host when the
//! defective composition was found there, and moved here when frame-host
//! migrated onto `frame_core::runtime` (scaffold-unification leg 1) — the
//! host no longer composes schedulers at all, so the wall guards the crate
//! that does. It drives the byte-exact module that proved the bug — the
//! attribution bundle's failing-variant bytecode (committed under
//! `fixtures/gcbif/` beside its source), real `Observed + 1` arithmetic —
//! through the fixed composition and pins all three properties: the child
//! survives news, replies with correct arithmetic, and the committed import
//! table holds zero deferred `erlang:*` entries.

use std::sync::Arc;
use std::time::Duration;

use beamr::module::{ModuleRegistry, ResolvedImportTarget};
use beamr::namespace::NamespaceId;
use beamr::scheduler::{SchedulerConfig, SchedulerServices};
use frame_core::component::{
    ChildArgument, ChildSpec, ComponentId, ComponentMeta, ServiceCapability, SupervisionPolicy,
};
use frame_core::event::LifecycleState;
use frame_core::registry::ComponentRegistry;
use frame_core::supervision::LifecycleConfig;

/// The byte-exact module from the attribution bundle (vsn MD5
/// 166667028645326824780613077217930580718): natural `Observed + 1`
/// arithmetic compiled by OTP 29 erlc into the typed-operand `gc_bif`.
const FAILING_VARIANT_BEAM: &[u8] = include_bytes!("fixtures/gcbif/graph_view_presence.beam");

const CHILD: &str = "presence";
const LIVENESS_MESSAGE: i64 = 0;
const STOP_MESSAGE: i64 = 2;
const OPERATION_TIMEOUT: Duration = Duration::from_secs(3);
const SCHEDULER_THREADS: usize = 2;

fn component_id() -> ComponentId {
    ComponentId::derive("frame.tests.bif-composition", "gcbif-regression")
}

fn meta() -> ComponentMeta {
    ComponentMeta {
        id: component_id(),
        name: "gcbif regression witness (attribution failing-variant bytecode)".to_owned(),
        version: "0.1.0".to_owned(),
        requires: Vec::new(),
        provides: vec![ServiceCapability {
            id: "frame.tests:gcbif-regression@v1".to_owned(),
            description: "real-arithmetic child over the once-failing bytecode".to_owned(),
        }],
        needs: Vec::new(),
        actions: Vec::new(),
        fragments: Vec::new(),
        children: vec![ChildSpec {
            name: CHILD.to_owned(),
            module: "graph_view_presence".to_owned(),
            function: "run".to_owned(),
            arguments: vec![ChildArgument::SupervisorPid],
            liveness_message: LIVENESS_MESSAGE,
            stop_message: STOP_MESSAGE,
        }],
        supervision: Some(SupervisionPolicy {
            max_restarts: 3,
            window: Duration::from_secs(30),
        }),
    }
}

#[test]
fn real_arithmetic_child_survives_news_on_the_once_failing_bytecode()
-> Result<(), Box<dyn std::error::Error>> {
    // The FIXED composition, exactly what `frame_core::runtime` performs
    // internally — composed directly here because this wall also inspects
    // the committed import table, which the wrapper deliberately hides.
    let scheduler = Arc::new(frame_core::composition::compose_scheduler(
        SchedulerConfig {
            thread_count: Some(SCHEDULER_THREADS),
            ..SchedulerConfig::default()
        },
        SchedulerServices::minimal(),
        Arc::new(ModuleRegistry::new()),
    )?);
    let registry = ComponentRegistry::new(
        Arc::clone(&scheduler),
        LifecycleConfig {
            operation_timeout: OPERATION_TIMEOUT,
            max_fragment_bytes: None,
        },
    );
    let id = component_id();

    registry.register(meta(), FAILING_VARIANT_BEAM.to_vec())?;
    registry.start(id)?;

    // The committed import table must hold ZERO deferred erlang:* entries:
    // the populated BIF registry resolved every built-in Native at load.
    let module_atom = scheduler.atom_table().intern("graph_view_presence");
    let committed = scheduler
        .lookup_module_in(NamespaceId::DEFAULT, module_atom)
        .ok_or("committed module missing after start")?;
    let erlang = scheduler.atom_table().intern("erlang");
    let deferred_erlang: Vec<_> = committed
        .resolved_imports
        .iter()
        .filter(|import| {
            import.module == erlang
                && matches!(import.target, ResolvedImportTarget::Deferred { .. })
        })
        .collect();
    assert!(
        deferred_erlang.is_empty(),
        "load left erlang:* imports deferred: {deferred_erlang:?}"
    );

    // The child SURVIVES news and its arithmetic is real: each non-protocol
    // message increments the probe answer, across the exact messages that
    // killed the pre-fix child (41, 43), with no supervision replacement.
    let pid_before = child_pid(&registry)?;
    assert_eq!(registry.probe_child(id, CHILD)?, 0);
    registry.send_child(id, CHILD, 41)?;
    assert_eq!(registry.probe_child(id, CHILD)?, 1, "first news must count");
    registry.send_child(id, CHILD, 43)?;
    assert_eq!(
        registry.probe_child(id, CHILD)?,
        2,
        "second news must count"
    );
    assert_eq!(
        child_pid(&registry)?,
        pid_before,
        "the child must survive arithmetic — a pid change means supervision replaced a dead child"
    );
    let status = registry
        .status(id)?
        .ok_or("component status missing after news")?;
    assert_eq!(status.state, LifecycleState::Running);
    assert!(status.failure.is_none());

    registry.remove(id)?;
    assert_eq!(scheduler.process_count(), 0);
    scheduler.shutdown();
    Ok(())
}

fn child_pid(registry: &ComponentRegistry) -> Result<u64, Box<dyn std::error::Error>> {
    Ok(registry
        .status(component_id())?
        .ok_or("component status missing")?
        .children
        .first()
        .ok_or("component has no child")?
        .pid)
}