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 `.xzbt ''` (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); });