a11oy / static /3d /selftest /pnt_mount_harness.mjs
betterwithage's picture
chore(sync): mirror static/3d kit + Dockerfile-COPY'd web/*.html to Space (hf-sync)
17074a9 verified
Raw History Blame
11.5 kB
// SPDX-License-Identifier: Apache-2.0
// © 2026 Lutar, Stephen P. Jr. — SZL Holdings · Doctrine v11
//
// pnt_mount_harness.mjs — headless mount harness for surfaces/pnt.js (Dev3).
//
// Runs under Node with NO browser: it stubs a minimal DOM + a minimal THREE module + the
// szl3d toolkit's poll/label so the real surface module can mount(), then feeds it the
// EXACT live JSON shapes that /api/a11oy/v1/pnt/{sensor,coast,resilience,limits} return.
// It asserts the surface wires every endpoint, reads the honesty label (MODELED) straight
// off the JSON, renders it on the HUD, degrades honestly on a 404/degraded payload, and
// tears everything down on unmount. Emits a JSON result object to stdout.
//
// This is the "mounts + polls real endpoints + honest MODELED labels + 0 CDN" test the
// Dev3 spec requires, executed against the genuine module (not a mock of it). The live
// JSON is passed in on argv[2] (captured from the real szl_pnt_mesh handlers by pytest),
// so the shapes are real, not invented here.
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dir = dirname(fileURLToPath(import.meta.url));
const SURFACE = resolve(__dir, "..", "surfaces", "pnt.js");
// ---- minimal DOM stub ------------------------------------------------------
function el() {
const node = {
style: {}, children: [], parentNode: null, _text: "",
className: "", attrs: {},
setAttribute(k, v) { this.attrs[k] = v; },
getAttribute(k) { return this.attrs[k]; },
appendChild(c) { c.parentNode = node; this.children.push(c); return c; },
removeChild(c) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); c.parentNode = null; return c; },
getContext() { return { font: "", measureText: () => ({ width: 80 }), beginPath() {}, moveTo() {}, arcTo() {}, closePath() {}, fill() {}, fillText() {}, fillStyle: "", textBaseline: "" }; },
set textContent(v) { this._text = String(v); },
get textContent() { return this._text; },
set innerHTML(v) { this._text = String(v); },
get innerHTML() { return this._text; },
};
return node;
}
global.document = {
createElement(tag) { const n = el(); n.tag = tag; if (tag === "canvas") { n.width = 0; n.height = 0; } return n; },
body: el(),
};
global.performance = global.performance || { now: () => 0 };
// ---- minimal THREE stub (only what pnt.js touches) -------------------------
class Vec3 {
constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; }
set(x, y, z) { this.x = x; this.y = y; this.z = z; return this; }
copy(v) { this.x = v.x; this.y = v.y; this.z = v.z; return this; }
setScalar(s) { this.x = this.y = this.z = s; return this; }
}
class Color { constructor(h = 0xffffff) { this.set(h); } set(h) { this.r = ((h >> 16) & 255) / 255; this.g = ((h >> 8) & 255) / 255; this.b = (h & 255) / 255; return this; } setHex(h) { return this.set(h); } }
function bufAttr(arr, item) {
return {
array: arr, itemSize: item, count: Math.floor(arr.length / item), needsUpdate: false,
getX(i) { return arr[i * item]; }, getY(i) { return arr[i * item + 1]; }, getZ(i) { return arr[i * item + 2]; },
setX(i, v) { arr[i * item] = v; }, setY(i, v) { arr[i * item + 1] = v; }, setZ(i, v) { arr[i * item + 2] = v; },
};
}
class Geometry {
constructor() { this.attributes = {}; }
setAttribute(k, a) { this.attributes[k] = a; return this; }
rotateX() { return this; } translate() { return this; }
computeVertexNormals() {} dispose() { this.disposed = true; }
}
function planeGeo(w, h, sx, sy) {
const g = new Geometry();
const nx = (sx || 1) + 1, ny = (sy || 1) + 1;
const arr = new Float32Array(nx * ny * 3);
let k = 0;
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
arr[k++] = (i / (nx - 1) - 0.5) * w; arr[k++] = 0; arr[k++] = (j / (ny - 1) - 0.5) * h;
}
g.setAttribute("position", bufAttr(arr, 3));
return g;
}
class Material { constructor(o = {}) { Object.assign(this, o); this.color = new Color(o.color || 0xffffff); this.emissive = new Color(o.emissive || 0); } dispose() { this.disposed = true; } }
class Obj3D {
constructor() { this.children = []; this.position = new Vec3(); this.rotation = new Vec3(); this.scale = new Vec3(1, 1, 1); this.userData = {}; }
add(c) { this.children.push(c); return c; }
remove(c) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); }
traverse(fn) { fn(this); this.children.forEach((c) => c.traverse && c.traverse(fn)); }
}
class Mesh extends Obj3D { constructor(g, m) { super(); this.geometry = g; this.material = m; this.isMesh = true; } }
class Points extends Obj3D { constructor(g, m) { super(); this.geometry = g; this.material = m; this.isPoints = true; } }
class Group extends Obj3D {}
class Sprite extends Obj3D { constructor() { super(); this.isSprite = true; } }
class CanvasTexture { constructor() {} }
class SpriteMaterial { constructor(o) { Object.assign(this, o); } }
const THREE = {
Vector3: Vec3, Color, BufferGeometry: Geometry, BufferAttribute: function (a, i) { return bufAttr(a, i); },
PlaneGeometry: function (w, h, sx, sy) { return planeGeo(w, h, sx, sy); },
SphereGeometry: function () { return new Geometry(); },
IcosahedronGeometry: function () { return new Geometry(); },
TorusGeometry: function () { return new Geometry(); },
RingGeometry: function () { return new Geometry(); },
BoxGeometry: function () { return new Geometry(); },
CylinderGeometry: function () { return new Geometry(); },
TubeGeometry: function () { return new Geometry(); },
CatmullRomCurve3: function (p) { this.points = p; },
MeshStandardMaterial: Material, MeshBasicMaterial: Material,
LineBasicMaterial: Material, PointsMaterial: Material, SpriteMaterial,
Mesh, Points, Group, Sprite, CanvasTexture,
DoubleSide: 2, SRGBColorSpace: "srgb", REVISION: "170",
};
// ---- load the szl3d label + a poll stub (we drive onData directly) ---------
const labelMod = await import(pathToFileURL(resolve(__dir, "..", "szl3d", "szl3d_label.js")).href);
const polled = []; // { endpoint, onData, badge }
const live = {
createBadge() {
const e = el();
let state = "init", label = null;
return { el: e, set(s, info) { state = s; if (info && info.label !== undefined) label = info.label; }, tick() {}, get state() { return state; }, get _label() { return label; } };
},
poll(endpoint, ms, onData, opts) {
const badge = opts && opts.badge;
const h = { endpoint, onData, badge, stopped: false, stop() { this.stopped = true; } };
polled.push(h);
return h;
},
};
// ---- a fake stage matching the szl3d Stage surface pnt.js uses -------------
const frameFns = [];
const scene = new Obj3D();
const stage = {
scene, THREE, backend: "webgl2",
onFrame(fn) { frameFns.push(fn); },
setBloom() { stage._bloom = true; },
};
// ---- mount the REAL surface ------------------------------------------------
const mod = await import(pathToFileURL(SURFACE).href);
const surface = mod.default || mod;
const result = { checks: {}, errors: [] };
function check(name, cond) { result.checks[name] = !!cond; if (!cond) result.errors.push(name); }
// contract shape
check("default_export", surface && typeof surface.mount === "function" && typeof surface.unmount === "function");
check("id_pnt", surface.id === "pnt");
check("endpoints_four", Array.isArray(surface.endpoints) && surface.endpoints.length >= 4);
check("endpoints_sensor", surface.endpoints.includes("/api/a11oy/v1/pnt/sensor"));
const container = el();
surface.mount({ stage, container, live, label: labelMod, THREE, szl3d: { probeBackend() { return {}; } } });
check("bloom_requested", stage._bloom === true);
check("polled_four_endpoints", polled.length >= 4);
const eps = polled.map((p) => p.endpoint);
check("polls_sensor", eps.includes("/api/a11oy/v1/pnt/sensor"));
check("polls_coast", eps.includes("/api/a11oy/v1/pnt/coast"));
check("polls_resilience", eps.includes("/api/a11oy/v1/pnt/resilience"));
check("polls_limits", eps.includes("/api/a11oy/v1/pnt/limits"));
check("scene_objects_added", scene.children.length > 5);
check("frame_registered", frameFns.length >= 1);
// feed the REAL live JSON captured by pytest (argv[2] = path to json file)
const liveJson = JSON.parse(readFileSync(process.argv[2], "utf-8"));
function feed(ep, json) {
const h = polled.find((p) => p.endpoint === ep);
if (!h) return;
const meta = { state: "live", label: (json && json.label) || null, at: Date.now() };
// mirror the REAL szl3d poller: it sets the badge from meta before calling onData.
if (h.badge) h.badge.set(meta.state, { label: meta.label, at: meta.at });
h.onData(json, meta);
}
feed("/api/a11oy/v1/pnt/sensor", liveJson.sensor);
feed("/api/a11oy/v1/pnt/coast", liveJson.coast);
feed("/api/a11oy/v1/pnt/resilience", liveJson.resilience);
feed("/api/a11oy/v1/pnt/limits", liveJson.limits);
// the sensor badge must reflect the honesty label read straight off the JSON
const sensorH = polled.find((p) => p.endpoint === "/api/a11oy/v1/pnt/sensor");
check("sensor_badge_label_modeled", sensorH.badge && sensorH.badge._label === "MODELED");
// run a frame tick (animators must not throw on live data)
let frameThrew = false;
try { frameFns.forEach((f) => f()); } catch (e) { frameThrew = true; result.errors.push("frame_threw:" + e.message); }
check("frame_tick_ok", !frameThrew);
// the HUD must now contain the MODELED label chip text somewhere
function collectText(node, acc) {
if (!node) return acc;
if (node._text) acc.push(node._text);
(node.children || []).forEach((c) => collectText(c, acc));
return acc;
}
const hudPieces = collectText(container, []);
const hudText = hudPieces.join(" | ");
// MODELED must appear on the live value rows (more than once — k_eff, σ_Φ, σ_a, ASD, ...)
const modeledCount = hudPieces.filter((t) => /^MODELED$/.test(t.trim())).length;
check("hud_shows_modeled", /MODELED/.test(hudText) && modeledCount >= 2);
// the sensor cert is MODELED, NEVER a measurement — the ONLY MEASURED token allowed is the
// doctrine legend's reference chip (exactly one), never a fabricated value-row label.
const measuredCount = hudPieces.filter((t) => /^MEASURED$/.test(t.trim())).length;
check("hud_no_fabricated_measured", measuredCount <= 1);
check("hud_shows_sql_true", /TRUE \(at\/above SQL\)/.test(hudText));
// honest degraded path: a 404/missing meta must not crash and must not invent a value
let degradedThrew = false;
try {
const meta404 = { state: "missing", label: null, status: 404, at: null };
sensorH.onData({ degraded: true }, meta404);
} catch (e) { degradedThrew = true; result.errors.push("degraded_threw:" + e.message); }
check("degraded_no_crash", !degradedThrew);
// unmount must stop every poll + clear the scene back to (near) empty
surface.unmount();
check("all_polls_stopped", polled.every((p) => p.stopped));
check("overlay_removed", container.children.length === 0);
// frame fns guard on _alive after unmount -> calling them must be a no-op, never throw
let postThrew = false;
try { frameFns.forEach((f) => f()); } catch (e) { postThrew = true; }
check("post_unmount_frame_safe", !postThrew);
result.ok = result.errors.length === 0;
result.total = Object.keys(result.checks).length;
result.passed = Object.values(result.checks).filter(Boolean).length;
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
process.exit(result.ok ? 0 : 1);