This repository has been archived on 2026-10-29. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
Hermodr/test/static-lowering.test.cjs
Meta Vens f8e6e8454e Add static lowering: an Eve surface rendered ahead of time to standalone HTML
The live lowering binds to a provider over CultMesh and updates. That is right
for an operator dashboard and structurally unavailable to everyone else, because
an anonymous visitor to a public page is not a Verse participant and never will
be. Rendering at publish time takes the mesh out of the delivery path, so a
published surface becomes bytes any web server can hand out.

It renders through renderEveSurface, the same function the browser calls, under
jsdom. The alternative was a second emitter walking the same graph, which would
drift from the live path and stay drifted until someone compared two pages side
by side. Eve already renders this way in its own tests, so the harness is
borrowed rather than invented.

Controls that would publish a command are disabled, greyed, and labelled. A
static page has no back-channel and a button that looks live is lying about one.
They are marked rather than removed: deleting them would misrepresent the
surface, when the truth is that the capability exists and is not reachable from
a snapshot.

No daemon. The point of static lowering is that a published surface stops
depending on Hermodr running, and a lowering reachable only by starting a server
would have handed that dependency straight back. jsdom is needed only here and
only at build time, resolving from this repository's install or from the Eve
checkout that already carries it.

29 tests pass. The suite covers the negative checks the extraction plan asked
for: no operable command control survives, no live endpoint appears in the page,
a title carrying markup is escaped rather than injected, and lowering the same
surface twice gives identical bytes, which is what would let a later conformance
check diff static output against the live renderer.

Verified end to end against Erycina's real operator surface: all four panels
render and its Run ingest control arrives inert.

One thing found while writing the fixtures: the renderer silently drops
component kinds it does not know. A fixture inventing one tests nothing, and the
first version of these tests did exactly that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:19:24 +02:00

145 lines
5.1 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const { lowerSurfaceToHtml, neutraliseCommandControls } = require("../src/static-lowering.cjs");
function surfaceWith(children) {
return {
providerId: "erycina",
title: "Erycina Operator",
surface: {
schema: "gamecult.eve.surface.v1",
id: "erycina-operator-surface",
title: "Erycina Operator",
root: { id: "root", kind: "dashboard", props: { title: "Erycina" }, children },
},
};
}
const panel = {
id: "readiness",
kind: "panel",
props: { title: "Readiness" },
// "metric" is the kind Erycina real surface uses for a labelled value.
// The renderer silently drops kinds it does not know, so a fixture that
// invents one tests nothing.
children: [{ id: "manifest", kind: "metric", props: { label: "Manifest", value: "present" } }],
};
const commandButton = {
id: "run-ingest",
kind: "control.button",
props: { label: "Run ingest", command: "erycina.ingest.run", target: "cultmesh://erycina/commands/ingest" },
};
test("a surface lowers to a standalone document", async () => {
const { html, title } = await lowerSurfaceToHtml(surfaceWith([panel]));
assert.equal(title, "Erycina Operator");
assert.match(html, /^<!doctype html>/);
assert.match(html, /<meta charset="utf-8">/);
assert.match(html, /<\/html>\s*$/);
// Content the graph declared actually reaches the page.
assert.match(html, /Readiness/);
assert.match(html, /Manifest/);
});
test("styles are inlined, so the page needs no second request", async () => {
const { html } = await lowerSurfaceToHtml(surfaceWith([panel]));
assert.match(html, /<style>/);
assert.doesNotMatch(html, /<link[^>]+stylesheet/);
});
test("command controls are rendered inert", async () => {
// The plan's negative check: a static page has no back-channel, so a control
// that looks operable is lying about one.
const { html, inertCount } = await lowerSurfaceToHtml(surfaceWith([panel, commandButton]));
assert.equal(inertCount, 1);
assert.match(html, /data-eve-static-inert="true"/);
assert.match(html, /disabled/);
assert.match(html, /eve-static-inert/);
});
test("an inert control keeps its label rather than vanishing", async () => {
// Removing it would misrepresent the surface. The operator should see that
// the capability exists and is simply not reachable from a snapshot.
const { html } = await lowerSurfaceToHtml(surfaceWith([commandButton]));
assert.match(html, /Run ingest/);
});
test("no command target survives into the static page as an operable route", async () => {
const { html } = await lowerSurfaceToHtml(surfaceWith([commandButton]));
// The button may name its command, but nothing may present a live endpoint
// the page could post to.
assert.doesNotMatch(html, /fetch\(/);
assert.doesNotMatch(html, /hermodr\/commands/);
});
test("a surface with no controls marks nothing inert", async () => {
const { inertCount } = await lowerSurfaceToHtml(surfaceWith([panel]));
assert.equal(inertCount, 0);
});
test("lowering the same surface twice gives the same bytes", async () => {
// Determinism is what makes the output diffable, and what would let a future
// conformance check compare static output against the live renderer.
const first = await lowerSurfaceToHtml(surfaceWith([panel, commandButton]));
const second = await lowerSurfaceToHtml(surfaceWith([panel, commandButton]));
assert.equal(first.html, second.html);
});
test("the title falls back through the surface before the provider id", async () => {
const untitled = surfaceWith([panel]);
delete untitled.title;
const { title } = await lowerSurfaceToHtml(untitled);
assert.equal(title, "Erycina Operator");
const bare = surfaceWith([panel]);
delete bare.title;
delete bare.surface.title;
assert.equal((await lowerSurfaceToHtml(bare)).title, "erycina");
});
test("an explicit title wins", async () => {
const { html, title } = await lowerSurfaceToHtml(surfaceWith([panel]), { title: "Velvet" });
assert.equal(title, "Velvet");
assert.match(html, /<title>Velvet<\/title>/);
});
test("a title carrying markup is escaped rather than injected", async () => {
const { html } = await lowerSurfaceToHtml(surfaceWith([panel]), { title: "<script>alert(1)</script>" });
assert.doesNotMatch(html, /<title><script>/);
assert.match(html, /&lt;script&gt;/);
});
test("lowering refuses a document that is not a surface", async () => {
await assert.rejects(() => lowerSurfaceToHtml(null), /requires a surface document/);
await assert.rejects(() => lowerSurfaceToHtml("nope"), /requires a surface document/);
});
test("neutralise marks every control it is given", () => {
const calls = [];
const fake = {
querySelectorAll: () => [makeControl(calls), makeControl(calls)],
};
const count = neutraliseCommandControls(fake, "because");
assert.equal(count, 2);
assert.ok(calls.filter((c) => c === "disabled").length === 2);
assert.ok(calls.filter((c) => c === "eve-static-inert").length === 2);
});
function makeControl(calls) {
return {
setAttribute(name) { calls.push(name); },
classList: { add(name) { calls.push(name); } },
};
}