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>
40 lines
2.9 KiB
JavaScript
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);
|
|
});
|