node-app-manifest 6.12.2

Canonical manifest schema and validation contract for Node apps
Documentation
//! `ui.requires` must be able to name the capabilities that actually exist.
//!
//! The stage catalog is built from `AppUiRequirements::resolved()`, and
//! `build_ui_stage_catalog` SKIPS any app whose requirements fail to resolve —
//! so a rule that rejects a legitimate name does not merely refuse one
//! declaration, it removes the whole stage from the shell. Capability actions
//! in this codebase are snake_case almost without exception, and so are the
//! app-event resources (`app.agent_session`), so a segment rule that forbade
//! `_` made the field unusable and every shipped stage left it empty.

use node_app_manifest::AppUiRequirements;

fn requirements(
    capabilities: &[&str],
    queries: &[&str],
    streams: &[&str],
) -> AppUiRequirements {
    AppUiRequirements {
        capabilities: capabilities.iter().map(|s| s.to_string()).collect(),
        queries: queries.iter().map(|s| s.to_string()).collect(),
        streams: streams.iter().map(|s| s.to_string()).collect(),
    }
}

#[test]
fn resolves_the_snake_case_names_real_capabilities_use() {
    let resolved = requirements(
        &[
            "contest.world.studio_state",
            "core.lightning.create_invoice",
            "core.did.current_did",
        ],
        &["stages.installed.v1"],
        &["app.contest", "app.agent_session", "transport.state-changed"],
    )
    .resolved()
    .expect("snake_case declarations must resolve");

    assert_eq!(
        resolved,
        vec![
            "contest.world.studio_state",
            "core.lightning.create_invoice",
            "core.did.current_did",
            "stages.installed.v1",
            "app.contest",
            "app.agent_session",
            "transport.state-changed",
        ],
        "capabilities, then queries, then streams — in declaration order"
    );
}

#[test]
fn a_capability_may_be_declared_as_a_wildcard_but_a_stream_may_not() {
    assert!(requirements(&["contest.world.*"], &[], &[]).resolved().is_ok());
    assert!(requirements(&[], &[], &["app.*"]).resolved().is_err());
}

#[test]
fn still_refuses_the_shapes_that_actually_matter() {
    for bad in [
        "core/lightning.create_invoice", // path separator
        "core..create_invoice",          // empty segment via traversal
        "Core.lightning.create_invoice", // uppercase
        "1core.lightning.invoice",       // leading digit
        "",                              // blank
    ] {
        assert!(
            requirements(&[bad], &[], &[]).resolved().is_err(),
            "{bad:?} must not resolve"
        );
    }
}

#[test]
fn duplicates_collapse_and_order_is_stable() {
    let resolved = requirements(
        &["core.did.sign", "core.did.sign"],
        &[],
        &["app.contest", "app.contest"],
    )
    .resolved()
    .expect("duplicates are tolerated, not fatal");

    assert_eq!(resolved, vec!["core.did.sign", "app.contest"]);
}