# Conflicts: # CLAUDE.md # docs/plans/counter-asic-3-status.md # docs/plans/release-0.3.21.md # docs/plans/release-0.3.22.md # tools/ci/merge-to-master.sh # tools/ci/pre-push.sh # tools/ci/red-watch.mjs
70 lines
7.1 KiB
JavaScript
70 lines
7.1 KiB
JavaScript
// node --test tools/scene/legend.test.mjs (key-22, the founder's screenshot of the app's chain key against /live, 7 October 2026)
|
||
// The key is ONE export: IgneumDag.legend({ mine }) in scene/live-dag.js (entries, order, token names, shapes); the app's
|
||
// chain card and the site's /live key render from it, so they cannot drift. This test fails when either key's entries,
|
||
// order or tokens differ from the export, and when a colour the scene draws on blocks has no key entry.
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import { execSync } from 'node:child_process';
|
||
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../..');
|
||
const src = readFileSync(join(root, 'scene/live-dag.js'), 'utf8');
|
||
const win = { document: { hidden: false, addEventListener() {}, createElement: () => ({ style: {} }) }, addEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {} }), getComputedStyle: () => ({ getPropertyValue: () => '' }), devicePixelRatio: 1, innerWidth: 1280 };
|
||
new Function('window', 'document', src)(win, win.document);
|
||
const Dag = win.IgneumDag;
|
||
const SITE_ORDER = ['Pending', 'Included', 'Excluded', 'Selected chain', 'Proven', 'Locked checkpoint'];
|
||
// the entries a page's markup carries today, from its literal spans (a hard-coded key) or none (a rendered key)
|
||
const literal = (html, tag) => [...html.matchAll(new RegExp('<span><i class="' + tag + '[^>]*></i>([^<]+)</span>', 'g'))].map((m) => m[1].trim());
|
||
|
||
test('the export: Pending first, Included, Excluded, Selected chain, Proven, Locked checkpoint; the app adds Your blocks first; every entry names its token and shape (known-failed on 0.3.22)', () => {
|
||
assert.ok(Dag && typeof Dag.legend === 'function', 'IgneumDag.legend exists');
|
||
const site = Dag.legend({ mine: false }), app = Dag.legend({ mine: true });
|
||
assert.deepEqual(site.map((e) => e.label), SITE_ORDER);
|
||
assert.deepEqual(app.map((e) => e.label), ['Your blocks'].concat(SITE_ORDER));
|
||
for (const e of app) { assert.match(e.token, /^--[a-z-]+$/, e.label + ' token'); assert.ok(['square', 'circle', 'tick', 'ringed'].includes(e.shape), e.label + ' shape'); assert.ok(e.id); }
|
||
assert.equal(app[0].token, '--molten'); assert.equal(app[0].shape, 'ringed');
|
||
assert.equal(site.find((e) => e.label === 'Included').token, '--included', 'the key draws Included in the colour the scene draws it');
|
||
assert.equal(site.find((e) => e.label === 'Excluded').token, '--excluded');
|
||
assert.equal(site.find((e) => e.label === 'Locked checkpoint').shape, 'circle');
|
||
assert.equal(site.find((e) => e.label === 'Proven').shape, 'tick');
|
||
assert.equal(typeof Dag.renderLegend, 'function', 'one renderer for both pages');
|
||
});
|
||
|
||
test('every colour the scene draws on a block or a band has a key entry', () => {
|
||
const tokens = new Set(Dag.legend({ mine: true }).map((e) => e.token));
|
||
// the renderer's palette read: col={ember:c('--ember',…), …, blue:c('--included',…), red:c('--excluded',…)}
|
||
const read = [...src.matchAll(/c\('(--[a-z0-9-]+)'/g)].map((m) => m[1]);
|
||
const drawn = ['--included', '--excluded', '--ember', '--molten', '--bone', '--ash'];
|
||
for (const t of drawn) { assert.ok(read.includes(t), 'the scene reads ' + t); assert.ok(tokens.has(t), 'the key has an entry in ' + t); }
|
||
});
|
||
|
||
// the /live key as ruled (7 October 2026): Included, Excluded, Selected chain, Proven, Locked checkpoint; no Pending and no
|
||
// Your blocks (the site has no "you"). The exceptions are the ruling's; every entry the two surfaces share must carry the
|
||
// same wording and the same token as the export, so the swatches cannot drift apart again.
|
||
const SITE_RULED = SITE_ORDER.filter((l) => l !== 'Pending');
|
||
// the site deploys from master alone and a release-* branch carries site/ frozen as it was cut (tools/scene/sync.mjs's scope
|
||
// rule), so the site half binds on master (or SCENE_SYNC_SCOPE=site, or a site/live.html passed as SITE_LIVE_HTML) and is a
|
||
// diagnostic on a release branch
|
||
const branch = (() => { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (e) { return ''; } })();
|
||
const siteBinds = process.env.SCENE_SYNC_SCOPE === 'site' || !!process.env.SITE_LIVE_HTML || branch === 'master';
|
||
// the app lives on the release branches (master carries no app/igneum-app/ui/live-dag.js), so the app half binds where that copy exists
|
||
const appBinds = existsSync(join(root, 'app/igneum-app/ui/live-dag.js'));
|
||
const siteEntries = (html) => [...html.matchAll(/<span><i class="lgi([^"]*)"(?: style="([^"]*)")?><\/i>([^<]+)<\/span>/g)].map((m) => { const tok = /var\((--[a-z0-9-]+)\)/.exec(m[2] || ''); return { label: m[3].trim(), token: tok ? tok[1] : (/\btick\b/.test(m[1]) ? '--bone' : ''), shape: /\btick\b/.test(m[1]) ? 'tick' : /border-radius:50%/.test(m[2] || '') ? 'circle' : 'square' }; });
|
||
|
||
test('the app key is the export, not a hand-written list; the site key is the export minus the ruled exceptions (Pending, Your blocks), same wording and tokens on every shared entry', (t) => {
|
||
const app = readFileSync(join(root, 'app/igneum-app/ui/index.html'), 'utf8'), site = readFileSync(process.env.SITE_LIVE_HTML || join(root, 'site/live.html'), 'utf8');
|
||
const appLit = literal(app, 'lg');
|
||
if (appBinds) { assert.ok(/data-legend="mine"/.test(app), 'the app legend renders from IgneumDag.legend({ mine: true })'); assert.deepEqual(appLit, [], 'hand-written entries in the app key: ' + appLit.join(', ')); }
|
||
else t.diagnostic('no app scene copy in this tree (master): the app half binds on the release branches');
|
||
const got = siteEntries(site), exp = Dag.legend({ mine: false }).filter((e) => e.label !== 'Pending'), problems = [];
|
||
if (JSON.stringify(got.map((e) => e.label)) !== JSON.stringify(SITE_RULED)) problems.push('the /live key reads ' + got.map((e) => e.label).join(', ') + ', ruled: ' + SITE_RULED.join(', '));
|
||
for (const e of exp) { const g = got.find((x) => x.label === e.label); if (!g) continue; if (g.token !== e.token) problems.push(e.label + ' token on /live is ' + g.token + ', the export says ' + e.token); if (g.shape !== e.shape) problems.push(e.label + ' shape on /live is ' + g.shape + ', the export says ' + e.shape); }
|
||
if (got.some((e) => /your/i.test(e.label))) problems.push('the site shows Your block; the ruling has none');
|
||
if (siteBinds) assert.deepEqual(problems, [], 'the /live key against the export:\n' + problems.join('\n'));
|
||
else if (problems.length) t.diagnostic('site/live.html on this release branch is the frozen cut (' + problems.join('; ') + '); the check binds on master, where the site deploys from');
|
||
// the app's source line stays
|
||
if (appBinds) assert.match(readFileSync(join(root, 'app/igneum-app/ui/app.js'), 'utf8'), /From Igneum’s observer\. Your blocks are ringed\./);
|
||
// the copies are the source (the sync gate says the same; this fails first when a legend edit misses the sync)
|
||
if (appBinds) assert.equal(readFileSync(join(root, 'app/igneum-app/ui/live-dag.js'), 'utf8'), src, 'the app copy of live-dag.js is the scene source');
|
||
});
|