import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import test from 'node:test'; import { ActivationController } from '../src/runtime/activation.js'; import { Diagnostics } from '../src/runtime/diagnostics.js'; import { LibraryManager } from '../src/runtime/library.js'; import { deriveStreamState, SeededRNG } from '../src/runtime/rng.js'; import { parseAndValidateExhibit } from '../src/runtime/validator.js'; import { buildStandalone, bundleRuntime } from '../tools/build-xzbt.mjs'; const fixedSource = readFileSync(resolve('exhibits/minimal-fixed.xzbt'), 'utf8'); const randomSource = readFileSync(resolve('exhibits/minimal-random.xzbt'), 'utf8'); test('the two Phase 1 exhibits parse and validate', () => { assert.equal(parseAndValidateExhibit(fixedSource, 'minimal-fixed.xzbt').valid, true); assert.equal(parseAndValidateExhibit(randomSource, 'minimal-random.xzbt').valid, true); }); test('malformed JSON, unsupported versions, and invalid metadata fail predictably', () => { assert.equal(parseAndValidateExhibit('{', 'broken.xzbt').errors[0].code, 'ERR_SCHEMA_VALIDATION'); const unsupported = JSON.stringify({ xzbt: '9', meta: { id: 'valid-id', name: 'Name' } }); assert.ok(parseAndValidateExhibit(unsupported).errors.some(({ code }) => code === 'ERR_UNSUPPORTED_VERSION')); const invalidMeta = JSON.stringify({ xzbt: '0.1', meta: { id: 'Not Valid', name: '' } }); const codes = parseAndValidateExhibit(invalidMeta).errors.map(({ code }) => code); assert.ok(codes.includes('ERR_INVALID_ID')); assert.ok(codes.includes('ERR_SCHEMA_VALIDATION')); }); test('production PRNG preserves the frozen GC4 vectors', () => { assert.deepEqual(deriveStreamState(42, 'scenario', 'storm:1'), [471422921, 3327820418, 120898024, 15893603]); const stream = new SeededRNG(42).stream('scenario', 'storm:1'); assert.deepEqual([stream.nextUint32(), stream.nextUint32(), stream.nextUint32(), stream.nextUint32()], [4101533927, 4149236977, 2113172072, 1672809915]); }); test('library imports, ignores identical bytes, persists, and restores records', async () => { const saved = []; const persistence = { async saveExhibit(record) { saved.push(record); } }; const library = new LibraryManager({ persistence, diagnostics: new Diagnostics() }); assert.equal((await library.importSource(fixedSource, 'minimal-fixed.xzbt')).status, 'imported'); assert.equal((await library.importSource(fixedSource, 'minimal-fixed.xzbt')).status, 'identical'); assert.equal(saved.length, 1); const restored = new LibraryManager({ diagnostics: new Diagnostics() }); restored.restore(saved); assert.equal(restored.get('minimal-fixed').document.meta.name, 'Minimal Fixed'); }); test('activation prepares before teardown and recovers the previous exhibit on failure', async () => { const events = []; let shouldFail = false; const make = (record) => ({ rng: new SeededRNG(42), async activate() { events.push(`activate:${record.id}`); if (shouldFail && record.id === 'next') throw new Error('injected'); }, async deactivate() { events.push(`deactivate:${record.id}`); }, async dispose() { events.push(`dispose:${record.id}`); } }); const controller = new ActivationController({ diagnostics: new Diagnostics(), performanceFactory: make }); const record = (id) => ({ id, document: { meta: { id, name: id }, runtime: { seed: 42 } } }); assert.equal(await controller.activate(record('previous')), true); shouldFail = true; assert.equal(await controller.activate(record('next')), false); assert.equal(controller.current.record.id, 'previous'); assert.deepEqual(events, ['activate:previous', 'deactivate:previous', 'dispose:previous', 'activate:next', 'dispose:next', 'activate:previous']); }); test('two clean standalone builds are byte-identical and self-contained', () => { const directory = mkdtempSync(join(tmpdir(), 'xzbt-phase1-')); const first = join(directory, 'first.html'); const second = join(directory, 'second.html'); buildStandalone(first); buildStandalone(second); const firstBytes = readFileSync(first); const secondBytes = readFileSync(second); assert.deepEqual(firstBytes, secondBytes); const html = firstBytes.toString('utf8'); assert.doesNotMatch(html, /<(script|link)[^>]+(?:src|href)=/i); // A module import *statement*, not the word in a comment: the bundler strips // the former, and prose about import-stage validation is not a leak. assert.doesNotMatch(html, /^[ \t]*import[ \t][^;(\n]*from[ \t]*['"]/m); // C14: Also catch side-effect imports (import './chunk.js';). assert.doesNotMatch(html, /^[ \t]*import[ \t]*['"]/m); assert.doesNotMatch(html, /^[ \t]*export[ \t]/m); assert.equal(createHash('sha256').update(firstBytes).digest('hex'), createHash('sha256').update(secondBytes).digest('hex')); const script = html.match(/