Files
XZBT/test/phase1-runtime.test.mjs
T

84 lines
4.6 KiB
JavaScript

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 } 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);
assert.doesNotMatch(html, /\bimport\s+[^;(]/);
assert.equal(createHash('sha256').update(firstBytes).digest('hex'), createHash('sha256').update(secondBytes).digest('hex'));
const script = html.match(/<script>([\s\S]*)<\/script>/)[1];
assert.doesNotThrow(() => new Function(script));
});