Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

xtask/tests/the_host_protocol_the_loader_speaks.rs

//! **The wire contract between the loader page and whatever HOST serves it — written
//! down, and held to the shipped page rather than to a briefing.**
//!
//! ## Why this file exists
//!
//! `pkg-facet-wrapped/src/facett/assets/index.html` is the ONE piece of
//! JavaScript this workspace ships, and it is a protocol endpoint: a host names a
//! component, hands it payloads, and reads back what rendered. Until now that protocol
//! lived only in the loader's own comments and in whatever the host's author
//! remembered — and the host is about to change hands. Rickard has ruled that **knut is
//! the Streamlit backend** (2026-09-06): knut serves this page, fetches from korp
//! server-to-server over ordinary gRPC, and feeds the loader over SSE. So the words
//! below stop being a comment in one repo and become the thing knut's `sse.rs` has to
//! satisfy.
//!
//! The loader is the side facett owns, so this is facett's half of the seam: it says
//! what the page PROMISES. It cannot say that knut delivers it — nothing in this repo
//! can — and it does not pretend to.
//!
//! ## Why it is not a grep
//!
//! Every one of these tokens also appears in the loader's PROSE, which is exactly the
//! blind guard LAW 2 warns about: a page that documents the protocol beautifully and
//! implements none of it would pass a substring scan. So the page's comments are
//! stripped first, and `a_page_that_only_DOCUMENTS_the_protocol_is_refused` proves the
//! stripping works by running every assertion against such a page and requiring each to
//! fail by name.

/// The shipped loader page. Not a copy — a copy would drift, and then this suite would
/// be guarding a page nobody ships. Same source `no_js_graphics.rs` reads.
fn real_loader() -> String {
    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("pkg-facet-wrapped/src/facett/assets/index.html");
    std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("the shipped loader must exist at {}: {e}", path.display()))
}

/// `html` with every JavaScript comment removed — `/* … */` blocks and whole lines
/// whose first non-space characters are `//`.
///
/// Deliberately NOT a trailing-`//` stripper: a `//` inside a string literal (`"http://"`)
/// would be eaten, and a guard that quietly deletes code is worse than one that reads a
/// comment. The loader writes its prose on its own lines, so whole-line removal is
/// enough and cannot damage a statement.
fn code_only(html: &str) -> String {
    let mut out = String::with_capacity(html.len());
    let mut rest = html;
    while let Some(open) = rest.find("/*") {
        out.push_str(&rest[..open]);
        match rest[open..].find("*/") {
            Some(close) => rest = &rest[open + close + 2..],
            None => {
                rest = "";
                break;
            }
        }
    }
    out.push_str(rest);
    out.lines()
        .filter(|l| !l.trim_start().starts_with("//"))
        .collect::<Vec<_>>()
        .join("\n")
}

/// The contract, one row per promise: `(token, what a host must know about it)`.
///
/// These are the exact spellings a host writes. `component` and `events` are the two
/// query parameters `knut_wasm_host::entry` already sends (`COMPONENT_PARAM` /
/// `EVENTS_PARAM`), which is why the two halves need no bridge.
const PROMISES: [(&str, &str); 12] = [
    (
        "URLSearchParams",
        "the loader is driven by its own query string, so a host configures it by URL",
    ),
    (
        r#"q.get("component")"#,
        "?component=<name> names WHICH component to mount; without it the page waits and \
         NEVER substitutes a fixture",
    ),
    (
        r#"q.get("data")"#,
        "?data=<url-encoded JSON> is the one-shot payload path (a bare page, no host)",
    ),
    (
        r#"q.get("events")"#,
        "?events=<url> is the LIVE path — this is the parameter knut fills",
    ),
    (
        "new EventSource(",
        "the live path is Server-Sent Events, not a socket and not a poll: a host serves \
         `text/event-stream` and each event's `data` is ONE payload for the named component",
    ),
    (
        "frame.done === true",
        r#"the terminal frame `{"done": true}` closes the feed and LEAVES THE LAST FRAME UP"#,
    ),
    (
        "frame.error",
        r#"the refusal frame `{"error": "<why>"}` shows the host's own reason in the refusal \
         panel — the host's words, not the loader's"#,
    ),
    (
        r#""streamlit:render""#,
        "an embedder may instead post args; Streamlit posts this one after componentReady",
    ),
    (
        r#""facet:render""#,
        "the generic form of the same message, for a host that is not Streamlit",
    ),
    (
        r#""streamlit:componentReady""#,
        "the loader announces itself; Streamlit mounts nothing until it does",
    ),
    (
        r#""streamlit:setFrameHeight""#,
        "the loader sizes its own iframe; a host that ignores this gets a clipped canvas",
    ),
    (
        "window.facet_adapter",
        "the readback naming WHICH PHYSICAL DEVICE the canvas opened — `name`, `kind` \
         (cpu/integrated/discrete), `vendor`, `software`. A host or a robot asserts on \
         `kind`, never on an adapter existing: `--use-angle=swiftshader` gives a real \
         WebGPU adapter that is a CPU rasteriser, and `facet_backend()` and \
         `facet_canvas_api()` both answer `webgpu` on it, truthfully",
    ),
];

/// **Every promise the loader makes to a host is IN THE CODE, not only in its prose.**
///
/// Seen red by `a_page_that_only_DOCUMENTS_the_protocol_is_refused` below, which is the
/// same assertion over a page whose whole protocol lives in comments.
#[test]
fn the_loader_implements_every_promise_a_host_builds_against() {
    let code = code_only(&real_loader());

    // NON-VACUITY: the stripper must leave a real page behind. Without this floor a
    // stripper that returned "" would make every `contains` below fail — which the
    // test would report — but one that returned the WHOLE file would make them all
    // pass, and nothing would say so.
    assert!(
        code.contains("<script type=\"module\">") && code.len() > 2_000,
        "the comment stripper ate the page: {} bytes left",
        code.len()
    );
    assert!(
        !code.contains("Each Server-Sent Event's data is ONE payload"),
        "the comment stripper left prose behind, so the assertions below prove nothing"
    );

    let missing: Vec<String> = PROMISES
        .iter()
        .filter(|(token, _)| !code.contains(token))
        .map(|(token, why)| format!("`{token}` — {why}"))
        .collect();
    assert!(
        missing.is_empty(),
        "the loader no longer implements {} of the {} promises a host builds against, and a \
         host that sends them will hang waiting for a canvas:\n  {}",
        missing.len(),
        PROMISES.len(),
        missing.join("\n  ")
    );
}

/// **The loader never reads `streamlitUrl`, and that is load-bearing.**
///
/// Streamlit appends `?streamlitUrl=<parent origin+path>` to a component URL declared
/// with `declare_component(url=…)`. Under the knut shape the browser lands on knut's
/// bundle mount and is answered with a `303` to this page — and **a redirect drops the
/// query string Streamlit added**. That is harmless only for as long as the loader does
/// not need it: it posts to `window.parent` with `"*"` and never looks the value up.
///
/// If someone teaches the loader to read `streamlitUrl`, the knut entry redirect starts
/// silently losing it, and the failure appears as a component that renders and never
/// talks back. This names it at the edit instead.
///
/// Measured against streamlit 1.60.0's own `ComponentInstance` chunk, 2026-09-06.
#[test]
fn the_loader_does_not_depend_on_a_query_parameter_a_redirect_would_drop() {
    let code = code_only(&real_loader());
    assert!(
        !code.contains("streamlitUrl"),
        "the loader now reads `streamlitUrl`, which knut's entry redirect does not carry \
         through — either the host must stop redirecting, or this dependency must go"
    );
    // The positive half: it does post to the parent, which is how it talks back without
    // ever needing the parent's URL.
    assert!(
        code.contains("window.parent.postMessage"),
        "…and it still talks to its host at all"
    );
}

/// **RED CONTROL — a page that only DOCUMENTS the protocol is refused, promise by
/// promise.** This is the test that makes the two above mean something: without it,
/// `code_only` returning its input unchanged would keep them green forever.
#[test]
fn a_page_that_only_documents_the_protocol_is_refused() {
    // Every token from `PROMISES`, written as beautifully as a comment can be written,
    // over a page that mounts nothing.
    let mut documented = String::from("<!DOCTYPE html><html><body><script type=\"module\">\n");
    documented.push_str("/*\n");
    for (token, why) in PROMISES {
        documented.push_str(&format!("  {token} — {why}\n"));
    }
    documented.push_str("  streamlitUrl is read here too, allegedly.\n*/\n");
    for (token, _) in PROMISES {
        documented.push_str(&format!("// this page speaks {token}\n"));
    }
    documented.push_str("console.log('nothing is mounted');\n</script></body></html>\n");

    let code = code_only(&documented);
    for (token, _) in PROMISES {
        assert!(
            !code.contains(token),
            "`{token}` survived the comment stripper, so the guard is reading prose"
        );
    }
    assert!(!code.contains("streamlitUrl"), "the stripper missed the block comment");
    // …and what is left is the page's actual (empty) behaviour, not nothing at all.
    assert!(code.contains("console.log"), "the stripper ate the code as well as the prose");
}

/// **The loader routes by the EMITTED table and spells no bundle of its own.**
///
/// Bundle groups (2026-09-29): which wasm bundle holds which component is
/// `bundles.json`, emitted by facett-build-thing from `BUNDLE_GROUPS`. The page must
/// read it, load the bundle an entry routes to, and refuse a component another bundle
/// holds BY NAMING that bundle. It must not import a bundle file by name — a static
/// `import … from "./facet_wrapped.js"` is a hand list of one, and the docs/model3d
/// components would never load.
#[test]
fn the_loader_routes_by_the_emitted_table_and_names_no_bundle_file() {
    let code = code_only(&real_loader());
    for (token, why) in [
        (r#"fetch("./bundles.json""#, "reads the emitted routing table"),
        ("routing.default", "an entry no group names goes to the table's default (core)"),
        (".entries", "an entry routes to the group whose `entries` name it"),
        ("import(\"./\" + g.js)", "loads the routed group's JS by the table's path"),
        ("wrongBundle(pick, group)", "a component another bundle holds is refused"),
        ("\"` bundle (\"", "…and the refusal names the right bundle and its file"),
        ("window.facet_bundle", "a driver can read WHICH bundle the page loaded"),
    ] {
        assert!(code.contains(token), "the loader lost `{token}`: {why}");
    }
    assert!(
        !code.contains("facet_wrapped.js") && !code.contains("facet_wrapped_group"),
        "the loader names a bundle file — it must come from bundles.json"
    );
    // The table it reads is the recipe's emission, and names the files the recipe builds.
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
    let table = std::fs::read_to_string(root.join(facett_build_thing::ROUTING_PATH)).unwrap();
    assert_eq!(table, facett_build_thing::routing_json(), "bundles.json is not the emission");
}