The mode (own | external | none) is re-decided every 5 s while the app reads or refuses another node; gone for 60 s, the app starts its own node and says so in Activity. node.mode and node.mode_reason in api/state and on the Node details. Pure extnode::step with the goes-away test first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
462 lines
41 KiB
JavaScript
462 lines
41 KiB
JavaScript
// 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 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');
|
||
assert.equal(V.cardRow(card({ state: 'faulted' })).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).');
|
||
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('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 (it predates 0.3.17)' });
|
||
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' });
|
||
});
|