Files
XZBT-NGN/tests/connection.test.js
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

77 lines
4.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { ExhibitConnection, exhibitURL } from '../src/connection.js';
import { validateCatalog } from '../src/validation.js';
const base = 'http://127.0.0.1:4173/';
test('connection accepts served paths and same-origin URLs, rejects unsupported locations', () => {
assert.equal(exhibitURL('/public/example/index.html', base), base + 'public/example/index.html');
assert.equal(exhibitURL(' public/example/ ', base), base + 'public/example/');
for (const value of ['', ' ', 'file:///C:/exhibit.html', 'https://other.example/', 'http://127.0.0.1:4174/', 'javascript:alert(1)', 'http://user:[email protected]:4173/']) {
assert.throws(() => exhibitURL(value, base));
}
});
function setup(t, loadTimeoutMs = 1000) {
const frames = [], connections = [], errors = [];
const host = { disconnect() {}, async connect(frame) { connections.push(frame); }, report(e) { errors.push(e); } };
const connection = new ExhibitConnection({ host, base, loadTimeoutMs, transport: frame => frame,
createFrame() {
const frame = { handlers: {}, addEventListener(type, fn) { this.handlers[type] = fn; }, remove() { this.removed = true; } };
frames.push(frame); return frame;
}
});
t.after(() => connection.disconnect());
return { connection, frames, connections, errors };
}
test('switch and disconnect cancel old frame loads; invalid URL preserves active frame', async t => {
const { connection, frames, connections } = setup(t);
connection.load('/public/one/'); connection.load('/public/two/');
frames[0].handlers.load(); assert.equal(connections.length, 0);
frames[1].handlers.load(); assert.equal(connections.length, 1);
assert.throws(() => connection.load('https://other.example/'));
assert.equal(connection.frame, frames[1]);
connection.disconnect(); frames[1].handlers.load();
assert.equal(connections.length, 1); assert.equal(connection.frame, null);
assert.ok(frames.every(f => f.removed));
});
test('reconnect negotiates existing frame and reloads remembered URL after disconnect', async t => {
const { connection, frames, connections } = setup(t);
connection.load('/public/one/'); frames[0].handlers.load();
await connection.reconnect(); assert.equal(connections.length, 2); assert.equal(frames.length, 1);
connection.disconnect(); connection.reconnect();
assert.equal(frames.length, 2); assert.equal(frames[1].src, base + 'public/one/');
frames[1].handlers.load(); assert.equal(connections.length, 3);
});
test('stalled document load ends with recoverable error', async t => {
const { connection, frames, errors } = setup(t, 5);
connection.load('/public/stalled/');
await new Promise(resolve => setTimeout(resolve, 20));
assert.equal(connection.loading, false); assert.equal(connection.frame, null);
assert.equal(errors[0].code, 'LOAD_TIMEOUT');
connection.reconnect(); assert.equal(frames.length, 2);
});
test('redirected control document supplies the final same-origin base; foreign navigation is refused', t => {
const { connection, frames, connections, errors } = setup(t);
connection.load('/public/entry.html');
frames[0].contentWindow = { location: { href: base + 'public/gallery/control.html' } };
frames[0].handlers.load();
assert.equal(connection.url, base + 'public/gallery/control.html');
assert.equal(connections.length, 1);
frames[0].contentWindow.location.href = 'https://other.example/';
frames[0].handlers.load();
assert.equal(connection.frame, null); assert.equal(errors[0].code, 'INVALID_LOCATION');
assert.equal(connections.length, 1);
});
test('optional descriptor metadata is permissive when omitted and typed when supplied', () => {
const message = { exhibit: {}, contract: { major: 5 }, registryRevision: 0, stateRevision: 0, capabilities: [],
targets: [{ id: 'sample.enabled', kind: 'state', readable: true, writable: true, valueType: 'boolean', requires: [] }] };
validateCatalog(message);
for (const metadata of [{ restorable: false, category: '' }, { restorable: true, category: 'Custom category' }]) {
validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] });
}
for (const metadata of [{ restorable: 'false' }, { restorable: null }, { restorable: 1 }, { category: null }, { category: [] }, { category: false }]) {
assert.throws(() => validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] }), { code: 'INVALID_MESSAGE' });
}
});