igneum/tools/scene/legend.test.mjs

70 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// node --test tools/scene/legend.test.mjs (key-22, the project lead'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');
});