generated from Labyricorn/labyricorn-project-template
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.
77 lines
4.4 KiB
JavaScript
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' });
|
|
}
|
|
});
|