scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use std::process::Command;

#[test]
fn scena_schema_cli_lists_and_gets_stable_contracts() {
    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "list"])
        .output()
        .expect("scena schema list runs");

    assert!(output.status.success(), "stderr={}", stderr(&output));
    assert!(
        output.stderr.is_empty(),
        "schema list should keep stdout JSON clean and stderr empty, got {}",
        stderr(&output)
    );
    let catalog: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema list emits JSON");
    assert_eq!(catalog["schema"], "scena.schema_catalog.v1");
    assert_schema(&catalog, "scena.render_introspection.v1");
    assert_schema(&catalog, "scena.render_quality.v1");
    assert_schema(&catalog, "scena.visibility_diagnosis.v1");
    assert_schema(&catalog, "scena.scene_recipe.v1");
    assert_schema(&catalog, "scena.scene_recipe_validation.v1");
    assert_schema(&catalog, "scena.scene_recipe_build.v1");
    assert_schema(&catalog, "scena.recipe_render_result.v1");
    assert_schema(&catalog, "scena.placement_result.v1");
    assert_schema(&catalog, "scena.asset_catalog.v1");
    assert_schema(&catalog, "scena.asset_readiness_report.v1");
    assert_schema(&catalog, "scena.asset_doctor.v1");
    assert_schema(&catalog, "scena.visual_repair_plan.v1");
    assert_schema(&catalog, "scena.agent_loop_result.v1");
    assert_schema(&catalog, "scena.agent_smoke_template.v1");
    assert_schema(&catalog, "scena.browser_proof_run.v1");
    assert_schema(&catalog, "scena.animation_introspection.v1");
    assert_schema(&catalog, "scena.interaction_expectation.v1");
    assert_schema(&catalog, "scena.interaction_verification.v1");
    assert_schema(&catalog, "scena.connector_browser.v1");
    assert_schema(&catalog, "scena.product_options.v1");
    assert_schema(&catalog, "scena.presentation_timeline.v1");
    assert_schema(&catalog, "scena.scene_host_grounding.v1");
    assert_schema(&catalog, "scena.scene_host_measurement_overlay.v1");
    assert_listed_fixtures_exist(&catalog);

    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "get", "scena.render_introspection.v1"])
        .output()
        .expect("scena schema get runs");
    assert!(output.status.success(), "stderr={}", stderr(&output));
    let entry: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema get emits JSON");
    assert_eq!(entry["schema"], "scena.schema_entry.v1");
    assert_eq!(entry["entry"]["schema"], "scena.render_introspection.v1");
    assert_eq!(entry["example"]["schema"], "scena.render_introspection.v1");

    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "get", "scena.scene_recipe.v1"])
        .output()
        .expect("scena schema get recipe runs");
    assert!(output.status.success(), "stderr={}", stderr(&output));
    let entry: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema get recipe emits JSON");
    assert_eq!(entry["schema"], "scena.schema_entry.v1");
    assert_eq!(entry["entry"]["schema"], "scena.scene_recipe.v1");
    assert_eq!(entry["example"]["schema"], "scena.scene_recipe.v1");
    assert_eq!(entry["invalid_example"]["schema"], "scena.scene_recipe.v1");
    assert_eq!(entry["invalid_example"]["importe"][0]["id"], "part");

    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "get", "scena.render_introspect.v1"])
        .output()
        .expect("unknown schema command runs");
    assert!(!output.status.success());
    assert!(
        stderr(&output).contains("scena.render_introspection.v1"),
        "unknown schema should suggest a near miss, stderr={}",
        stderr(&output)
    );
}

#[test]
fn scena_schema_cli_stdout_matches_golden_fixture() {
    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "list"])
        .output()
        .expect("scena schema list runs");
    assert!(output.status.success(), "stderr={}", stderr(&output));
    let actual: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema list emits JSON");
    let expected: serde_json::Value =
        serde_json::from_str(include_str!("assets/cli-golden/schema_list_stdout.json"))
            .expect("golden schema list fixture parses");
    assert_eq!(actual, expected);

    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "get", "scena.scene_recipe.v1"])
        .output()
        .expect("scena schema get recipe runs");
    assert!(output.status.success(), "stderr={}", stderr(&output));
    let actual: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema get emits JSON");
    let expected: serde_json::Value = serde_json::from_str(include_str!(
        "assets/cli-golden/schema_get_scene_recipe_stdout.json"
    ))
    .expect("golden schema get fixture parses");
    assert_eq!(actual, expected);
}

#[test]
fn scena_schema_cli_catalog_covers_stable_contract_fixture_schemas() {
    let output = Command::new(env!("CARGO_BIN_EXE_scena"))
        .args(["schema", "list"])
        .output()
        .expect("scena schema list runs");
    assert!(output.status.success(), "stderr={}", stderr(&output));
    let catalog: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("schema list emits JSON");
    let catalog_schemas = catalog["entries"]
        .as_array()
        .expect("schema catalog has entries")
        .iter()
        .filter_map(|entry| entry["schema"].as_str())
        .collect::<BTreeSet<_>>();

    for entry in fs::read_dir("tests/assets/stable-contracts")
        .expect("stable-contract fixture directory exists")
    {
        let path = entry.expect("fixture dir entry").path();
        if path.extension().and_then(|extension| extension.to_str()) != Some("json") {
            continue;
        }
        let fixture: serde_json::Value =
            serde_json::from_str(&fs::read_to_string(&path).expect("fixture file reads as UTF-8"))
                .expect("stable-contract fixture parses as JSON");
        let Some(schema) = fixture["schema"].as_str() else {
            continue;
        };
        assert!(
            catalog_schemas.contains(schema),
            "schema catalog missing {schema} for fixture {}",
            path.display()
        );
    }
}

fn assert_schema(catalog: &serde_json::Value, schema: &str) {
    let entries = catalog["entries"]
        .as_array()
        .expect("schema catalog has entries");
    assert!(
        entries.iter().any(|entry| entry["schema"] == schema),
        "catalog missing {schema}: {catalog:#}"
    );
}

fn assert_listed_fixtures_exist(catalog: &serde_json::Value) {
    let entries = catalog["entries"]
        .as_array()
        .expect("schema catalog has entries");
    for entry in entries {
        let Some(path) = entry["fixture_path"].as_str() else {
            continue;
        };
        assert!(
            Path::new(path).exists(),
            "schema catalog fixture path does not exist: {path}"
        );
    }
}

fn stderr(output: &std::process::Output) -> String {
    String::from_utf8_lossy(&output.stderr).into_owned()
}