Files
XZBT-NGN/tests/postmessage-interop.test.js
T
Labyricorn 745912e451 Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3,
which had never been split into per-step commits:

- src/local-surfaces.js + src/surface-url.js (new); src/ui.js,
  src/validation.js, src/connection.js and public/index.html updated for
  local-surface hosting and generic surface rendering
- tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7)
  new; connection/museum-gallery/surface-validation suites updated
- reference exhibits: shared/contract-core.js defaults to Contract 5.3
  (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface
  catalog; aquarium/haunted-house/planetarium adapters updated
- SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js,
  Observation-surface boot branch, local-change hooks, view.pillars /
  view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT
- SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no
  longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on
  its presence/type, matching the host's own envelope validation; the
  adapter now advertises contract minor 3 / version 5.3.0, which it already
  implemented via the 5.3 surfaces field. Root cause of the five failing
  postmessage-interop tests (host hello was silently dropped).
- docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs;
  test-fixtures/PROVENANCE.md resync record

Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7);
SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check
clean for changed files; two pre-existing trailing-whitespace lines remain
in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from
the authoritative SciFi source.

Step 6.7 live verification (browser Observation, packaged standalone) is
still pending and is not claimed here.
2026-09-14 19:45:27 -07:00

248 lines
13 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
import { ExhibitHost } from '../src/host.js';
import { postMessageTransport } from '../src/transport/post-message.js';
const SHARED = new URL('../test-fixtures/reference-exhibits/shared/', import.meta.url);
const ROOT = new URL('../test-fixtures/reference-exhibits/', import.meta.url);
const BASE_ORIGIN = 'http://127.0.0.1:4173';
function source(base, file) {
return readFileSync(new URL(file, base), 'utf8');
}
/**
* A REAL postMessage transport between two window-like objects -- not a
* synthetic in-memory peer that hand-crafts contract responses (see
* tests/host.test.js's peer() fixture, which fakes the wire protocol
* itself). This harness only supplies the generic browser plumbing --
* origin-checked delivery, async dispatch, and window.source identity,
* exactly per the postMessage spec -- and lets the REAL, unmodified
* production code on both ends (src/host.js + src/transport/post-message.js
* for NGN, and the exhibit-side code under test-fixtures/reference-exhibits/)
* do 100% of the actual protocol handling. This is the only way an
* exact-string envelope-gating bug living inside an exhibit's inbound
* message filter can ever be caught by an automated test -- a hand-rolled
* peer() fixture that answers `hello` directly can never exercise that gate
* at all. An optional `rewriteOutgoing` hook lets a test mutate every
* message NGN sends before it crosses the wire, without touching src/host.js
* itself -- used below to prove the advisory `xzbt` tag really is ignored.
*/
function makeWindowPair(exhibitPath, { rewriteOutgoing } = {}) {
const ngn = { location: { origin: BASE_ORIGIN, href: BASE_ORIGIN + '/' }, listeners: new Set() };
const exhibit = { location: { origin: BASE_ORIGIN, href: BASE_ORIGIN + exhibitPath }, listeners: new Set() };
ngn.parent = ngn;
exhibit.parent = ngn;
for (const w of [ngn, exhibit]) {
w.addEventListener = (type, fn) => { if (type === 'message') w.listeners.add(fn); };
w.removeEventListener = (type, fn) => { if (type === 'message') w.listeners.delete(fn); };
}
// Real postMessage semantics: targetOrigin is checked at delivery time,
// delivery is asynchronous, and event.source is the caller's own window
// object -- never something the message payload can spoof.
exhibit.postMessage = (data, targetOrigin) => {
if (targetOrigin !== '*' && targetOrigin !== exhibit.location.origin) return;
let payload = data;
if (rewriteOutgoing) payload = rewriteOutgoing(payload) || payload;
const cloned = JSON.parse(JSON.stringify(payload));
setTimeout(() => { for (const fn of [...exhibit.listeners]) fn({ origin: ngn.location.origin, source: ngn, data: cloned }); }, 0);
};
ngn.postMessage = (data, targetOrigin) => {
if (targetOrigin !== '*' && targetOrigin !== ngn.location.origin) return;
const cloned = JSON.parse(JSON.stringify(data));
setTimeout(() => { for (const fn of [...ngn.listeners]) fn({ origin: exhibit.location.origin, source: exhibit, data: cloned }); }, 0);
};
return { ngn, exhibit };
}
/** Boots the real shared-core exhibit-side scripts (unmodified) in their own
* vm realm, with `window` bound to the fake-but-spec-faithful exhibit window. */
function bootSharedCoreExhibit(exhibitWindow, files) {
const ctx = vm.createContext({ window: exhibitWindow, setTimeout, clearTimeout, console, Date, Math });
vm.runInContext(source(SHARED, 'contract-core.js'), ctx);
vm.runInContext(source(SHARED, 'host-transport.js'), ctx);
for (const file of files) vm.runInContext(source(ROOT, file), ctx);
return ctx;
}
async function connectThroughRealTransport({ exhibitPath, bootFiles, createCore, rewriteOutgoing }) {
const { ngn, exhibit } = makeWindowPair(exhibitPath, { rewriteOutgoing });
const ctx = bootSharedCoreExhibit(exhibit, bootFiles);
const core = createCore(ctx);
const hostTransport = new ctx.window.XZBTHostTransport({ core });
const frame = { src: exhibit.location.href, contentWindow: exhibit };
const transport = postMessageTransport(frame, ngn);
const host = new ExhibitHost({ timeoutMs: 2000 });
await host.connect(transport, exhibit.location.href);
return { host, core, hostTransport };
}
/** Boots the real, self-contained SciFi-XZBT contract-adapter.js (unmodified)
* -- this adapter does NOT use the shared contract-core.js/host-transport.js
* at all; it is its own complete Contract implementation with its own
* window-message bridge (`_setupWindowBridge`). Minimal, real (not
* hand-rolled-protocol) bindings are supplied so `state.get` and `set` round
* trip through actual application-shaped state instead of the adapter's own
* empty {} default -- the bindings only stand in for SciFi's app.js/audio.js
* runtime, never for any part of the contract adapter itself. */
function defaultValueFor(target) {
if (target.kind === 'range') return target.min;
if (target.kind === 'selection') return target.options[0].value;
if (target.kind === 'state') {
if (target.valueType === 'boolean') return false;
if (target.valueType === 'string') return '';
return 0;
}
return null;
}
function bootSciFiAdapter(exhibitWindow) {
const ctx = vm.createContext({ window: exhibitWindow, setTimeout, clearTimeout, console, Date, Math });
vm.runInContext(source(ROOT, 'scifi/js/contract-adapter.js'), ctx);
const adapter = new ctx.window.XZBTContractAdapter({ product: 'SciFi-XZBT' });
const state = new Map();
for (const target of adapter.targets.values()) {
if (target.readable && target.kind !== 'impulse') state.set(target.id, defaultValueFor(target));
}
adapter.bindings.getState = () => Object.fromEntries(state);
adapter.bindings.setters = new Proxy({}, { get: (_t, id) => (value) => { state.set(id, value); } });
return { ctx, adapter };
}
async function connectToSciFiThroughRealTransport({ rewriteOutgoing } = {}) {
const { ngn, exhibit } = makeWindowPair('/test-fixtures/reference-exhibits/scifi/index.html', { rewriteOutgoing });
const { adapter } = bootSciFiAdapter(exhibit); // constructor wires the real _setupWindowBridge()
const frame = { src: exhibit.location.href, contentWindow: exhibit };
const transport = postMessageTransport(frame, ngn);
const host = new ExhibitHost({ timeoutMs: 2000 });
await host.connect(transport, exhibit.location.href);
return { host, adapter };
}
test('NGN hello reaches a real Contract 5.3 Museum Gallery iframe over the real postMessage transport, session establishes, describe surfaces surfaces', async (t) => {
const { host, hostTransport } = await connectThroughRealTransport({
exhibitPath: '/test-fixtures/reference-exhibits/museum-gallery/control.html',
bootFiles: ['museum-gallery/exhibit.js', 'museum-gallery/contract-adapter.js'],
createCore: (ctx) => {
const gallery = new ctx.window.MuseumGalleryExhibit.Gallery();
return ctx.window.MuseumGalleryContract.create(gallery);
}
});
t.after(() => host.disconnect());
assert.equal(host.status, 'connected');
assert.equal(hostTransport.connected, true, 'host-transport must have accepted the hello and replied');
assert.equal(host.contract.major, 5);
assert.equal(host.contract.minor, 3);
assert.equal(host.sync, 'synchronized');
assert.ok(Array.isArray(host.surfaces), 'describe() must surface Contract 5.3 presentation surfaces');
const ids = host.surfaces.map((s) => s.id).sort();
assert.deepEqual(ids, ['surface.artifact', 'surface.control', 'surface.info-wall']);
});
/* ------------------------------------------------------------------ *
* SciFi-XZBT: the real bridge, over the real transport, forward on
* Contract 5.3 -- this is the fix for the "hello timed out" regression.
* ------------------------------------------------------------------ */
test('NGN hello reaches the real SciFi-XZBT bridge over the real postMessage transport and negotiates Contract 5.3', async (t) => {
const { host, adapter } = await connectToSciFiThroughRealTransport();
t.after(() => host.disconnect());
assert.equal(host.status, 'connected', 'hello must not time out against the real SciFi bridge');
assert.equal(adapter.sessionActive, true, 'the real adapter must have accepted the hello and opened a session');
assert.equal(host.contract.major, 5);
assert.equal(host.contract.minor, 3, 'SciFi-XZBT is a maintained exhibit and must negotiate the current contract, not stay pinned to 5.2');
assert.equal(host.sessionId, adapter.sessionId);
});
test('SciFi-XZBT: describe, state.get and synchronization all complete over the real transport', async (t) => {
const { host } = await connectToSciFiThroughRealTransport();
t.after(() => host.disconnect());
assert.equal(host.sync, 'synchronized', 'connect() must run describe + state.get and reach synchronized');
assert.ok(host.catalog.length > 0, 'describe.result must report a non-empty target catalog');
assert.ok(host.capabilities.length > 0, 'describe.result must report capabilities');
assert.ok(host.values.size > 0, 'state.get must report readable persistent state');
assert.ok(host.values.has('mix.master'), 'a known SciFi target must be present in the snapshot');
});
test('SciFi-XZBT: a representative command (set on a harmless mixer target) round-trips after synchronization', async (t) => {
const { host } = await connectToSciFiThroughRealTransport();
t.after(() => host.disconnect());
assert.equal(host.sync, 'synchronized');
const before = host.stateRevision;
await host.set('mix.master', 0.42);
assert.equal(host.values.get('mix.master'), 0.42, 'the set value must be reflected in NGN\'s cache');
assert.ok(host.stateRevision > before, 'a genuine value change must advance stateRevision, proving more than handshake-only connectivity');
await assert.doesNotReject(host.invoke('sfx.comm-badge', {}), 'a zero-argument impulse must invoke cleanly through the real bridge');
});
/* ------------------------------------------------------------------ *
* Regression coverage for the exact bug class: `xzbt` must never be an
* equality gate, anywhere a maintained exhibit or the shared transport
* decides whether to accept a message.
* ------------------------------------------------------------------ */
test('Regression: no maintained bridge source gates on exact equality against the advisory `xzbt` field', () => {
// Matches `<something>.xzbt <op> '<quoted 5.x>'` (in either operand order)
// for op in {===, !==, ==, !=}. This is a static guard against exactly the
// bug class fixed here: the field is metadata (Contract §6.5), never a
// condition a bridge is allowed to branch on to accept or reject a message.
const gatePattern = /\.xzbt\s*(===|!==|==|!=)\s*['"]5\.\d['"]|['"]5\.\d['"]\s*(===|!==|==|!=)\s*[\w.]*\.xzbt\b/;
const files = [
[SHARED, 'contract-core.js'],
[SHARED, 'host-transport.js'],
[ROOT, 'scifi/js/contract-adapter.js'],
[ROOT, 'museum-gallery/contract-adapter.js'],
[ROOT, 'haunted-house/contract-adapter.js'],
[ROOT, 'aquarium/contract-adapter.js'],
[ROOT, 'planetarium/contract-adapter.js']
];
for (const [base, file] of files) {
const text = source(base, file);
assert.ok(!gatePattern.test(text), `${file} must not gate on exact equality against the advisory xzbt field`);
}
});
test('Regression: Museum Gallery (shared-core) still negotiates when NGN stamps an unexpected advisory xzbt tag', async (t) => {
const { host, hostTransport } = await connectThroughRealTransport({
exhibitPath: '/test-fixtures/reference-exhibits/museum-gallery/control.html',
bootFiles: ['museum-gallery/exhibit.js', 'museum-gallery/contract-adapter.js'],
createCore: (ctx) => {
const gallery = new ctx.window.MuseumGalleryExhibit.Gallery();
return ctx.window.MuseumGalleryContract.create(gallery);
},
// A caller stamping a nonsense advisory tag is still a well-formed,
// major-5-compatible request. If anything on the exhibit side were
// still comparing this string for equality, this would time out again.
rewriteOutgoing: (message) => ({ ...message, xzbt: 'not-a-real-version' })
});
t.after(() => host.disconnect());
assert.equal(host.status, 'connected', 'an unrecognized advisory xzbt tag must not block negotiation');
assert.equal(hostTransport.connected, true);
assert.equal(host.contract.minor, 3);
});
test('Regression: SciFi-XZBT still negotiates when NGN stamps an unexpected advisory xzbt tag', async (t) => {
const { host, adapter } = await connectToSciFiThroughRealTransport({
rewriteOutgoing: (message) => ({ ...message, xzbt: 'not-a-real-version' })
});
t.after(() => host.disconnect());
assert.equal(host.status, 'connected', 'an unrecognized advisory xzbt tag must not block the real SciFi bridge either');
assert.equal(adapter.sessionActive, true);
assert.equal(host.contract.minor, 3);
});