igneum/tools/scene/feed-contract.test.mjs
igneum-labs 032e0e591e Chain scene 2.0.3: /live and the home fold paint on every push whatever the document's visibility says; the renderer, its palette and its feed contract move to one shared folder scene/ with byte-equal copies checked by the gate (7 October 2026, 15:2x UK)
The blank /live (the project lead, 14:3x UK): IgneumDag 2.0.2 painted only from a requestAnimationFrame loop gated on document.hidden and
the IntersectionObserver, so a page that loaded with document.hidden true (the desktop pane, a background tab) and whose embedder
never fired visibilitychange took every push, reported live and never drew a frame. 2.0.3 paints the current picture synchronously
on push, size and theme change; the motion loop alone waits for a visible document and an intersecting canvas. Confirmed headless
on build-2 against the live feed: hidden document 0 lit pixels before, 110,007 after; the never-intersecting observer repaints
on every push. Known-failed test tools/scene/paint-test.cjs (the 2.0.2 renderer draws nothing in the same world).

The second 2.0.3 change: the phone rule (30 s window, four lanes) keys on the viewport width, not the canvas width; a 640 px hero
on a 1,440 px laptop was rendering as a phone while the app's card beside it was not.

scene/ is the one source: live-dag.js, proof-core.js, tokens.css (the fourteen palette tokens, the brand package's values, dark and
light), feed-contract.md and .json (one JSON shape for the observer's /api/live and the app's api/live), a recorded reply as the
fixture. tools/scene/sync.mjs writes the copies and the scene-tokens block into site.css and app.css; --check is the gate line
(byte-equal scripts, an equal block, the names defined nowhere else, a print block excepted), --self-test fails five known cases
first. The site's token definitions move out of the package's :root line into the block; no value changes on the site.
tools/scene/feed-contract.mjs validates a reply against the key lists; its test refuses a miner rewritten to "you", a float now,
a stray key. Three new lines in tools/ci/pre-push.sh. The app side (branch scene-parity, for 0.3.21) takes the same folder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 13:29:38 +00:00

40 lines
2.9 KiB
JavaScript

// The live feed contract: the recorded observer reply validates; known-bad shapes are refused, each for its own reason.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { validate, CONTRACT } from './feed-contract.mjs';
const fixture = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scene', 'fixtures', 'live-2026-10-07.json'), 'utf8'));
const edit = fn => { const r = JSON.parse(JSON.stringify(fixture)); fn(r); return r; };
test('the recorded /api/live reply validates (905 blocks, 15 miners, 40 checkpoints)', () => {
assert.deepEqual(validate(fixture), []);
assert.equal(fixture.blocks.length, 905); assert.equal(fixture.miners.length, 15); assert.equal(fixture.finality.checkpoints.length, 40);
});
test('the app\'s documented extensions are accepted', () => {
const r = edit(r => { r.partial = true; r.state.source = 'observer'; r.state.you_blocks = 2; r.state.node = { peers: 3 }; });
assert.deepEqual(validate(r), []);
});
test('known-bad: a block whose miner is the word "you" (the 0.3.19 app rewrote the field) is refused', () => {
const p = validate(edit(r => { r.blocks[0].miner = 'you'; }));
assert.match(p.join('\n'), /blocks\[0\]\.miner is not an 8-hex vote key id \("you"\)/);
});
test('known-bad: now as a float of seconds (the 0.3.19 node fallback) is refused', () => {
assert.match(validate(edit(r => { r.now = 1791301670.5; })).join('\n'), /now is not an ISO 8601 string/);
});
test('known-bad: a missing proving object, a missing block key, an unknown key', () => {
assert.match(validate(edit(r => { delete r.proving; })).join('\n'), /reply lacks proving/);
assert.match(validate(edit(r => { delete r.blocks[3].rx; })).join('\n'), /blocks\[3\] lacks rx/);
assert.match(validate(edit(r => { r.blocks[3].vote_key_hash = 'abc'; })).join('\n'), /blocks\[3\] carries an unknown key vote_key_hash/);
assert.match(validate(edit(r => { r.miners[0] = { id: r.miners[0].id, vote_key_hash: 'x', blocks_10m: 1 }; })).join('\n'), /miners\[0\] lacks blocks/);
});
test('known-bad: a colour, a shard state and a checkpoint state outside the enums', () => {
assert.match(validate(edit(r => { r.blocks[0].color = 'winning'; })).join('\n'), /color is not one of blue, red, pending/);
assert.match(validate(edit(r => { r.blocks[0].shards = [{ i: 0, n: 1, state: 'jackpot', prover: null, lag: null, payout: null, pgas: null }]; })).join('\n'), /state is not one of planned, proving, verified, paid/);
assert.match(validate(edit(r => { r.finality.checkpoints[0].state = 'maybe'; })).join('\n'), /is not pending or locked/);
});
test('the contract file lists what the Rust test reads', () => {
for (const k of ['top', 'state', 'block', 'miner', 'finality', 'checkpoint', 'shard', 'proving_unsupported']) assert(Array.isArray(CONTRACT[k]) && CONTRACT[k].length > 0, k);
});