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");
}