import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import vm from 'node:vm'; import { validateReportedValue } from '../src/validation.js'; function setup() { const context = vm.createContext({ window: {} }); for (const file of ['shared/contract-core.js', 'haunted-house/exhibit.js', 'haunted-house/contract-adapter.js']) { vm.runInContext(readFileSync(new URL(`../test-fixtures/reference-exhibits/${file}`, import.meta.url), 'utf8'), context); } const exhibit = new context.window.HauntedHouseExhibit({ canvas: { getContext: () => ({ setTransform() {} }), getBoundingClientRect: () => ({ width: 100, height: 100 }) }, announcer: { flash() {} } }); const core = context.window.HauntedHouseContract.create(exhibit, { sync() {} }); const descriptor = core.catalog.get('room.haunt-level'); assert.deepEqual([descriptor.min, descriptor.max, descriptor.step], [0, 1, 0.01]); return { exhibit, core, descriptor }; } test('Haunted House reports bounded hundredths while preserving continuous drift', () => { const { exhibit, core, descriptor } = setup(); for (const [raw, expected] of [[-0.1, 0], [0, 0], [0.0049, 0], [0.0051, 0.01], [0.303, 0.3], [0.999, 1], [1, 1], [1.1, 1]]) { exhibit.house.hauntLevel = raw; assert.equal(core.readValue(descriptor.id), expected); assert.equal(exhibit.house.hauntLevel, raw, 'reading must not quantize the simulation'); } for (const room of ['foyer', 'library', 'cellar', 'attic', 'nursery']) { exhibit.setRoom(room); for (const spirit of ['restless', 'vengeful', 'mourning', 'mischievous']) { exhibit.setSpirit(spirit); exhibit.house.hauntLevel = 0.303; for (let i = 0; i < 600; i++) { exhibit.house.step(1 / 60); validateReportedValue(descriptor, core.readValue(descriptor.id)); } assert.notEqual(exhibit.house.hauntLevel, 0.303, 'simulation must keep drifting'); } } }); test('Haunted House snapshots and mutation events stay valid through Manifest, drift and Calm', () => { const { exhibit, core, descriptor } = setup(); core.handleHello({ supportedContractMajors: [5] }); const checkSnapshot = () => { const snapshot = core.stateSnapshot(); for (const target of core.catalog.descriptors().filter(t => t.readable)) validateReportedValue(target, snapshot.values[target.id]); return snapshot.values[descriptor.id]; }; assert.equal(core.invokeAction('action.manifest', {}, 'host').ok, true); assert.equal(exhibit.house.hauntLevel, 1); assert.equal(checkSnapshot(), 1); for (let i = 0; i < 600; i++) { exhibit.house.step(1 / 60); checkSnapshot(); } for (const value of [0, 0.01, 0.29, 0.5, 0.99, 1]) { assert.equal(core.applyMutation(descriptor.id, value, 'ui').ok, true); assert.equal(checkSnapshot(), value); } for (const event of core.eventLog().filter(e => e.type === 'state.changed')) validateReportedValue(core.catalog.get(event.target), event.value); assert.ok(core.eventLog().some(e => e.type === 'action.executed' && e.target === 'action.manifest')); assert.equal(core.invokeAction('action.calm', {}, 'host').ok, true); assert.equal(checkSnapshot(), 0); });