import test from 'node:test'; import assert from 'node:assert/strict'; import { VisualEngine } from '../src/runtime/visual-engine.js'; import { ResolutionEngine } from '../src/runtime/resolution.js'; import { SeededRNG } from '../src/runtime/rng.js'; import { ActionExecutor } from '../src/runtime/actions.js'; import { VisualDiagnosticCadence } from '../src/runtime/visual-diagnostics.js'; import { VisualInstance } from '../src/runtime/visual-lifecycle.js'; import { applyVisualEffect, renderVisualEffects } from '../src/runtime/visual-effects.js'; import { validateExhibit } from '../src/runtime/validator.js'; const point = { type: 'point', position: { x: 10, y: 20 }, style: { fill: '#ffffff', pointSize: 3 } }; const track = (target, a = 0, b = 1, extra = {}) => ({ target, points: [{ at: '0ms', value: a }, { at: '1s', value: b }], ...extra }); function setup(visuals, extra = {}) { const document = { xzbt: '0.1', meta: { id: 'execution-test', name: 'Execution test' }, runtime: { seed: 42 }, visuals: { scene: { coordinateSpace: 'virtual', width: 100, height: 100 }, ...visuals }, ...extra }; const rng = new SeededRNG(42), resolution = new ResolutionEngine(document, rng); const engine = new VisualEngine(document, { rng, resolution }); const tick = ms => { resolution.advance(ms); engine.advance(ms); }; return { document, engine, resolution, rng, tick, frame: () => engine.planFrame({ width: 100, height: 100 }) }; } const close = (actual, expected) => assert.ok(Math.abs(actual - expected) < 1e-8, `${actual} != ${expected}`); test('19.7.1 shared curves and modes drive actual graphic geometry', () => { for (const interpolation of ['step', 'linear', 'smooth', 'exponential']) for (const mode of ['absolute', 'offset', 'scale']) { const run = setup({ systems: { s: { type: 'graphic', content: { p: point }, automation: [track('p.position.x', 2, 8, { interpolation, mode })] } } }); run.tick(500); const value = interpolation === 'step' ? 2 : interpolation === 'exponential' ? 4 : 5; close(run.frame().layers[0].nodes[0].center[0], mode === 'offset' ? 10 + value : mode === 'scale' ? 10 * value : value); } }); test('19.7.2 repeat and finite ping-pong loops hold the correct endpoint', () => { for (const mode of ['repeat', 'ping-pong']) { const run = setup({ camera: { zoom: 1 }, automation: [track('camera.zoom', 1, 3, { loop: { mode, count: 2 } })], systems: {} }); run.tick(1500); close(run.frame().camera.zoom, 2); run.tick(3500); close(run.frame().camera.zoom, mode === 'repeat' ? 3 : 1); } }); test('4e rendered graphic motion, nested repeater motion, and morph reach geometry', () => { const run = setup({ systems: { s: { type: 'graphic', content: { p: { ...point, behaviors: [{ type: 'drift', velocity: { x: 10, y: 0 } }] }, a: { type: 'polyline', points: [{ x: 0, y: 0 }, { x: 10, y: 0 }], behaviors: [{ type: 'morph', to: 'b', duration: '1s' }] }, b: { type: 'polyline', visible: false, points: [{ x: 0, y: 10 }, { x: 20, y: 10 }] } } }, r: { type: 'repeater', count: 1, repeat: { ...point, behaviors: [{ type: 'drift', velocity: { x: 20, y: 0 } }] } } } }); run.tick(500); const nodes = run.frame().layers[0].nodes; close(nodes[0].center[0], 15); close(nodes[1].subpaths[0].start[1], 5); close(nodes[2].children[0].center[0], 20); }); test('19.7.4 behavior and automation collision is rejected at import', () => { const run = setup({ systems: {} }); run.document.visuals.systems.s = { type: 'graphic', content: { p: { ...point, behaviors: [{ type: 'drift', velocity: { x: 1 } }] } }, automation: [track('p.position.x')] }; assert.ok(validateExhibit(run.document).errors.some(error => error.code === 'ERR_AUTOMATION_CONFLICT')); }); test('19.7.4 cross-scope, undeclared, and out-of-registry targets raise documented errors', () => { const doc = visuals => ({ xzbt: '0.1', meta: { id: 't', name: 'T' }, runtime: { seed: 42 }, visuals: { scene: { coordinateSpace: 'virtual', width: 100, height: 100 }, ...visuals } }); // Cross-scope target from exhibit to system: ERR_INVALID_REFERENCE const r1 = doc({ systems: { s: { type: 'graphic', content: { p: point } } }, automation: [track('systems.s.p.position.x')] }); assert.ok(validateExhibit(r1).errors.some(e => e.code === 'ERR_INVALID_REFERENCE')); // Cross-scope target from system to camera: ERR_INVALID_REFERENCE const r2 = doc({ systems: { s: { type: 'graphic', content: { p: point }, automation: [track('camera.zoom')] } } }); assert.ok(validateExhibit(r2).errors.some(e => e.code === 'ERR_INVALID_REFERENCE')); // Undeclared layer in exhibit scope: ERR_INVALID_REFERENCE const r3 = doc({ layers: { main: {} }, systems: { s: { type: 'graphic', layer: 'main', content: { p: point } } }, automation: [track('layers.missing.opacity')] }); assert.ok(validateExhibit(r3).errors.some(e => e.code === 'ERR_INVALID_REFERENCE')); // Out-of-range effect index: ERR_INVALID_REFERENCE const r4 = doc({ effects: [{ type: 'blur', radius: 4 }], systems: { s: { type: 'graphic', content: { p: point } } }, automation: [track('effects[5].radius')] }); assert.ok(validateExhibit(r4).errors.some(e => e.code === 'ERR_INVALID_REFERENCE')); // Out-of-registry target (scene.background is a color, not automatable): ERR_UNSUPPORTED_TARGET const r5 = doc({ systems: { s: { type: 'graphic', content: { p: point } } }, automation: [track('scene.background')] }); assert.ok(validateExhibit(r5).errors.some(e => e.code === 'ERR_UNSUPPORTED_TARGET')); }); test('19.7.3 system-scope track at is measured from instantiation (two spawns offset by 4s)', () => { const visuals = { systems: { s: { type: 'graphic', lifecycle: 'spawned', content: { p: { ...point, position: { x: 0, y: 0 } } }, automation: [track('p.position.x', 0, 100)] } } }; const run = setup(visuals); const a = run.engine.spawn('s'); run.tick(4000); const b = run.engine.spawn('s'); // At t=4000ms, b has age 0. Advance 500ms further (halfway through the 1s track). run.tick(500); // b's age is 500ms, so its position.x should be 50. close(b.system.objects[0].position.x, 50); }); test('19.7.7 override masks moving visual automation and releases to its current value', () => { const run = setup({ automation: [track('camera.zoom', 1, 3)], systems: {} }); const actions = new ActionExecutor(run.resolution); const [result] = actions.execute([{ type: 'override', target: 'visuals.camera.zoom', value: 5, scope: 'duration', duration: '10s', release: '0ms' }]); run.tick(500); close(run.frame().camera.zoom, 5); run.resolution.overrides.beginRelease(result.instanceId); run.tick(100); close(run.frame().camera.zoom, 2.2); }); test('19.7.8-10 spawned templates activate next tick, fade, dispose and reproduce', () => { const visuals = { systems: { s: { type: 'graphic', lifecycle: 'spawned', spawn: { release: '1s' }, content: { p: { ...point, position: { x: { random: { min: 0, max: 100 } }, y: 20 }, style: { opacity: 0.4 } } } } } }; const first = setup(visuals), second = setup(visuals); const a = first.engine.spawn('s'), b = second.engine.spawn('s'); assert.equal(first.frame().layers[0].nodes.length, 0); first.tick(10); second.tick(10); assert.deepEqual(first.frame().layers, second.frame().layers); first.engine.remove(a.id); first.tick(500); close(first.frame().layers[0].nodes[0].alpha, 0.2); first.tick(500); assert.equal(a.state, 'DISPOSED'); first.engine.remove(a.id); assert.equal(first.engine.instances.size, 0); assert.equal(b.state, 'ACTIVE'); }); test('19.7.9 every lifecycle edge is enforced, zero release lasts a tick', () => { const edges = { CREATED: ['ACTIVE', 'FINISHED', 'FAILED'], ACTIVE: ['RELEASING', 'FINISHED', 'FAILED'], RELEASING: ['FINISHED', 'FAILED'], FINISHED: ['DISPOSED'], DISPOSED: [], FAILED: [] }; for (const [from, allowed] of Object.entries(edges)) for (const to of Object.keys(edges)) { const instance = new VisualInstance({ system: { objects: [] } }); instance.state = from; if (allowed.includes(to)) assert.doesNotThrow(() => instance.transition(to)); else assert.throws(() => instance.transition(to)); } const run = setup({ systems: { s: { type: 'graphic', lifecycle: 'spawned', content: { p: point } } } }); const instance = run.engine.spawn('s'); run.tick(10); run.engine.remove(instance.id); assert.equal(instance.state, 'RELEASING'); assert.equal(instance.factor, 0); run.tick(10); assert.equal(instance.state, 'DISPOSED'); }); test('19.7.11 ownership, originating scenario and forced cleanup are separate', () => { const run = setup({ systems: { s: { type: 'graphic', lifecycle: 'spawned', spawn: { ownership: 'persistent', cancelWithScenario: false, release: '10s' }, content: { p: point } } } }); const inherited = run.engine.spawn('s', { owner: 'scenario' }); const persistent = run.engine.spawn('s', { owner: 'scenario', ownership: 'persistent' }); run.tick(10); run.engine.cleanup('scenario'); assert.equal(inherited.state, 'RELEASING'); assert.equal(persistent.state, 'ACTIVE'); run.tick(5100); assert.equal(inherited.state, 'DISPOSED'); assert.ok(run.engine.cadence.raised.some(w => w.code === 'WARN_CLEANUP_FORCED')); }); test('19.7.6/12 live spawn and automation budgets refuse atomically without consuming ordinal', () => { for (const manyTracks of [false, true]) { const content = Object.fromEntries(Array.from({ length: 3 }, (_, i) => [`p${i}`, point])); const run = setup({ systems: { s: { type: 'graphic', lifecycle: 'spawned', content, automation: manyTracks ? [0, 1, 2].map(i => track(`p${i}.position.x`)) : [] } } }); const count = manyTracks ? 42 : 64; const instances = Array.from({ length: count }, () => run.engine.spawn('s')); assert.ok(instances.every(Boolean)); assert.equal(run.engine.spawn('s'), null); assert.equal(run.engine.spawn('s'), null); assert.equal(run.engine.spawnOrdinals.get('s'), count); assert.equal(run.engine.cadence.raised.length, 1); run.engine.remove(instances[0].id); run.tick(10); assert.equal(run.engine.spawn('s').id, `instances.s#${count}`); } }); test('spawn/remove actions use PRD target/with shape and instance addresses', () => { const run = setup({ systems: { s: { type: 'graphic', lifecycle: 'spawned', spawn: { inputs: { x: { type: 'number', default: 1 } } }, content: { p: { ...point, position: { x: { ref: 'inputs.x' }, y: 20 } } } } } }); const actions = new ActionExecutor(run.resolution); actions.visual = run.engine; const [result] = actions.execute([{ type: 'spawn', target: 'visuals.systems.s', with: { x: 77 } }]); run.tick(10); close(run.frame().layers[0].nodes[0].center[0], 77); actions.execute([{ type: 'remove', target: result.instanceId }]); run.tick(10); assert.equal(run.frame().layers[0].nodes.length, 0); }); test('19.7.15 each effect changes known pixels, preserves alpha and respects order', () => { const source = new Uint8ClampedArray([255, 255, 255, 255, 60, 40, 20, 255, 0, 0, 0, 255]); for (const effect of [ { type: 'fade', amount: 0.5 }, { type: 'vignette', radius: 0, amount: 1, softness: 0.1 }, { type: 'scanlines', amount: 1 }, { type: 'grain', amount: 1 }, { type: 'color-adjust', brightness: 0.5 }, { type: 'blur', radius: 1 }, { type: 'bloom', radius: 1, intensity: 1 } ]) { const actual = applyVisualEffect(source, 3, 1, effect); assert.notDeepEqual(actual, source, effect.type); assert.equal(actual[3], 255); } const bloom = data => applyVisualEffect(data, 3, 1, { type: 'bloom', radius: 1 }); const adjust = data => applyVisualEffect(data, 3, 1, { type: 'color-adjust', brightness: 0.4 }); assert.notDeepEqual(bloom(adjust(source)), adjust(bloom(source))); assert.deepEqual([...applyVisualEffect(new Uint8ClampedArray([0, 0, 0, 255]), 1, 1, { type: 'fade', color: '#804020', amount: 1 })], [128, 64, 32, 255]); }); test('19.7.16-17 effects do not consume procedural randomness and unavailable passes warn', () => { const run = setup({ effects: [{ type: 'grain', enabled: false }, { type: 'blur', radius: 2 }], systems: {} }); const before = run.rng.stream('visual', 'later').nextFloat(); for (let i = 0; i < 10; i++) run.frame(); const reference = new SeededRNG(42); close(before, reference.stream('visual', 'later').nextFloat()); close(run.rng.stream('visual', 'later').nextFloat(), reference.stream('visual', 'later').nextFloat()); assert.equal(run.frame().effects.length, 1); renderVisualEffects({}, run.frame(), (effect, message) => run.engine.cadence.once('WARN_VISUAL_APPROXIMATION', `effect:${effect.index}`, message)); renderVisualEffects({}, run.frame(), (effect, message) => run.engine.cadence.once('WARN_VISUAL_APPROXIMATION', `effect:${effect.index}`, message)); assert.equal(run.engine.cadence.raised.length, 1); }); test('19.7.18 diagnostic cadence counts ticks, not refusals, and recovers promptly', () => { const cadence = new VisualDiagnosticCadence(); for (let tick = 0; tick <= 120; tick++) { for (let i = 0; i < 4; i++) cadence.shed('WARN_VISUAL_CEILING', 'test', 'test', tick * 1000 / 60); cadence.endTick(); } assert.equal(cadence.keys.get('WARN_VISUAL_CEILING test').consecutive, 121); assert.ok(cadence.raised.at(-1).sustained); cadence.endTick(); cadence.shed('WARN_VISUAL_CEILING', 'test', 'test', 2010); assert.equal(cadence.raised.at(-1).at, 2010); }); test('19.7.18 particle overload evicts oldest from largest populations; declared systems survive', () => { const run = setup({ systems: Object.fromEntries([0, 1, 2].map(i => [`s${i}`, { type: 'particles', capacity: 4096, count: 4096, render: { type: 'point' } }])) }); assert.equal(run.engine.systems.length, 3); assert.equal(run.engine.systems.reduce((sum, system) => sum + system.procedural.items.length, 0), 8192); assert.ok(run.engine.systems.every(system => system.procedural.items[0].ordinal > 0)); }); test('19.7.18 emitted items, trail histories, and link tails obey aggregate ceilings', () => { const run = setup({ systems: Object.fromEntries([0, 1, 2, 3, 4].map(i => [`s${i}`, { type: 'emitter', capacity: 512, count: 512, emit: { type: 'point' } }])) }); const systems = run.engine.systems.map(system => system.procedural); assert.equal(systems.reduce((n, system) => n + system.items.length, 0), 2048); for (const system of systems) { for (const item of system.items) item.trail = Array.from({ length: 17 }, (_, i) => ({ t: i, x: 0, y: 0 })); system.linkPairs = () => Array.from({ length: 1024 }, (_, ordinal) => ({ ordinal })); } run.engine.enforcePopulations(); assert.equal(systems.reduce((n, system) => n + system.items.reduce((m, item) => m + item.trail.length, 0), 0), 32768); assert.equal(run.engine.systems.reduce((n, system) => n + system.links.length, 0), 4096); assert.equal(run.engine.systems.at(-1).links.length, 0); }); test('19.7.18 fields evaluate nearest items first and remaining forces become zero', () => { const run = setup({ fields: Object.fromEntries([0, 1, 2, 3].map(i => [`f${i}`, { type: 'directional', strength: 1, direction: 0 }])), systems: { far: { type: 'particles', count: 4096, capacity: 4096, z: 100, fields: ['f0', 'f1', 'f2', 'f3'], render: { type: 'point' }, behaviors: [{ type: 'field-follow', field: 'f0' }] }, near: { type: 'particles', count: 4096, capacity: 4096, z: 0, fields: ['f0', 'f1', 'f2', 'f3'], render: { type: 'point' }, behaviors: [{ type: 'field-follow', field: 'f0' }] } } }); run.tick(1000 / 60); assert.equal(run.engine.fieldSet.evaluations, 32768); assert.equal(run.engine.fieldSet.refused, true); assert.ok(run.engine.systems[1].procedural.items.every(item => item.vx > 0)); assert.equal(run.engine.systems[0].procedural.items.at(-1).vx, 0); }); test('19.7.19 resolved counts cannot enlarge pools or repeater limits', () => { assert.throws(() => setup({ systems: { s: { type: 'repeater', count: { random: { min: 1025, max: 1026 } }, repeat: point } } }), error => error.code === 'ERR_VISUAL_LIMIT_EXCEEDED'); }); test('runtime failure reclaims a spawned instance without stopping another system', () => { const run = setup({ systems: { s: { type: 'particles', lifecycle: 'spawned', count: 1, render: point }, healthy: { type: 'graphic', content: { p: point } } } }); const instance = run.engine.spawn('s'); instance.system.procedural.advance = () => { throw new Error('injected failure'); }; assert.doesNotThrow(() => run.tick(10)); assert.equal(instance.state, 'FAILED'); assert.equal(instance.system.procedural.items.length, 0); assert.equal(run.engine.instances.size, 0); assert.equal(run.frame().layers[0].nodes.length, 1); }); test('viewport default camera center still participates in automation and bindings', () => { const run = setup({ scene: { coordinateSpace: 'viewport' }, automation: [track('camera.x', 0, 100)], systems: {} }); run.tick(250); close(run.frame().camera.x, 25); }); test('19.7.3 exhibit automation samples point values in one scope stream', () => { const value = { random: { min: 1, max: 2 } }; const run = setup({ automation: [track('camera.zoom', value, value), track('camera.rotation', value, value)], systems: {} }); const stream = new SeededRNG(42).stream('visual', 'automation'); const expected = Array.from({ length: 4 }, () => 1 + stream.nextFloat()); assert.deepEqual([...run.resolution.automation.values()].flatMap(track => track.points.map(point => point.value)), expected); });