igneum/app/igneum-app/ui/view.test.mjs

874 lines
76 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 app/igneum-app/ui/view.test.mjs (no dependencies; CI runs it in the site job)
// Loads the View block of app.js (plain browser JS: the file is run with `module` defined and no `document`, so only
// the pure blocks execute) and checks the words each page shows for a given state (miner-ui-2).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const src = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.js'), 'utf8');
const mod = { exports: {} };
new Function('module', src)(mod);
const V = mod.exports.View;
const card = (over) => ({ key: 'nvidia:0:RTX 5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia', kind: 'discrete', worker: 'CUDA', vram_mb: 32768, enabled: true, state: 'mining', hash_now: 124.3, hash_avg: 120, accepted: 3, rejected: 0, identities: 8, ids: [], prepared: true, restart_in_s: 0, message: '', reason: '', power_w: 410.2, power_limit_w: 460, power_default_w: 575, power_pct: 80, power_applied: true, temp_gpu: 61, temp_mem: 72, telemetry_at: 1, ...over });
test('the five sections and their order (miner-ui-4)', () => {
assert.deepEqual(V.PAGES.map((p) => p.id), ['overview', 'cards', 'earnings', 'prove', 'settings']);
assert.equal(V.page('cards').title, 'Cards');
assert.equal(V.page('earnings').title, 'Earnings');
assert.equal(V.page('nonsense').id, 'overview');
});
test('a GPU row: name, kind, the numbers where known, the switch', () => {
const r = V.cardRow(card());
assert.equal(r.name, 'NVIDIA GeForce RTX 5090');
assert.equal(r.kindWord, 'Discrete');
assert.equal(r.integrated, false);
assert.equal(r.on, true);
assert.equal(r.word, 'mining');
assert.equal(r.tone, 'on');
assert.equal(r.hash, '124');
assert.equal(r.temp, '61 °C');
assert.equal(r.tempTone, '');
assert.equal(r.power, '410 W');
assert.equal(r.cap, 460);
assert.equal(r.meta, '32 GB · 3 blocks · next program ready');
assert.equal(r.canToggle, true);
});
test('a GPU row: temperatures turn amber then red; unknown numbers are empty', () => {
assert.equal(V.cardRow(card({ temp_mem: 92 })).tempTone, 'warm');
assert.equal(V.cardRow(card({ temp_mem: 96 })).tempTone, 'hot');
assert.equal(V.cardRow(card({ temp_gpu: 93 })).tempTone, 'hot');
const apple = V.cardRow(card({ key: 'apple::Apple M5 Max', name: 'Apple M5 Max', vendor: 'apple', kind: 'apple', worker: 'Metal', vram_mb: 131072, power_w: 0, power_default_w: 0, temp_gpu: 0, temp_mem: 0, telemetry_at: 0, hash_now: 7.4, accepted: 0, prepared: false }));
assert.equal(apple.kindWord, 'Apple silicon');
assert.equal(apple.hash, '7.4');
assert.equal(apple.temp, '');
assert.equal(apple.power, '');
assert.equal(apple.cap, 0);
assert.equal(apple.meta, '128 GB unified');
});
test('an Intel Arc card is a discrete Intel row: INTEL badge, no power reading sentence, measure-only tune words (intel-arc, 7 October 2026)', () => {
const arc = card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', kind: 'discrete', worker: 'OpenCL', vram_mb: 12208, enabled: true, state: 'mining', hash_now: 15.2, power_w: 0, temp_gpu: 0, sweep_supported: false, sweep_state: 'unsupported', sweep_note: 'not available: Intel Arc exposes no power cap or reading through OpenCL (the driver\'s IGCL counters come next)' });
const r = V.cardRow(arc);
assert.equal(r.vendor, 'intel');
assert.equal(r.kindWord, 'Discrete');
assert.equal(r.integrated, false);
assert.equal(r.on, true);
assert.ok(r.meta.startsWith('12 GB'), r.meta);
assert.equal(r.power, '', 'no watts cell without a reading');
assert.equal(V.noReading(arc), 'No power or temperature reading on Intel Arc yet: the driver exposes them through IGCL, not OpenCL');
assert.equal(V.tunable(arc), false, 'no cap, no clock control: not tunable');
assert.equal(V.proveTier(arc), 'Intel(R) Arc(TM) B580 Graphics: cannot prove yet (proving needs an NVIDIA card).');
const tm = mod.exports.TuneLine.model(arc, 1000);
assert.equal(tm.kind, 'off');
assert.equal(tm.text, 'tuning: measure only on Intel Arc once a power reading exists');
});
test('driver-check: the row\'s driver strip per state (offer, running, reboot, error, note, fine)', () => {
const offer = { vendor: 'intel', status: 'missing', installed: '', wanted: '32.0.101.9034', min_version: '32.0.101.9034', size: 932631144, url: 'https://downloadmirror.intel.com/929959/gfx_win_101.9034.exe', hash_source: 'https://www.intel.com/content/www/us/en/download/785597/', text: 'Install the Intel Arc driver 32.0.101.9034 (889 MB). No driver was found, so this card cannot mine yet.', installable: true };
const arc = card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', kind: 'discrete', worker: 'OpenCL', driver_os: '', driver_offer: offer });
const idle = { drivers: { status: 'idle', vendor: '', version: '', progress: 0, message: '', error: '', reboot_required: false, dry_run: false } };
let w = V.driverWords(arc, idle);
assert.equal(w.kind, 'offer');
assert.equal(w.button, 'install');
assert.equal(w.text, offer.text);
assert.ok(w.sub.startsWith('Downloaded from downloadmirror.intel.com, checked against the vendor'), w.sub);
// running: the progress line names the vendor, the version and the percent while downloading
w = V.driverWords(arc, { drivers: { status: 'downloading', vendor: 'intel', version: '32.0.101.9034', progress: 0.42, message: 'downloading 32.0.101.9034 (889 MB) from downloadmirror.intel.com', dry_run: false } });
assert.equal(w.kind, 'running');
assert.equal(w.text, 'Downloading the Intel Arc driver 32.0.101.9034 · 42%');
w = V.driverWords(arc, { drivers: { status: 'installing', vendor: 'intel', version: '32.0.101.9034', progress: 0.7, message: 'installing INTEL 32.0.101.9034: Windows asks for permission once', dry_run: true } });
assert.equal(w.text, 'Installing the Intel Arc driver 32.0.101.9034');
assert.equal(w.dry, true);
// reboot: the Restart now button, mining continues until the click
w = V.driverWords(arc, { drivers: { status: 'reboot', vendor: 'intel', version: '32.0.101.9034', reboot_required: true } });
assert.equal(w.kind, 'reboot');
assert.equal(w.button, 'restart');
assert.equal(w.text, 'Intel Arc driver 32.0.101.9034 installed. Restart Windows to finish.');
// error: the reason and Install again
w = V.driverWords(arc, { drivers: { status: 'error', vendor: 'intel', version: '32.0.101.9034', error: 'sha256 mismatch: the file is not the one the table names' } });
assert.equal(w.kind, 'error');
assert.equal(w.button, 'install');
assert.ok(w.text.startsWith('The Intel Arc driver did not install: sha256 mismatch'), w.text);
// another vendor\'s install does not touch this row
w = V.driverWords(arc, { drivers: { status: 'downloading', vendor: 'nvidia', version: '617.42', progress: 0.1 } });
assert.equal(w.kind, 'offer');
// a note (macOS, Linux, HiveOS): no button
const mac = card({ vendor: 'apple', kind: 'apple', driver_offer: { vendor: 'apple', status: 'none', text: 'Apple silicon: no driver step, macOS carries it.', installable: false } });
w = V.driverWords(mac, idle);
assert.equal(w.kind, 'note');
assert.equal(w.button, undefined);
const linux = card({ driver_offer: { vendor: 'nvidia', status: 'missing', text: 'Linux: install the package nvidia-driver-570 (the worker needs 570.00 or newer).', installable: false } });
assert.equal(V.driverWords(linux, idle).kind, 'note');
// fine: nothing on the row (the details panel carries the sentence)
const fine = card({ driver_os: '617.42', driver_offer: { vendor: 'nvidia', status: 'fine', text: 'NVIDIA driver 617.42: fine.', installable: false } });
assert.equal(V.driverWords(fine, idle).kind, 'fine');
// no table: nothing
assert.equal(V.driverWords(card({ driver_offer: null }), idle).kind, 'none');
assert.equal(V.vendorWord('amd'), 'AMD');
});
test('an integrated GPU is shown as integrated and off, with its reason', () => {
const r = V.cardRow(card({ key: 'intel:1:UHD', name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated', enabled: false, state: 'off', hash_now: 0, reason: 'integrated GPU: slow and shares the machine memory', power_w: 0, temp_gpu: 0 }));
assert.equal(r.integrated, true);
assert.equal(r.kindWord, 'Integrated');
assert.equal(r.on, false);
assert.equal(r.word, 'off');
assert.equal(r.tone, 'off');
assert.equal(r.hash, '');
assert.equal(r.sub, 'integrated GPU: slow and shares the machine memory');
assert.equal(V.cardRow(card({ kind: 'unknown' })).canToggle, false);
assert.equal(V.cardRow(card({ state: 'restarting', restart_in_s: 7 })).word, 'restart in 7 s');
// no fault is permanent (7 October 2026, docs/plans/miner-faults.md): a restarting card is the bad tone, there is no faulted state
assert.equal(V.cardRow(card({ state: 'restarting', restart_in_s: 120, message: 'hash rate 0 for 60 s while the node is synced; trying again' })).tone, 'bad');
assert.equal(V.cardRow(card({ state: 'waiting' })).word, 'waiting for the node');
});
test('the big button: start when paused, stop when mining, disabled with no card on', () => {
const m = (over) => ({ state: 'mining', paused: false, cards: [card()], ...over });
assert.deepEqual(V.toggle(m(), { synced: true }, {}), { label: 'Stop mining', sub: 'mining on 1 of 1 card', cls: 'stop', disabled: false, act: 'pause' });
assert.deepEqual(V.toggle(m({ state: 'paused', paused: true }), { synced: true }, {}), { label: 'Start mining', sub: 'paused · the node keeps running', cls: '', disabled: false, act: 'resume' });
const none = V.toggle(m({ cards: [card({ enabled: false })] }), { synced: true }, {});
assert.equal(none.disabled, true);
assert.equal(none.act, '');
assert.equal(none.sub, 'switch a card on in Cards first');
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no card this app can use');
assert.equal(V.toggle(m({ state: 'waiting' }), { synced: false }, {}).sub, 'waiting for the node to sync');
assert.equal(V.toggle(m(), { synced: true }, { quitting: true }).disabled, true);
});
test('the node words: synced, syncing with a percentage, failed, a blocked clock', () => {
const n = (over) => ({ state: 'synced', blocks: 135200, headers: 135200, peers: 3, daa: 140000, last_reading_age_s: 4, message: '', restart_in_s: 0, ...over });
const ok = V.nodeWords(n(), { severity: 'none' }, '');
assert.equal(ok.word, 'synced');
assert.equal(ok.tone, 'ok');
assert.match(ok.line, /every block/);
const sy = V.nodeWords(n({ state: 'syncing', blocks: 50000, headers: 100000 }), { severity: 'none' }, 'about 3 min left at 300 blocks/s');
assert.equal(sy.word, 'syncing');
assert.equal(sy.line, 'about 3 min left at 300 blocks/s · 50,000 of 100,000 (50%)');
assert.equal(V.nodeWords(n({ state: 'failed', message: 'igneumd could not start' }), { severity: 'none' }, '').tone, 'bad');
const blocked = V.nodeWords(n(), { severity: 'block', skew_s: -75 }, '');
assert.equal(blocked.tone, 'bad');
assert.match(blocked.line, /clock is off by 75 s/);
assert.equal(V.peersLine(n({ peers: 0 })), 'none yet: looking for the seed node');
assert.equal(V.peersLine(n({ peers: 1 })), 'one other node this one talks to');
assert.equal(V.heightLine(n({ state: 'syncing', blocks: 10, headers: 500 })), 'of 500 headers seen');
assert.equal(V.heightLine(n()), 'blocks this node holds');
});
test('the next consensus switch is the first height above the DAA score, in plain words', () => {
const sw = [
{ key: 'fees_v1_activation_daa', name: 'Fees v1', daa: 210000 },
{ key: 'difficulty_v2_activation_daa', name: 'Difficulty v2', daa: 33000 },
{ key: 'finality_v3_activation_daa', name: 'Finality v3', daa: 135200 }
];
const next = V.nextSwitch(sw, 140000);
assert.equal(next.name, 'Fees v1');
assert.equal(next.away, 70000);
assert.equal(V.switchLine(next, 140000), 'Fees v1 at block 210,000: 70,000 blocks away, about 19 h 27 min at one block a second.');
assert.equal(V.nextSwitch(sw, 20000).name, 'Difficulty v2');
assert.equal(V.nextSwitch(sw, 300000), null);
assert.match(V.switchLine(null, 300000), /Every planned rule change is behind this node/);
assert.match(V.switchLine(null, 0), /^A rule change is planned ahead/);
assert.equal(V.nextSwitch([], 5), null);
});
test('the prove words follow the switch, the setup, the node and the status', () => {
const pv = (over) => ({ enabled: true, available: true, setup_hint: '', backend: 'cuda', status: 'idle', message: '', current: '', verifier_mode: 'command', pool_entries: 4, pool_verified: 4, pool_failed: 0, ...over });
assert.equal(V.proveWords(pv(), false, true).word, 'off');
assert.equal(V.proveWords(pv({ status: 'setup', available: false, setup_hint: 'proving needs the WSL2 setup' }), true, true).word, 'needs setup');
assert.equal(V.proveWords(pv(), true, false).word, 'waiting');
assert.equal(V.proveWords(pv({ status: 'proving', current: 'block 59199 shard 0' }), true, true).sub, 'block 59199 shard 0');
assert.equal(V.proveWords(pv({ status: 'submitted' }), true, true).tone, 'on');
assert.equal(V.proveWords(pv(), true, true).word, 'idle');
assert.equal(V.verifierWords(pv()).word, 'verifying · pool 4/4');
assert.equal(V.verifierWords(pv()).tone, 'ok');
assert.equal(V.verifierWords(pv({ verifier_mode: 'off', pool_entries: 0 })).tone, 'warn');
assert.equal(V.verifierWords(pv({ verifier_mode: 'off', verifier_reason: 'the WSL2 prover is not installed' })).needsSetup, true);
assert.equal(V.verifierWords({}).word, 'not read yet');
});
test('the dev-fee lines name the share and the switch that turns it off', () => {
const s = (on, fee) => ({ settings: { dev_fee: on }, mining: { fee_total: fee }, dev_fee: { on, percent: on ? 1 : 0, address: '0x1234567890abcdef1234567890abcdef12345678', line: '' } });
assert.equal(V.devFeeLine(s(true, 12)), 'One block in 100 pays the miner software’s dev fee (12 so far). The switch above turns it off.');
assert.equal(V.devFeeLine(s(false, 0)), 'The dev fee is off. Every block pays this address.');
assert.equal(V.devFeeText(s(true, 0)), 'Dev fee 1% (1 block in 100) to 0x123456…5678');
assert.match(V.devFeeText(s(false, 0)), /^Dev fee off/);
});
test('the remote-jobs line and the helpers', () => {
const title = (j) => j.title || j.kind;
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off. Nothing runs here until the switch above allows jobs from the team.');
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'This build has no jobs address.');
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running: build.');
assert.equal(V.jobsNote({ allowed: true, url_set: true, checked_at: 940, queued: 2 }, 1000, title), 'Nothing running; checked 1 min ago; 2 queued.');
assert.equal(V.shortHex('0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a'), '0x2b1a81…79ef7a');
assert.equal(V.shortHex('0xabc'), '0xabc');
assert.equal(V.withCommas(1234567), '1,234,567');
assert.equal(V.compact(2500000), '2.50M');
assert.equal(V.rel(90), '1 min ago');
});
test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as such, with no switch, and leave the counts', () => {
const gone = card({ key: 'amd:gfx1201', name: 'gfx1201', vendor: 'amd', state: 'removed', removed_at: 1000, added_at: 0, gone: false, hash_now: 0 });
const r = V.cardRow(gone);
assert.equal(r.removed, true);
assert.equal(r.on, false);
assert.equal(r.canToggle, false);
assert.equal(r.word, 'removed');
assert.equal(r.tone, 'off');
assert.equal(r.hash, '');
assert.match(r.sub, /^unplugged\. Its row goes/);
const bad = card({ key: 'amd:gfx1201#2', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd', enabled: false, state: 'unusable', problem: 'Code 43', message: 'not usable (Code 43)', reason: 'reboot with the card attached; if it persists, reinstall the driver with the card attached', added_at: 900, removed_at: 0, device: '1', platform: 'AMD Accelerated Parallel Processing', bus: '0000:03:00.0' });
const b = V.cardRow(bad);
assert.equal(b.unusable, true);
assert.equal(b.canToggle, false);
assert.equal(b.word, 'not usable (Code 43)');
assert.equal(b.tone, 'bad');
assert.match(b.sub, /^reboot with the card attached/);
assert.equal(b.title, 'gfx1201 · CUDA device 1 · AMD Accelerated Parallel Processing · bus 0000:03:00.0');
assert.equal(V.cardRow(card()).title, 'CUDA device undefined'.replace(' device undefined', '') === '' ? '' : V.cardRow(card()).title);
// a row that is gone (five minutes after removal) is not shown at all; the big button counts only present cards
assert.deepEqual(V.shownCards([card(), card({ key: 'x', gone: true })]).map((c) => c.key), ['nvidia:0:RTX 5090']);
assert.equal(V.present(card()), true);
// performance order (6 October 2026): PC 1 detects 5090, integrated AMD, 9070 XT; the list shows the 5090, the 9070 XT,
// then the integrated card, whatever the detection order and whatever the integrated card's rate
const pc1 = [
card({ key: 'nvidia:0:RTX 5090', hash_avg: 122 }),
card({ key: 'amd:gfx1036', name: 'AMD Radeon(TM) Graphics', vendor: 'amd', kind: 'integrated', vram_mb: 512, hash_avg: 2.1, enabled: true }),
card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', vendor: 'amd', kind: 'discrete', vram_mb: 16384, hash_avg: 18.2 }),
];
assert.deepEqual(V.shownCards(pc1).map((c) => c.key), ['nvidia:0:RTX 5090', 'amd:gfx1201', 'amd:gfx1036']);
// before any rate (first start): discrete by memory, integrated last; a removed card last of all; ties keep detection order
const fresh = [
card({ key: 'amd:gfx1036', kind: 'integrated', vram_mb: 512, hash_avg: 0, hash_now: 0, state: 'off' }),
card({ key: 'amd:gfx1201', kind: 'discrete', vram_mb: 16384, hash_avg: 0, hash_now: 0, state: 'waiting' }),
card({ key: 'nvidia:0:RTX 5090', hash_avg: 0, hash_now: 0, state: 'waiting' }),
card({ key: 'nvidia:1:RTX 5090', hash_avg: 0, hash_now: 0, state: 'waiting', removed_at: 5 }),
];
assert.deepEqual(V.shownCards(fresh).map((c) => c.key), ['nvidia:0:RTX 5090', 'amd:gfx1201', 'amd:gfx1036', 'nvidia:1:RTX 5090']);
// a small rate change does not reorder (5 MH/s buckets): 122 and 124 sort as equal and keep detection order
assert.deepEqual(V.shownCards([card({ key: 'a', hash_avg: 122 }), card({ key: 'b', hash_avg: 124 })]).map((c) => c.key), ['a', 'b']);
assert.equal(V.present(gone), false);
assert.equal(V.present(bad), false);
const t = V.toggle({ state: 'mining', paused: false, cards: [gone, bad] }, { synced: true }, {});
assert.equal(t.disabled, true);
assert.equal(t.sub, 'no card this app can use');
const t2 = V.toggle({ state: 'mining', paused: false, cards: [card(), gone] }, { synced: true }, {});
assert.equal(t2.sub, 'mining on 1 of 1 card');
});
// ---- miner-ui-3: the row as the hero object, the money, the tune line, the prove tier, the node line, the asks ----
const NOW = 1_800_000_000;
const settings = (over) => ({ settings: { sweep: true, tuning_off: false, tune_period_s: 604800, power_control: true, ...over }, now: NOW });
test('the row word: every state carries a reason, and a paused miner says paused on a card that is on', () => {
const m = { paused: true, state: 'paused' };
assert.equal(V.rowWord(V.cardRow(card({ state: 'off', hash_now: 0 })), m), 'paused');
assert.equal(V.rowWord(V.cardRow(card({ state: 'waiting', hash_now: 0 })), m), 'paused');
assert.equal(V.rowWord(V.cardRow(card()), { paused: false, state: 'mining' }), 'mining');
assert.equal(V.rowWord(V.cardRow(card({ enabled: false, state: 'off' })), m), 'off');
assert.equal(V.cardRow(card({ state: 'held', hash_now: 0, message: 'held for a remote job: shard benchmark' })).word, 'held for a remote job: shard benchmark');
assert.equal(V.cardRow(card({ state: 'tuning', tune_step: 3, tune_steps: 9, power_w: 300 })).word, 'tuning: step 3 of 9 · measuring 300 W');
assert.equal(V.cardRow(card({ state: 'starting', hash_now: 0 })).word, 'starting');
assert.equal(V.cardRow(card({ state: 'failed', hash_now: 0 })).tone, 'bad');
});
test('money: £ a day from watts at a price, nothing without one, the cells leave out what is not reported', () => {
assert.equal(V.poundsPerDay(310, 28.5).toFixed(4), '2.1204');
assert.equal(V.money(V.poundsPerDay(310, 28.5)), '£2.12');
assert.equal(V.poundsPerDay(310, 0), null);
assert.equal(V.poundsPerDay(0, 28), null);
assert.equal(V.money(123.4), '£123');
assert.equal(V.money(null), '');
const c = V.cells(card(), 28);
assert.equal(c.hash, '124'); assert.equal(c.power, '410 W'); assert.equal(c.money, '£2.76'); assert.equal(c.temp, '61 °C'); assert.equal(c.needsPrice, false); assert.equal(c.noReading, '');
assert.equal(c.eff, '0.30 MH/W');
const noPrice = V.cells(card(), 0);
assert.equal(noPrice.money, ''); assert.equal(noPrice.needsPrice, true);
const apple = V.cells(card({ vendor: 'apple', kind: 'apple', power_w: 0, temp_gpu: 0, temp_mem: 0, eff_mhw: 0 }), 28);
assert.equal(apple.power, ''); assert.equal(apple.money, ''); assert.equal(apple.temp, ''); assert.equal(apple.needsPrice, false);
assert.equal(apple.noReading, 'No power or temperature reading on Apple silicon');
assert.equal(V.cells(card({ kind: 'integrated', vendor: 'other', power_w: 0, temp_gpu: 0, eff_mhw: 0 }), 28).noReading, 'No power or temperature reading on an integrated GPU');
assert.equal(V.wattsTotal([card(), card({ key: 'b', power_w: 100 }), card({ key: 'c', power_w: 50, removed_at: 5 })]), 510.2);
assert.equal(V.wattsTotal([card(), card({ key: 'd', power_w: 48, state: 'off', enabled: false })]), 410.2, 'an idle card’s standby draw is not counted');
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0.422 }))), '0.42 MH/W');
assert.equal(V.effText(V.effOf(card({ eff_mhw: 0, power_w: 0 }))), '');
});
test('the tune line: never run, running with a step and a bar, tuned with the point and the next check, the prior, measured only, pinned, stopped, fleet pause, no control', () => {
const nv = (over) => card({ sweep_state: 'idle', sweep_note: '', sweep_pct: 0, sweep_at: 0, tune_line: '', tune_source: '', tune_clock_mhz: 0, tune_control: true, pinned: false, tune_step: 0, tune_steps: 0, tune_eta_s: 0, ...over });
const idle = V.tuneWords(nv(), settings(), NOW);
assert.equal(idle.kind, 'idle'); assert.equal(idle.text, 'Not tuned yet: starts after 2 min of steady mining'); assert.equal(idle.button, 'tune'); assert.equal(idle.progress, -1);
assert.equal(V.tuneWords(nv(), settings({ sweep: false }), NOW).text, 'Not tuned yet: Ember Tune is off in Settings');
const run = V.tuneWords(nv({ state: 'tuning', tune_step: 7, tune_steps: 9, tune_eta_s: 230 }), settings(), NOW);
assert.equal(run.kind, 'running'); assert.equal(run.text, 'Tuning: step 7 of 9 · about 4 min left'); assert.equal(run.button, 'stop'); assert.equal(run.progress.toFixed(2), '0.67');
const runNoStep = V.tuneWords(nv({ sweep_state: 'running', sweep_note: 'tuning: waits for 120 s of steady mining' }), settings(), NOW);
assert.equal(runNoStep.text, 'Tuning: waits for 120 s of steady mining');
const tuned = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 7200 }), settings(), NOW);
assert.equal(tuned.kind, 'tuned'); assert.equal(tuned.note, '122.3 MH/s at 290 W (0.422 MH/W)'); assert.equal(tuned.button, 'tune');
assert.match(tuned.text, /^Tuned 2 h ago · 2470 MHz at 100% · next check /);
const prior = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'confirm', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 120 }), settings(), NOW);
assert.match(prior.text, /^Tuned 2 min ago from what other miners found, confirmed · 2470 MHz at 100%/);
const measured = V.tuneWords(nv({ tune_line: 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed' }), settings(), NOW);
assert.equal(measured.kind, 'measured'); assert.equal(measured.text, 'Measured 26.7 MH/s at 38 W (0.703 MH/W) as it runs, 10 min ago');
assert.equal(measured.note, 'Measured only on Apple silicon: the system sets the clocks and the power; no control exposed');
const off = V.tuneWords(nv({ tune_control: false, sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)' }), settings({ power_control: false }), NOW);
assert.equal(off.kind, 'idle'); assert.equal(off.note, 'Measured only until Power control is on in Settings (Windows asks for administrator rights once)');
const pinned = V.tuneWords(nv({ tune_line: 'Tuned: 120 MH/s at 300 W (0.400 MH/W)', tune_source: 'full', pinned: true, power_pct: 80, sweep_at: NOW - 60 }), settings(), NOW);
assert.match(pinned.text, /^Your cap stays pinned at 80% · Tuned 1 min ago/);
const stopped = V.tuneWords(nv({ sweep_note: 'tuning stopped: a remote job took the GPU' }), settings(), NOW);
assert.equal(stopped.kind, 'stopped'); assert.equal(stopped.text, 'Tuning stopped: a remote job took the GPU');
const paused = V.tuneWords(nv(), settings({ tuning_off: true, tuning_note: 'tuning paused fleet-wide by the signed manifest' }), NOW);
assert.equal(paused.kind, 'paused'); assert.equal(paused.button, '');
assert.equal(V.tuneWords(card({ vendor: 'other', kind: 'integrated', sweep_supported: false }), settings(), NOW).kind, 'none');
assert.equal(V.tunable(card({ vendor: 'apple', kind: 'apple' })), true);
assert.equal(V.tunable(card({ removed_at: 5 })), false);
});
test('the next check is a weekday inside a week, a date beyond it, due now when overdue', () => {
assert.equal(V.nextCheck(0, 604800, NOW), '');
assert.equal(V.nextCheck(NOW - 700000, 604800, NOW), 'next check due now');
assert.equal(V.nextCheck(NOW - 604800 + 3600, 604800, NOW), 'next check within a day');
const inThree = V.nextCheck(NOW - 604800 + 3 * 86400, 604800, NOW);
assert.match(inThree, /^next check (Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday)$/);
assert.match(V.nextCheck(NOW, 604800 * 3, NOW), /^next check \d{1,2} [A-Z][a-z]{2}$/);
assert.equal(V.nextCheck(NOW - 100, 0, NOW), V.nextCheck(NOW - 100, 604800, NOW), 'no period means seven days');
});
test('the Tuning card schedule and the goal consequence', () => {
const nv = (over) => card({ sweep_at: 0, tune_steps: 0, ...over });
assert.equal(V.schedule([nv()], settings(), NOW), 'Not tuned yet: the first tune starts 2 min into steady mining.');
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work when you press them.');
assert.equal(V.schedule([nv({ state: 'tuning', tune_step: 2, tune_steps: 9, tune_eta_s: 500 })], settings(), NOW), 'Tuning now: NVIDIA GeForce RTX 5090, step 2 of 9, mining again in about 8 min.');
assert.match(V.schedule([nv({ sweep_at: NOW - 7200 }), nv({ key: 'b', name: 'B', sweep_at: NOW - 60 })], settings(), NOW), /^Last tune 1 min ago \(B\) · next check /);
assert.equal(V.schedule([], settings(), NOW), 'No card here can be tuned.');
assert.equal(V.schedule([nv()], settings({ tuning_off: true, tuning_note: 'tuning paused fleet-wide by the signed manifest' }), NOW), 'tuning paused fleet-wide by the signed manifest');
const g = V.goalWords('efficiency', [card({ sweep_watts: 290, power_w: 410 })], 28);
assert.equal(g.watts, 290); assert.equal(g.consequence, 'about £1.95 a day at 290 W');
assert.equal(V.goalWords('rate', [card({ power_default_w: 575 })], 28).watts, 575);
assert.equal(V.goalWords('balanced', [card()], 28).watts, 410);
assert.equal(V.goalWords('nonsense', [card()], 0).goal, 'balanced');
assert.equal(V.goalWords('balanced', [card()], 0).consequence, 'about 410 W');
});
test('the prove tier sentence per card, and the counts in one line', () => {
assert.equal(V.proveTier(card()), 'NVIDIA GeForce RTX 5090: proves while it mines (32 GB; a full shard needs 20.4 GB).');
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 4070', vram_mb: 12288 })), 'NVIDIA GeForce RTX 4070: cannot prove a full shard (12 GB; a full shard needs 20.4 GB).');
// main's rule, 7 October 2026: under 12 GB the prover refuses and says why (the same sentence as src/provedefault.rs)
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 3080', vram_mb: 10240 })), 'NVIDIA GeForce RTX 3080: proving needs a 12 GB card; mining continues (10 GB).');
assert.equal(V.proveTier(card({ name: 'Apple M5 Max', vendor: 'apple' })), 'Apple M5 Max: proves on the CPU, slowly.');
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (proving needs an NVIDIA card).');
assert.equal(V.proveLine({ assigned: 0, submitted: 0, paid: 0, paid_wei: '0' }, false, true), 'Off · not proving · 0 assigned · 0 proven · 0 paid · 0.0000 IGN');
assert.equal(V.proveLine({ status: 'idle', assigned: 3, submitted: 2, paid: 1, paid_wei: '1500000000000000000', available: true, enabled: true }, true, true), 'Idle · nothing assigned · 3 assigned · 2 proven · 1 paid · 1.5000 IGN');
});
test('the node line, the activity line, the asks', () => {
const n = { state: 'synced', blocks: 109859, headers: 109859, peers: 6, daa: 198000, last_reading_age_s: 5, message: '' };
const l = V.nodeLine(n, { severity: 'none' }, '');
assert.equal(l.text, 'Node synced · 6 peers · 109,859 blocks'); assert.equal(l.tone, 'ok'); assert.match(l.sub, /every block/);
assert.equal(V.nodeLine({ ...n, peers: 1, blocks: 0, state: 'starting' }, { severity: 'none' }, '').text, 'Node starting · 1 peer');
assert.equal(V.nodeLine(n, { severity: 'block', skew_s: -412 }, '').tone, 'bad');
assert.equal(V.eventLine('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}'), 'consensus parameters from the signed manifest (details in the log)');
assert.equal(V.eventLine('node synced: 151512 blocks, 6 peer(s)'), 'node synced: 151512 blocks, 6 peer(s)');
assert.equal(V.eventLine('a '.repeat(100), 40).length <= 41, true);
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? Mining restarts.');
assert.match(V.trustAsk(true), /^Include proofs the node never checked\? Devnet only\. The node restarts\.$/);
assert.match(V.trustAsk(false), /^Check proofs again\?/);
assert.equal(V.ago(30), 'just now'); assert.equal(V.ago(7200), '2 h ago'); assert.equal(V.ago(200000), '2 d ago');
});
test('the activity feed and the pill speak in the user\'s voice; the engine line is kept as the tooltip', () => {
const pe = V.plainEvent('job update-now-0313-switch-d937c69d (Exec restart: re-read the manifest (exec_restart_number 27276) and restart the node with it) done after 0 min, exit 0, report uploaded: update check and install asked');
assert.equal(pe.text, 'A job from the team ran: update check, 0 min');
assert.match(pe.tech, /^job update-now/);
assert.equal(V.plainEvent('updated to Igneum Miner 0.3.14 from 0.3.13').text, 'Updated to 0.3.14');
assert.equal(V.plainEvent('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}').text, 'Rule settings read from the team');
assert.equal(V.plainEvent('node synced: 151512 blocks, 6 peer(s)').text, 'Node synced: 151,512 blocks, 6 peers');
assert.equal(V.plainEvent('block accepted by the node (Apple M5 Max)').text, 'Block found by Apple M5 Max');
assert.equal(V.plainEvent('1 remote job received from Igneum').text, '1 job from the team received');
assert.equal(V.plainEvent('remote job: update check now; a newer version installs at once').text, 'The team asked for an update check');
assert.equal(V.plainEvent('a node already answers on 127.0.0.1:26610; using it (it is not stopped by this app)').text, 'Using the node already running on this machine');
assert.equal(V.plainEvent('igneumd exited with code 101 after 300 s; restarting').text, 'The node stopped (code 101). Restarting');
assert.equal(V.plainEvent('miner apple:0 exited with code 1; restarting in 8 s').text, 'A card stopped (code 1). Restarting in 8 s');
assert.equal(V.plainEvent('NVIDIA GeForce RTX 5090 kernel race: l128w16 at 122.3 MH/s (+4.1% over base, 6 variants, 40 s)').text, 'NVIDIA GeForce RTX 5090: fastest program chosen, 122.3 MH/s');
assert.equal(V.plainEvent('block 59199 shard 0 paid 0.0125 IGN').text, 'Proof paid: 0.0125 IGN');
assert.equal(V.plainEvent('rewards go to 0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8').text, 'Rewards go to 0xdd44…86e8');
assert.equal(V.plainEvent('mining resumed').text, 'Mining resumed');
assert.equal(V.plainEvent('something new the engine says').text, 'Something new the engine says');
assert.equal(V.plainEvent('something new the engine says').tech, '');
const base = { setup_done: true, quitting: false, mining: { state: 'mining', hash_total: 510.6, cards: [] }, node: { state: 'synced' } };
assert.deepEqual(V.pill(base), { text: 'Mining · 511 MH/s', tone: 'on' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 17.04, cards: [] } }), { text: 'Mining · 17.0 MH/s', tone: 'on' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Paused', tone: '' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'syncing' } }), { text: 'Syncing the node', tone: '' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'restarting' } }), { text: 'Node restarting', tone: 'bad' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 1, cards: [{ state: 'tuning' }] } }), { text: 'Tuning', tone: 'on' });
assert.deepEqual(V.pill({ ...base, setup_done: false, detecting: true }), { text: 'Detecting cards', tone: '' });
assert.deepEqual(V.pill({ ...base, quitting: true }), { text: 'Stopping', tone: '' });
assert.deepEqual(V.pill({ ...base, mining: { state: 'idle', hash_total: 0, cards: [] } }), { text: 'Not mining', tone: '' });
});
test('Ember on the row (miner-ui-4): the flame fill, before -> after, the saving, the sparkline points, the plain label', () => {
const curve = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 60, watts: 228, mhs: 104.9, eff: 0.46, mark: 'hot' }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }];
const tuned = card({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: NOW - 7200, sweep_watts: 290, sweep_mhs: 122.3, tune_curve: curve, sweep_state: 'idle', sweep_note: '' });
const e = V.ember(tuned, settings(), NOW);
assert.equal(e.kind, 'tuned'); assert.equal(e.fill, 1);
assert.equal(e.label, 'Tuned 2 h ago');
assert.match(e.sub, /^2470 MHz at 80% · next check /);
assert.equal(e.before, 374); assert.equal(e.after, 290); assert.equal(e.savingW, 84);
assert.equal(e.savingText, 'saves 84 W, 0.16% of rate');
assert.equal(e.points.length, 4); assert.equal(e.points.filter((p) => p.chosen).length, 1); assert.equal(e.points.filter((p) => p.marked).length, 1);
// Ember's own fields win over the curve (tune_before_watts / tune_before_mhs, step 0 of the last full plan)
const withFields = V.ember(card({ ...tuned, tune_before_watts: 380, tune_before_mhs: 122.4, tune_source: 'confirm' }), settings(), NOW);
assert.equal(withFields.before, 380); assert.equal(withFields.savingW, 90); assert.match(withFields.label, /from what other miners found$/);
// a confirm plan without the fields claims no saving (its curve's first row is the prior, not an untuned point)
assert.equal(V.ember(card({ ...tuned, tune_source: 'confirm' }), settings(), NOW).savingText, '');
assert.equal(V.ember(card({ ...tuned, tune_before_watts: 0 }), settings(), NOW).savingText, '', 'zero means never measured');
const running = V.ember(card({ state: 'tuning', tune_step: 4, tune_steps: 9, tune_eta_s: 410, sweep_state: 'running' }), settings(), NOW);
assert.equal(running.kind, 'running'); assert.equal(running.fill.toFixed(2), '0.33'); assert.equal(running.label, 'Tuning: step 4 of 9'); assert.equal(running.sub, 'about 7 min left');
const measured = V.ember(card({ tune_line: 'Measured: 122.3 MH/s at 410 W (0.298 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)', sweep_state: 'idle' }), settings({ power_control: false }), NOW);
assert.equal(measured.kind, 'measured'); assert.equal(measured.label, 'Measured, not tuned'); assert.equal(measured.sub, 'Power control is off'); assert.equal(measured.fill, 0.5);
const idle = V.ember(card({ sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0 }), settings(), NOW);
assert.equal(idle.kind, 'idle'); assert.equal(idle.fill, 0); assert.equal(idle.label, 'Not tuned yet'); assert.equal(idle.sub, 'starts after 2 min of steady mining');
const off = V.ember(card({ sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0 }), settings({ sweep: false }), NOW);
assert.equal(off.label, 'Not tuned: Ember Tune is off'); assert.equal(off.sub, 'Tune starts one by hand');
assert.equal(V.ember(card({ vendor: 'other', kind: 'integrated', sweep_supported: false }), settings(), NOW).kind, 'none');
assert.equal(V.fleetSaving([tuned, card({ key: 'b', sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), 'Ember saves 84 W, £0.56 a day across 1 tuned card');
assert.equal(V.fleetSaving([card({ sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), '');
assert.deepEqual(V.beforeOf({ tune_before_watts: 0, tune_curve: curve }), null);
});
test('the 0.3.16 engine fields (ember-tune fd03f35): the balance row, the fleet totals, the chain scene note', () => {
assert.deepEqual(V.balanceWords({}, NOW), { text: '', sub: '', pounds: null }, 'an older engine has no balance field: the row stays hidden');
const unread = V.balanceWords({ balance_wei: null, balance_age_s: -1, balance_note: '', price_gbp_per_ign: null }, NOW);
assert.equal(unread.text, 'not read yet'); assert.equal(unread.pounds, null);
const read = V.balanceWords({ balance_wei: '12345600000000000000', balance_age_s: 20, balance_note: '', price_gbp_per_ign: null }, NOW);
assert.equal(read.text, '12.3456 IGN'); assert.equal(read.sub, 'in the wallet · read just now'); assert.equal(read.pounds, null);
const priced = V.balanceWords({ balance_wei: '12345600000000000000', balance_age_s: 90, balance_note: '', price_gbp_per_ign: 0.5 }, NOW);
assert.equal(priced.pounds.toFixed(2), '6.17'); assert.equal(priced.sub, 'in the wallet · read 1 min ago');
assert.equal(V.balanceWords({ balance_wei: '5000000000000000000000', balance_age_s: 3, balance_note: 'the node answered 0x for the balance, not a quantity' }, NOW).sub, 'in the wallet · read just now · the node answered 0x for the balance, not a quantity');
assert.equal(V.balanceWords({ balance_wei: '5000000000000000000000', balance_age_s: 3 }, NOW).text, '5,000 IGN');
assert.equal(V.ign('1500000000000000000'), 1.5); assert.equal(V.ign('x'), null);
const cards = [card(), card({ key: 'b', power_w: 100 })];
assert.equal(V.fleetWatts({ watts_total: 0 }, cards), 510.2, 'no engine figure: the UI sums the cards');
assert.equal(V.fleetWatts({ watts_total: 629 }, cards), 629, 'the engine figure wins');
assert.equal(V.fleetPounds({ watts_total: 629, pounds_per_day: 4.23 }, cards, 28), 4.23);
assert.equal(V.fleetPounds({ watts_total: 629, pounds_per_day: 0 }, cards, 28).toFixed(2), '4.23', 'no engine £: computed from the watts and the price');
assert.equal(V.fleetPounds({}, cards, 0), null);
assert.equal(V.liveNote({ ok: true, source: 'node', blocks: [] }), 'From your node. Your blocks are ringed.');
assert.equal(V.liveNote({ ok: true, source: 'site', blocks: [] }), 'From Igneum’s observer. Your blocks are ringed.');
assert.equal(V.liveNote({ ok: true, state: { source: 'site' } }), 'From Igneum’s observer. Your blocks are ringed.', 'the 0.3.15 engine carries the source under state');
assert.equal(V.liveNote({ ok: false, pending: true, source: 'site' }), 'connecting');
assert.equal(V.liveNote(null), '');
const mineCards = [card({ ids: ['8fafda27', '1b2c3d4e'] })];
assert.equal(V.isMine('8fafda27', mineCards), true, 'the site: the first 8 hex of the vote key hash');
assert.equal(V.isMine('8fafda..e01f22', mineCards), true, 'the 0.3.16 engine: head6..tail6 of the same hash');
assert.equal(V.isMine('8fafda27a9c1', mineCards), true, 'a longer prefix of the same hash');
assert.equal(V.isMine('0x1B2C3D4E', mineCards), true);
assert.equal(V.isMine('8fafdb..e01f22', mineCards), false, 'one hex off');
assert.equal(V.isMine('deadbeef', mineCards), false);
assert.equal(V.isMine('8faf', mineCards), false, 'under 6 hex never matches');
assert.equal(V.isMine('', mineCards), false);
});
test('N4 (6 October 2026): a frozen tip reads behind, never synced; the tip age is on the node line', () => {
const n = (over) => ({ state: 'synced', blocks: 133000, headers: 133000, peers: 1, daa: 140000, last_reading_age_s: 4, tip_age_s: 3, message: '', restart_in_s: 0, ...over });
const ok = V.nodeLine(n(), { severity: 'none' }, '');
assert.equal(ok.text, 'Node synced · 1 peer · 133,000 blocks · last block 3 s ago'); assert.equal(ok.tone, 'ok');
assert.match(ok.sub, /Last block 3 s ago/);
const behind = V.nodeLine(n({ state: 'behind', synced: false, tip_age_s: 3180, sync_cause: 'frozen' }), { severity: 'none' }, '');
assert.equal(behind.text, 'Node behind · 1 peer · 133,000 blocks · last block 53 min ago'); assert.equal(behind.tone, 'bad');
assert.match(behind.sub, /^No new block for 53 min\. The chain may have moved on without this node\./);
const none = V.nodeLine(n({ state: 'no peers', synced: false, peers: 0, tip_age_s: 500 }), { severity: 'none' }, '');
assert.equal(none.text, 'No peers · 133,000 blocks'.replace('No peers', 'Node no peers')); assert.equal(none.tone, 'bad');
assert.match(none.sub, /^No other node on our chain is connected/);
// an older node without the age keeps the old words
assert.equal(V.nodeLine(n({ tip_age_s: -1 }), { severity: 'none' }, '').text, 'Node synced · 1 peer · 133,000 blocks');
assert.equal(V.tipAge(12), '12 s'); assert.equal(V.tipAge(240), '4 min'); assert.equal(V.tipAge(3180), '53 min'); assert.equal(V.tipAge(7300), '2 h');
const base = { setup_done: true, quitting: false, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'behind' } };
assert.deepEqual(V.pill(base), { text: 'Node behind', tone: 'bad' });
assert.deepEqual(V.pill({ ...base, node: { state: 'no peers' } }), { text: 'No peers', tone: 'bad' });
assert.equal(V.toggle({ state: 'waiting', paused: false, cards: [card({ state: 'waiting' })] }, { synced: false, state: 'behind' }, {}).sub, 'waiting: the node is behind the chain');
});
test('not merging (0.3.18 design note): behind with a moving tip, the miner held', () => {
const w = V.nodeWords({ state: 'behind', sync_cause: 'not merging', tip_age_s: 3, message: 'behind: your blocks are not merging into the network', blocks: 1, headers: 1, peers: 2 }, { severity: 'none' }, '');
assert.equal(w.word, 'behind'); assert.equal(w.tone, 'bad'); assert.match(w.line, /not merging into the network/); assert.doesNotMatch(w.line, /No new block/);
});
test('finality paused (ember-tune 1357d280): the words, no lock called final while paused', () => {
const p = V.finalityWords({ paused: true, last_lock: 412, age_s: 1200, line: 'Finality paused since 18:39 UTC: under two thirds of the weight is signing' });
assert.equal(p.paused, true); assert.equal(p.age, 'paused'); assert.equal(p.note, 'Finality paused since 18:39 UTC: under two thirds of the weight is signing');
assert.equal(/\bfinal\b/i.test(p.note), false);
assert.equal(V.finalityWords({ paused: true, last_lock: 0, age_s: 0 }).note, 'Finality paused: under two thirds of the weight is signing');
const ok = V.finalityWords({ paused: false, last_lock: 412, age_s: 90 });
assert.equal(ok.paused, false); assert.equal(ok.age, '1 min ago'); assert.match(ok.note, /never be undone/);
const none = V.finalityWords({ paused: false, last_lock: 0, message: 'waiting for the miner' });
assert.equal(none.age, 'n/a'); assert.equal(none.note, 'waiting for the miner');
// N5: a node built without the mining engine is a fault
assert.equal(V.nodeWords({ state: 'stub', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '').word, 'stub engine');
assert.deepEqual(V.pill({ setup_done: true, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'stub' } }), { text: 'Node fault', tone: 'bad' });
});
test('whose node (publish 2): the app’s own, another node on this machine checked or unchecked, or none when the ports are taken by other rules', () => {
assert.deepEqual(V.nodeSource({ source: 'app' }), { text: 'this app’s own node', sub: '' });
assert.deepEqual(V.nodeSource({ source: 'external', rules_check: 'match' }), { text: 'another node on this machine', sub: 'same network and rules, checked' });
assert.deepEqual(V.nodeSource({ source: 'external', rules_check: 'unknown' }), { text: 'another node on this machine', sub: 'its rules could not be checked (an older node)' });
assert.deepEqual(V.nodeSource({ source: 'none', port_note: 'Node not started: port 26611 is taken by a node on another network (chain id 7778, ours 7777)' }), { text: 'no node', sub: 'Node not started: port 26611 is taken by a node on another network (chain id 7778, ours 7777)' });
assert.deepEqual(V.nodeSource({}), { text: '', sub: '' });
// 7 October 2026: the mode is re-decided; after the other node leaves for 60 s the app runs its own and says why
assert.deepEqual(V.nodeSource({ source: 'app', mode: 'own', mode_reason: 'the node this app was reading left port 26611 for 60 s, so the app started its own node' }), { text: 'this app’s own node', sub: 'the node this app was reading left port 26611 for 60 s, so the app started its own node' });
const gone = V.nodeWords({ state: 'stopped', source: 'external', message: 'the other node on port 26611 went away; this app starts its own node in 45 s unless it comes back', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '');
assert.equal(gone.word, 'stopped'); assert.match(gone.line, /starts its own node in 45 s/);
const taken = V.nodeWords({ state: 'stopped', source: 'none', message: 'Node not started: port 26611 is taken by a node on other rules (fees_v1_activation_daa 200000, ours 210000)', blocks: 0, headers: 0, peers: 0 }, { severity: 'none' }, '');
assert.equal(taken.word, 'not started'); assert.equal(taken.tone, 'bad'); assert.match(taken.line, /^Node not started: port 26611 is taken/);
assert.deepEqual(V.pill({ setup_done: true, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'stopped', source: 'none' } }), { text: 'Node not started', tone: 'bad' });
});
// ---- miner-ui-5 (7 October 2026): the miner's first month ----
// The chain facts as GET api/ladder shapes them (src/chainfacts.rs) for one key, 6ad0e117 (the example key of the project lead's
// chain-scene reference), under the finality params the devnet node reports (api/live params, 7 Oct 2026: dust 5,
// weightWindow 7,200 s, presenceWindow 20, checkpointInterval 30; tools/fleet/devnet2-override.json sets none of them,
// so Devnet 2 runs the same values). Every number the rungs show is a field of getFinalityWeights,
// getFinalityCheckpoints, /api/stats or this app's own ladder.json, and the rung names it on hover.
const DN2 = { dust: 5, weight_window: 7200, presence_window: 20, checkpoint_interval: 30, min_daa: 7200 };
const MAINNET = { dust: 100, weight_window: 2592000, presence_window: 240, checkpoint_interval: 30, min_daa: 7200 };
const chainOf = (over, params) => ({ ok: true, source: 'node', rpc: { weights: 'getFinalityWeights', checkpoints: 'getFinalityCheckpoints', stats: '/api/stats', relay: '' }, params: params || DN2, checkpoint_index: 8495, latest_locked_index: 8495, next_index: 8496, finality_active: true, total_weight: 6000, active_weight: 5900, voters: 14, keys_listed: 4, keys_cap: 0, network: { hashrate_hps: 100e9, miner_ign_per_block: 80, ign_per_block: 100, ramp_factor: 1, daa: 1284117, blocks_per_day_measured: 86400, bps: 1, stale: false }, mine: [], best: null, ...over });
const key = (over) => ({ id: '6ad0e117', blocks: 0, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4, ...over });
const T5 = 1791362116;
const stateOf = (over) => ({ now: T5, uptime_s: 1800, version: '0.3.19', installed_at: T5 - 3000, setup_done: true, address: { display: '0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8' }, settings: { prove: false, dev_fee: true, power_price_pence: 28 }, node: { synced: true }, proving: { paid: 0, paid_wei: 0 }, mining: { state: 'mining', hash_total: 100, accepted_total: 0, accepted_session: 0, found: [], cards: [card({ key: 'nvidia:0:RTX 4070', name: 'NVIDIA GeForce RTX 4070', hash_now: 100, hash_avg: 100, power_w: 150, ids: ['6ad0e117'] })] }, ladder: { first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: 0, first_mining_at: 0 }, ...over });
const RPC = /getFinalityWeights|getFinalityCheckpoints|igneum_getProofRecords|\/api\/stats|this app/;
test('the Poisson count-up: the gap from the card’s rate and the network’s, the chance from the time waited', () => {
const a = V.poisson(100, 100e9, 0, 1);
assert.equal(Math.round(a.gap_s), 1000);
assert.equal(a.gapText, '17 minutes');
assert.equal(a.chanceText, '0%');
assert.equal(a.line, 'A card like yours finds a block about every 17 minutes on today’s network. Chance so far: 0%.');
assert.equal(a.pool, '');
const b = V.poisson(17, 100e9, 1800, 1);
assert.equal(b.gapText, '1.6 hours');
assert.equal(b.chanceText, '26%');
const c = V.poisson(17, 1e12, 3600, 1);
assert.equal(c.gapText, '16 hours');
assert.match(c.pool, /^Above an 8-hour gap a pool pays by the share/);
assert.equal(V.poisson(100, 100e9, 1000, 1).chanceText, '63%');
assert.equal(V.poisson(17, 10e12, 0, 1).gapText, '6.8 days');
assert.equal(V.poisson(100, 100e9, 10, 1, true).line.indexOf('Your cards find'), 0);
assert.equal(V.poisson(0, 100e9, 10, 1), null);
assert.equal(V.poisson(100, 0, 10, 1), null);
// IGN a day: 86,400 blocks a day, the share of them, 80 producer points each (the litepaper's 6,912 example)
assert.equal(Math.round(V.ignPerDay(100, 100e9, 80, 1)), 6912);
assert.equal(V.ignPerDay(100, 0, 80, 1), null);
});
test('a Devnet 2 key walks every rung on screen, and every rung names its RPC field (the gate)', () => {
// 0. mining, nothing found: the first rung is "now", the rest wait; the count-up line is on
let s = stateOf(), c = chainOf();
let r = V.ladderRungs(s, c, T5);
assert.deepEqual(r.map((x) => x.id), ['first', 'vote', 'signed', 'window', 'rank', 'streak', 'shard']);
assert.deepEqual(r.map((x) => x.state), ['now', 'next', 'next', 'next', 'next', 'next', 'off']);
assert.equal(r[0].line, 'Waiting for your first block.');
assert.equal(V.currentRung(r).id, 'first');
r.forEach((x) => assert.match(x.field, RPC, x.id + ' names no RPC field: ' + x.field));
assert.equal(V.blockCard(null, s, c, T5), null);
// 1. the first block: the card raises, the key enters the table with one block under the dust line
const m1 = { kind: 'first', count: 1, card: 'nvidia:0:RTX 4070', card_name: 'NVIDIA GeForce RTX 4070', hash: '9f3a' + 'ab'.repeat(28) + 'c21e', daa: 1284117, at: T5 - 60 };
s = stateOf({ mining: { ...stateOf().mining, accepted_total: 1, found: [T5 - 60] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 60, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, milestone: m1 } });
c = chainOf({ mine: [key({ blocks: 1, rank: 4 })], best: key({ blocks: 1, rank: 4 }) });
r = V.ladderRungs(s, c, T5);
assert.deepEqual(r.map((x) => x.state), ['done', 'now', 'next', 'now', 'done', 'next', 'off']);
assert.equal(r[0].line, 'First block 1 min ago, NVIDIA GeForce RTX 4070.');
assert.equal(r[0].sub, '1 block with your key in the window');
assert.equal(r[1].line, '1 of 5 blocks to a vote.');
assert.equal(r[1].progress, 0.2);
assert.equal(r[3].line, '1 of 2 hours.');
assert.equal(r[4].line, 'Rank 4 of 4 keys.');
assert.equal(V.currentRung(r).id, 'vote');
const bc = V.blockCard(m1, s, c, T5);
assert.equal(bc.eyebrow, 'your first block');
assert.equal(bc.title, 'Block 1,284,117 is yours.');
assert.equal(bc.cardLine, 'NVIDIA GeForce RTX 4070 · 100 MH/s · 0.100% of the network');
assert.equal(bc.ignLine, '80.00 IGN to 0xdd44…86E8 (72 producer points, 8 for signing)');
assert.equal(bc.rankLine, 'rank 4 of 4 keys · hour 1 of 2');
assert.equal(bc.url, 'https://igneum.network/block/' + m1.hash);
assert.equal(bc.hashShort, '9f3aabab…abc21e');
assert.match(bc.fields.ign, /block_reward\.miner_ign/);
// 2. five blocks: the vote
c = chainOf({ mine: [key({ blocks: 5, voter: true, revealed: true, rank: 4 })], best: key({ blocks: 5, voter: true, revealed: true, rank: 4 }) });
r = V.ladderRungs(s, c, T5);
assert.equal(r[1].state, 'done');
assert.equal(r[1].line, 'Your key has a vote.');
assert.equal(r[1].sub, '5 blocks in the window, the line is 5');
assert.equal(r[2].state, 'now');
assert.equal(r[2].line, 'Signing: the next locked checkpoint carries your vote.');
assert.equal(r[5].state, 'now');
// 3. the signature in a locked checkpoint, the streak
s = stateOf({ ...s, ladder: { ...s.ladder, votes_signed: 31, last_vote_index: 8496, last_vote_at: T5 - 5, last_signed_locked: 8496, signed_locked_count: 30, streak_s: 900 } });
c = chainOf({ latest_locked_index: 8496, mine: [key({ blocks: 5, voter: true, participation: 1, rank: 4 })], best: key({ blocks: 5, voter: true, participation: 1, rank: 4 }) });
r = V.ladderRungs(s, c, T5);
assert.equal(r[2].state, 'done');
assert.equal(r[2].line, 'Your signature is in checkpoint 8,496.');
assert.equal(r[2].sub, '30 signed and locked · presence 100% · latest lock 8,496');
assert.equal(r[5].state, 'done');
assert.equal(r[5].line, 'Signing 8 of 80 points, 15 min unbroken.');
// 4. the full window: two hours on the devnet; the rank
s = stateOf({ ...s, ladder: { ...s.ladder, first_block_at: T5 - 7300, blocks_30d: 61, blocks_today: 61 } });
c = chainOf({ latest_locked_index: 8700, mine: [key({ blocks: 61, voter: true, participation: 1, rank: 3 })], best: key({ blocks: 61, voter: true, participation: 1, rank: 3 }) });
r = V.ladderRungs(s, c, T5);
assert.equal(r[3].state, 'done');
assert.equal(r[3].line, '2 of 2 hours. Full weight.');
assert.equal(r[4].line, 'Rank 3 of 4 keys.');
assert.equal(r[4].sub, '14 keys vote · weight, never hashrate: a card that arrived today sits at the bottom');
// P. a paid shard
s = stateOf({ ...s, settings: { ...s.settings, prove: true }, proving: { paid: 1, paid_wei: '1150000000000000000' }, ladder: { ...s.ladder, shards: [{ block: 1284117, shard: 3, wei: '1150000000000000000', at: T5 - 30 }] } });
r = V.ladderRungs(s, c, T5);
assert.equal(r[6].state, 'done');
assert.equal(r[6].line, 'Your card proved shard 3 of block 1,284,117.');
assert.equal(r[6].sub, '1.15 IGN · 1 paid');
assert.deepEqual(r.map((x) => x.state), ['done', 'done', 'done', 'done', 'done', 'done', 'done']);
r.forEach((x) => assert.match(x.field, RPC));
assert.equal(V.currentRung(r).id, 'shard');
// proving off: the rung reads off, never a fault
assert.equal(V.ladderRungs(stateOf(), chainOf(), T5)[6].line, 'Proving is off.');
});
test('the rungs under mainnet-sized params: 100 blocks to a vote, 30 days to full weight; the relay and the cap are named', () => {
const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 61, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 61 } });
const c = chainOf({ source: 'observer', keys_listed: 64, keys_cap: 64, voters: 1204, mine: [key({ blocks: 61, rank: 41 })], best: key({ blocks: 61, rank: 41 }) }, MAINNET);
const r = V.ladderRungs(s, c, T5);
assert.equal(r[1].line, '61 of 100 blocks to a vote.');
assert.equal(r[3].line, '23 of 30 days.');
assert.equal(r[3].progress, 23 / 30);
assert.equal(r[4].line, 'Rank 41 of 64 keys.');
assert.equal(r[4].sub, '1,204 keys vote · the top 64 are listed · weight, never hashrate: a card that arrived today sits at the bottom');
// a key below the observer's 64 says so instead of inventing a rank
const below = V.ladderRungs(s, chainOf({ source: 'observer', keys_listed: 64, keys_cap: 64 }, MAINNET), T5);
assert.equal(below[4].line, 'Below the top 64 keys.');
assert.equal(below[1].sub, 'your key is not in the table yet');
// no chain facts at all: the words say so, nothing is invented
const none = V.ladderRungs(s, { ok: false, error: 'neither the node nor the observer answered' }, T5);
assert.equal(none[1].line, 'A vote needs the dust line’s blocks in the window.');
assert.equal(none[1].sub, 'neither the node nor the observer answered');
assert.equal(none[4].line, 'Rank: not read yet.');
assert.deepEqual(V.windowWords(2592000), { n: 30, unit: 'days', text: '30 days' });
assert.deepEqual(V.windowWords(7200), { n: 2, unit: 'hours', text: '2 hours' });
assert.deepEqual(V.windowWords(0), { n: 30, unit: 'days', text: '30 days' });
});
test('Earnings in IGN first: measured from the last hour, expected from the share, the typed price, the share and rank, IGN per kWh', () => {
// 100 MH/s on 100 GH/s is 86.4 blocks a day, 3.6 an hour: four in the last hour here
const found = [T5 - 3400, T5 - 2500, T5 - 1300, T5 - 200];
const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 2592, found: found, watts_total: 150 }, proving: { paid: 14, paid_wei: '16100000000000000000' }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 2592 } });
const c = chainOf({ source: 'observer', keys_listed: 1204, keys_cap: 64, voters: 1204, mine: [key({ blocks: 2592, voter: true, rank: 41 })], best: key({ blocks: 2592, voter: true, rank: 41 }) }, MAINNET);
const e = V.earningsLines(s, c, null, 28, T5);
assert.equal(e.day.text, '7,680 IGN a day');
assert.equal(e.day.sub, 'at your last hour’s rate (4 blocks, 80.00 IGN each) · about 6,912 IGN expected at 100 MH/s');
assert.match(e.day.field, /mining\.found.*block_reward\.miner_ign/);
assert.equal(e.window.text, '2,592 blocks in 30 days, 207,360 IGN');
assert.equal(e.window.sub, 'with your key in the weight window · at today’s reward · 14 shards, 16.10 IGN');
assert.equal(e.price.text, 'Type a price');
assert.equal(e.price.set, false);
assert.equal(e.share.text, 'your cards are 0.100% of the network · weight rank 41 of 1,204 keys · 23 of 30 days');
assert.match(e.share.sub, /through igneum\.network’s observer/);
assert.equal(e.perKwh.text, '2,133 IGN per kWh');
assert.equal(e.perKwh.sub, 'IGN a day over 3.6 kWh a day at 150 W');
// a typed price: £ a day from the IGN line, named as the user's figure
const p = V.earningsLines(s, c, 0.05, 28, T5);
assert.equal(p.price.text, 'about £384 a day');
assert.equal(p.price.sub, 'at £0.05 per IGN, your figure · no exchange lists IGN');
assert.match(p.price.field, /never fetched/);
// a fresh miner with no block in the hour: the expectation, said as one
const f = V.earningsLines(stateOf(), chainOf(), null, 28, T5);
assert.equal(f.day.text, 'about 6,912 IGN a day');
assert.equal(f.day.sub, 'expected at 100 MH/s, 0.100% of today’s network · no block in the last hour yet');
assert.equal(f.window.text, '0 blocks in 2 hours, 0.0000 IGN');
// no chain facts: nothing invented
const n = V.earningsLines(stateOf(), { ok: false, error: 'neither answered' }, 0.05, 28, T5);
assert.equal(n.day.text, 'IGN a day: reading the network');
assert.equal(n.day.sub, 'neither answered');
assert.equal(n.price.text, 'at £0.05 per IGN');
assert.equal(n.perKwh, null);
assert.equal(V.cardIgnDay(card({ hash_now: 25 }), chainOf()), '1,728');
assert.equal(V.cardIgnDay(card({ hash_now: 25 }), null), '');
assert.equal(V.fmtIgn(0.5), '0.5000 IGN'); assert.equal(V.fmtIgn(4.88044084), '4.88 IGN'); assert.equal(V.fmtIgn(207360), '207,360 IGN');
});
test('the block card on the four thresholds and on a new card; the first-hour timeline; the profile words', () => {
const s = stateOf(), c = chainOf({ mine: [key({ blocks: 100, voter: true, rank: 2 })], best: key({ blocks: 100, voter: true, rank: 2 }) });
const m = (kind, count, over) => ({ kind, count, card: 'nvidia:0:RTX 4070', card_name: 'NVIDIA GeForce RTX 4070', hash: 'ab'.repeat(32), daa: 1284117, at: T5 - 10, ...over });
assert.equal(V.blockCard(m('hundred', 100), s, c, T5).eyebrow, 'block 100 of this machine');
assert.equal(V.blockCard(m('thousand', 1000), s, c, T5).eyebrow, 'block 1,000 of this machine');
assert.equal(V.blockCard(m('ten_thousand', 20000), s, c, T5).eyebrow, 'block 20,000 of this machine');
assert.equal(V.blockCard(m('card', 7), s, c, T5).eyebrow, 'first block on NVIDIA GeForce RTX 4070');
assert.equal(V.blockCard(m('first', 1, { hash: '', daa: 0 }), s, c, T5).title, 'A block is yours.');
assert.equal(V.blockCard(m('first', 1, { hash: '', daa: 0 }), s, c, T5).url, '');
assert.equal(V.blockCard(m('first', 1), s, chainOf({ network: { ...chainOf().network, ramp_factor: 0.19, miner_ign_per_block: 4.88044084 } }), T5).ignLine, '4.88 IGN to 0xdd44…86E8 (72 producer points, 8 for signing, ramp 19%)');
assert.equal(V.blockCard(m('first', 1), s, c, T5).key, 'card:1:' + (T5 - 10));
const t = V.timeline(stateOf({ ladder: { ...stateOf().ladder, first_synced_at: T5 - 2800, first_mining_at: T5 - 2700, first_block_at: T5 - 2400 } }), T5);
assert.deepEqual(t.steps.map((x) => x.id), ['install', 'started', 'synced', 'mining', 'block', 'payout']);
assert.deepEqual(t.steps.map((x) => x.rel), ['+0 s', '+20 min', '+3 min', '+5 min', '+10 min', '+10 min']);
assert.match(t.steps[5].note, /coinbase of the first block/);
const u = V.timeline(stateOf({ installed_at: 0, uptime_s: 100 }), T5);
assert.equal(u.steps[0].note, 'not recorded on this install');
assert.equal(u.steps[2].note, 'the moment is not recorded; synced now');
assert.match(V.profileWords(true), /^Your address page on igneum\.network is public/);
assert.match(V.profileWords(false), /unlisted/);
assert.equal(V.gapWords(45), '45 seconds'); assert.equal(V.spanWords(3700), '1 h 1 min'); assert.equal(V.spanWords(3 * 86400), '3 days');
// the Activity lines for the milestones
assert.equal(V.plainEvent('first block: found by NVIDIA GeForce RTX 4070 (block card)').text, 'Your first block, found by NVIDIA GeForce RTX 4070');
assert.equal(V.plainEvent('block 1,000 of this machine: found by RTX 4070 (block card)').text, 'Block 1,000 of this machine, found by RTX 4070');
assert.equal(V.plainEvent('first block on Apple M5 Max (block card)').text, 'First block on Apple M5 Max');
});
// ---------- vendor marks (gpu-logos, 7 October 2026) ----------
const css = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.css'), 'utf8');
const marks = (html) => (html.match(/data-mark="/g) || []).length;
test('every vendor renders its own mark: one inline SVG glyph per row, tinted by its vendor class, under 2 KB each', () => {
const rows = {
nvidia: card(),
amd: card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd', vram_mb: 16384 }),
intel: card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', worker: 'OpenCL', vram_mb: 12208 }),
apple: card({ key: 'apple::Apple M5 Max', name: 'Apple M5 Max', vendor: 'apple', kind: 'apple', worker: 'Metal', vram_mb: 131072 }),
};
for (const [v, cd] of Object.entries(rows)) {
const mk = V.vendorMark(cd);
assert.equal(mk.vendor, v);
assert.match(mk.svg, /^<svg viewBox="0 0 24 24"/);
assert.ok(Buffer.byteLength(mk.svg) < 2048, v + ' glyph is ' + Buffer.byteLength(mk.svg) + ' bytes');
assert.ok(!/<image|href=|data:/.test(mk.svg), v + ' glyph is drawn, never a raster or a file');
assert.ok(/currentColor/.test(mk.svg), v + ' glyph takes the vendor token through currentColor');
const html = V.markHtml(cd);
assert.ok(html.startsWith('<div class="badge ' + v + '" data-mark="' + v + '"'), html.slice(0, 60));
assert.equal((html.match(/<svg/g) || []).length, 1);
}
assert.notEqual(V.MARKS.nvidia, V.MARKS.amd); assert.notEqual(V.MARKS.intel, V.MARKS.apple);
});
test('an unknown vendor renders the neutral fallback glyph; a vendor "other" row whose name says Intel keeps the Intel glyph', () => {
const mali = card({ key: 'other:0:Mali', name: 'Mali-G78 MP20', vendor: 'other', kind: 'integrated', worker: 'OpenCL', vram_mb: 0 });
const mk = V.vendorMark(mali);
assert.equal(mk.vendor, 'gpu'); assert.equal(mk.label, 'GPU'); assert.equal(mk.svg, V.MARKS.gpu);
assert.ok(V.markHtml(mali).startsWith('<div class="badge gpu" data-mark="gpu"'));
assert.equal(V.vendorOf(card({ name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated' })), 'intel');
assert.equal(V.vendorOf(card({ name: 'AMD Radeon(TM) Graphics', vendor: 'other', kind: 'integrated' })), 'amd');
assert.equal(V.vendorOf({}), 'gpu');
});
test('the mark never appears twice on one row: the head of a first-run row and of a Cards row carries one data-mark, and only View draws a badge', () => {
const amd = card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd' });
assert.equal(marks(V.rowHead(amd)), 1);
assert.equal(marks(V.rowHead(amd, ': removed')), 1);
assert.equal(marks(V.markHtml(amd) + V.nameHtml(amd)), 1);
assert.equal(marks(V.nameHtml(amd)), 0, 'the name block carries no mark of its own');
// the renderers: the first-run rows, the Cards rows and the block card all go through View.markHtml; no other
// string in app.js opens a badge
assert.equal((src.match(/class="badge /g) || []).length, 1, 'one place in app.js opens a badge');
assert.equal((src.match(/View\.markHtml\(/g) || []).length, 3, 'first-run rows, Cards rows, the block card');
assert.equal(src.includes('badgeHtml('), false);
});
test('the series line under the name is mono text per generation; an integrated row (Intel iGPU, Apple) ends in "integrated" and keeps one mark', () => {
assert.equal(V.seriesLine(card()), 'RTX 50 series · Blackwell');
assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce RTX 4070' })), 'RTX 40 series · Ada Lovelace');
assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce RTX 3080' })), 'RTX 30 series · Ampere');
assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce GTX 1660 Super' })), 'GTX 16 series · Turing');
assert.equal(V.seriesLine(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd' })), 'RX 9000 series · RDNA 4');
assert.equal(V.seriesLine(card({ name: 'AMD Radeon RX 7800 XT', vendor: 'amd' })), 'RX 7000 series · RDNA 3');
assert.equal(V.seriesLine(card({ name: 'gfx1201', code: 'gfx1201', vendor: 'amd' })), 'RDNA 4');
assert.equal(V.seriesLine(card({ name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel' })), 'Arc B series · Battlemage');
assert.equal(V.seriesLine(card({ name: 'Intel Arc A770', vendor: 'intel' })), 'Arc A series · Alchemist');
assert.equal(V.seriesLine(card({ name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated' })), 'UHD Graphics · integrated');
assert.equal(V.seriesLine(card({ name: 'Intel Iris Xe Graphics', vendor: 'other', kind: 'integrated' })), 'Iris Xe · integrated');
assert.equal(V.seriesLine(card({ name: 'AMD Radeon(TM) Graphics', vendor: 'amd', kind: 'integrated' })), 'Radeon · integrated');
assert.equal(V.seriesLine(card({ name: 'Apple M5 Max', vendor: 'apple', kind: 'apple' })), 'M5 series · integrated');
const igpu = card({ name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated' });
const head = V.rowHead(igpu);
assert.equal(marks(head), 1); assert.match(head, /data-mark="intel"/); assert.match(head, /<div class="gen">UHD Graphics · integrated<\/div>/);
assert.equal(V.vendorMark(igpu).svg, V.MARKS.intel, 'an integrated Intel row keeps the Intel glyph, never a second mark');
assert.equal(V.nameHtml(card({ name: '<b>x</b>', vendor: 'zzz', kind: 'unknown' })).includes('<b>'), false, 'the name is escaped');
});
test('the light theme contrast ratio of every vendor colour on its well is at least 3:1, and app.css carries the same hex values and wells', () => {
for (const v of Object.keys(V.VENDORS)) {
const light = V.vendorContrast(v, 'light'), dark = V.vendorContrast(v, 'dark');
assert.ok(light >= 3, v + ' light: ' + light.toFixed(2) + ':1 on its well');
assert.ok(dark >= 3, v + ' dark: ' + dark.toFixed(2) + ':1 on its well');
const [r, g, b] = V.hexRgb(V.VENDORS[v].light), [dr, dg, db] = V.hexRgb(V.VENDORS[v].dark);
assert.ok(css.includes('--mark-' + v + ':' + V.VENDORS[v].dark + ';'), v + ' dark hex in app.css');
assert.equal((css.match(new RegExp('--mark-' + v + ':' + V.VENDORS[v].light + ';', 'g')) || []).length, 2, v + ' light hex in both light blocks');
assert.ok(css.includes('--mark-' + v + '-well:rgba(' + dr + ',' + dg + ',' + db + ',' + String(V.WELL_ALPHA_DARK).replace(/^0/, '') + ')'), v + ' dark well');
assert.equal((css.match(new RegExp('--mark-' + v + '-well:rgba\\(' + r + ',' + g + ',' + b + ',' + String(V.WELL_ALPHA_LIGHT).replace(/^0/, '') + '\\)', 'g')) || []).length, 2, v + ' light well in both light blocks');
}
// the ember accent is for state, never a brand
for (const v of Object.keys(V.VENDORS)) { assert.notEqual(V.VENDORS[v].dark, '#F2541B'); assert.notEqual(V.VENDORS[v].light, '#E04A14'); }
assert.equal(/\.badge\.(nvidia|amd|intel|apple|gpu)\{[^}]*var\(--ember/.test(css), false, 'no badge rule tints with ember');
assert.equal(/\.badge[^{]*\{[^}]*animation/.test(css), false, 'nothing on the mark animates');
});
// ---------- the custom switch (the Prove page fix, 7 October 2026) ----------
// the project lead's screenshot from the live 0.3.19 Mac app: a thin white rectangle above-left of the "Prove on this machine"
// switch and the knob drawn outside the track's left edge at off. Two generic classes reached the switch (the DAG
// legend swatch .lg hit label.switch.lg, the shard track .track hit .switch .track) and the native input was an
// unpositioned absolute. This test reads app.css: no bare .lg or .track rule may exist (every use is scoped), the
// native input is hidden the accessible way (1 px clip, still focusable), and the knob's box sits inside the track's
// box at off and at on for both sizes, in the base rules and untouched by both theme blocks.
const cssRules = (text) => { const out = []; const re = /([^{}]+)\{([^{}]*)\}/g; let m; text = text.replace(/\/\*[\s\S]*?\*\//g, ''); while ((m = re.exec(text))) out.push({ sel: m[1].trim(), body: m[2] }); return out; };
const ruleOf = (sel) => cssRules(css).filter((r) => r.sel.split(',').map((x) => x.trim()).includes(sel)).map((r) => r.body).join(';');
const px = (body, prop) => { const m = new RegExp('(?:^|;)\\s*' + prop + ':\\s*(-?[\\d.]+)px').exec(body); return m ? parseFloat(m[1]) : null; };
const translate = (body) => { const m = /transform:\s*translateX\((-?[\d.]+)px\)/.exec(body); return m ? parseFloat(m[1]) : 0; };
test('no generic .lg or .track rule reaches a custom switch: every use of those class names is scoped (known-failed on the 0.3.19 CSS)', () => {
const bare = cssRules(css).flatMap((r) => r.sel.split(',').map((x) => x.trim())).filter((sel) => /^\.(lg|track)(\.[\w-]+)*(\s+[\w.:-]+)*$/.test(sel) && !/^\.switch\b/.test(sel));
assert.deepEqual(bare, [], 'selectors that start with a bare .lg or .track: ' + bare.join(' | '));
assert.equal(/<label class="switch lg"/.test(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'index.html'), 'utf8')), false, 'the Prove switch no longer shares the legend swatch class');
});
test('the native input of every custom switch is hidden the accessible way: 1 px clipped, still in the tree and focusable, the label kept', () => {
const sw = ruleOf('.switch'), input = ruleOf('.switch input');
assert.match(sw, /position:\s*relative/, '.switch is the positioned ancestor of its input');
assert.match(input, /position:\s*absolute/);
assert.equal(px(input, 'width'), 1); assert.equal(px(input, 'height'), 1);
assert.match(input, /clip:\s*rect\(0 0 0 0\)|clip-path:\s*inset\(50%\)/, 'the clip pattern');
assert.match(input, /overflow:\s*hidden/);
assert.equal(/display:\s*none|visibility:\s*hidden/.test(input), false, 'display none would take the input out of the tab order');
assert.match(ruleOf('.switch input:focus-visible+.track'), /outline/, 'focus stays visible on the track');
});
test('the knob sits inside the track at off and at on, both sizes, and neither theme block touches the switch geometry', () => {
const track = ruleOf('.switch .track'), knob = ruleOf('.switch .track::after'), on = ruleOf('.switch input:checked+.track::after');
const big = ruleOf('.switch.big .track'), bigKnob = ruleOf('.switch.big .track::after'), bigOn = ruleOf('.switch.big input:checked+.track::after');
const inside = (tw, th, kw, kh, left, top, dx, label) => {
assert.ok(left >= 0 && top >= 0, label + ': the knob starts inside (left ' + left + ', top ' + top + ')');
assert.ok(left + kw <= tw && top + kh <= th, label + ' off: knob ' + kw + 'x' + kh + ' at ' + left + ',' + top + ' in a ' + tw + 'x' + th + ' track');
assert.ok(left + dx + kw <= tw, label + ' on: knob right edge ' + (left + dx + kw) + ' in a ' + tw + ' track');
assert.ok(left + dx >= 0, label + ' on: knob left edge ' + (left + dx));
};
assert.match(track, /position:\s*relative/, 'the track positions its knob');
inside(px(track, 'width'), px(track, 'height'), px(knob, 'width'), px(knob, 'height'), px(knob, 'left'), px(knob, 'top'), translate(on), 'the 40 px switch');
inside(px(big, 'width'), px(big, 'height'), px(bigKnob, 'width'), px(bigKnob, 'height'), px(knob, 'left'), px(knob, 'top'), translate(bigOn), 'the big switch');
// the two theme blocks (prefers-color-scheme light, data-theme light) only set tokens: no .switch or .track rule inside them
for (const m of css.matchAll(/@media \(prefers-color-scheme:light\)\{:root:not\(\[data-theme="dark"\]\)\{[^}]*\}\}|:root\[data-theme="light"\]\{[^}]*\}/g)) assert.equal(/\.switch|\.track/.test(m[0]), false, 'a theme block touches the switch');
assert.equal(cssRules(css).some((r) => /\.switch|\.track/.test(r.sel) && /@media \(prefers-color-scheme/.test(r.sel)), false);
});
// ---------- the shared marks module (brand/marks/vendor-marks.mjs, for the site) ----------
test('brand/marks/vendor-marks.mjs carries the app\'s marks and tokens verbatim, plus the Windows mark in the same treatment', async () => {
const mod = await import(join(dirname(fileURLToPath(import.meta.url)), '../../../brand/marks/vendor-marks.mjs'));
assert.deepEqual(mod.MARKS, V.MARKS, 'the glyph strings (regenerate with node brand/marks/regen.mjs)');
assert.deepEqual(mod.VENDORS, V.VENDORS);
assert.deepEqual(mod.WELL_ALPHA, { dark: V.WELL_ALPHA_DARK, light: V.WELL_ALPHA_LIGHT });
assert.equal(mod.OS_MARKS.macos, V.MARKS.apple);
assert.match(mod.OS_MARKS.windows, /^<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path /);
assert.ok(Buffer.byteLength(mod.OS_MARKS.windows) < 2048);
assert.equal(mod.markHtml('amd'), V.markHtml({ vendor: 'amd' }));
assert.equal(mod.markHtml('zzz'), V.markHtml({ vendor: 'zzz' }));
// the tokens the module prints are the ones app.css carries
for (const line of mod.tokensCss().split('\n')) { const inner = /\{([^{}]*)\}\}?$/.exec(line)[1]; assert.ok(css.includes(inner), 'app.css carries: ' + inner.slice(0, 60)); }
});