igneum/tools/community/discord-hooks.test.mjs
igneum-labs 8ca045eb5f Discord structure: the build sheet (channel map, moderation, onboarding, bots, events) and the hourly network feed (7 October 2026, 11:4x UK)
docs/community/discord/structure.md: every channel with topic, pinned first post, slowmode and thread settings; the
AutoMod rules; the rules screen; the server guide to-dos and the pre-join question aligned with the map; the two bot
specs; the office-hour template; what needs the founder. Every row carries "set / read back"; all wait on the Discord
sign-in (the session expired; nothing typed).

tools/community/discord-hooks.mjs: `feed` (hourly: height, hash rate, keys, the last lock from /api/live), milestones
posted once each, the daily hash-origin field, `--via updates`, optional DISCORD_WEBHOOK_FEED, the tick wiring. Six
tests, 37 pass. One live post through the updates webhook as the proof of the path (feed:2026-10-07:11).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 12:14:12 +00:00

459 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

// node --test tools/community/discord-hooks.test.mjs
// Shaping against fixtures cut from the live API on 6 October 2026 (fixtures/discord), the guards, the limits,
// idempotency, the 429 backoff, the watcher's state machine and the London scheduler. Nothing here touches the network.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
shapePulse, shapeDigest, shapeWeekly, shapeRelease, shapeIncidentOpen, shapeIncidentResolve, changedLinesFromPlan, plainLine,
guardText, guardPayload, embed, embedLength, LIMITS, snapshot, devnet2Line, versionShare, watchPass, WATCH, WATCH_CONDITIONS,
Poster, postWebhook, dueNow, ukStamp, londonParts, fmtHash, fmtDur, fmtDelta, fmtChangePct, renderPreview, ICON, EMBER,
shapeFeed, feedMilestones, shapeMilestone, hashOriginLine, runFeed, FEED_MILESTONES,
} from './discord-hooks.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const fx = name => JSON.parse(fs.readFileSync(path.join(HERE, 'fixtures', 'discord', `${name}.json`), 'utf8'));
const NOW = new Date('2026-10-06T18:44:50Z'); // the fixtures' moment, 19:44 UK
const data = () => ({ stats: fx('stats'), live: fx('live'), supply: fx('supply'), fleet: fx('fleet'), now: NOW });
const tmpDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'discord-hooks-'));
// ---------- the pulse ----------
test('pulse: one embed, ember, icon, at most eight fields, UK footer with UTC in brackets', () => {
const { payload } = shapePulse(data(), null);
guardPayload(payload);
const e = payload.embeds[0];
assert.equal(payload.embeds.length, 1);
assert.equal(e.color, EMBER);
assert.equal(e.thumbnail.url, ICON);
assert.ok(e.fields.length <= 8);
assert.match(e.footer.text, /^6 Oct 2026, 19:44 UK \(18:44 UTC\)/);
assert.deepEqual(payload.allowed_mentions, { parse: [] });
assert.equal(payload.username, 'Igneum');
});
test('pulse: first pulse has no window; the second computes blocks per second and the difficulty change from the snapshot', () => {
const first = shapePulse(data(), null);
assert.match(first.payload.embeds[0].fields[0].value, /blocks\/s last 60 s/);
assert.match(first.payload.embeds[0].fields[2].value, /first pulse/);
const prev = { ...first.snapshot, at: new Date(NOW.getTime() - 6 * 3600e3).toISOString(), block_count: first.snapshot.block_count - 21600, difficulty: first.snapshot.difficulty / 1.1, paid_shards_total: first.snapshot.paid_shards_total - 40 };
const second = shapePulse(data(), prev);
const f = second.payload.embeds[0].fields;
assert.match(f[0].value, /1\.00 blocks\/s over 6 h/);
assert.match(f[2].value, /\+10\.0% in 6 h/);
assert.match(f[4].value, /\+40 shards paid in 6 h/);
});
test('pulse: the fields carry the asked numbers in plain words', () => {
const { payload } = shapePulse(data(), null);
const text = payload.embeds[0].fields.map(f => `${f.name}: ${f.value}`).join('\n');
assert.match(text, /Chain block: 141,642/);
assert.match(text, /2\.56 GH\/s/);
assert.match(text, /56 vote keys active \(a card runs several\)/);
assert.match(text, /checkpoint 6,842 at \d+\.\d% of weight/);
assert.match(text, /1\.09 IGN to provers in the last 5 min/);
assert.match(text, /median n\/a/); // the fixture's median_proof_lag_s is null
assert.match(text, /0 provers in 10 min/);
assert.match(text, /ramp 17\.3%/);
});
test('pulse: Devnet 2 is numbers and the gate word only; the seed address and the state text never pass', () => {
const line = devnet2Line(fx('fleet'), NOW.getTime());
assert.equal(line, '4 boxes, height 3,848, last gate FAIL 37 min ago');
assert.equal(devnet2Line(null), null);
assert.equal(devnet2Line({ devnet2: { boxes: 2, height: 10, last_gate: 'PASS: abc', last_gate_at: NOW.toISOString() } }, NOW.getTime()), '2 boxes, height 10, last gate PASS 0 s ago');
assert.throws(() => guardText(fx('fleet').devnet2.seed), /IP address/);
});
test('pulse: finality paused puts a since line in the description, from the newest lock', () => {
const { payload } = shapePulse(paused(), null);
assert.match(payload.embeds[0].description, /\nFinality paused since 2026-10-06 18:4\d UTC: nothing is final until two thirds/);
assert.doesNotMatch(shapePulse(data(), null).payload.embeds[0].description, /Finality paused/);
});
test('pulse: a stale observer changes the first line and keeps the numbers', () => {
const d = data(); d.stats.stale = true; d.stats.age_s = 95;
const { payload } = shapePulse(d, null);
assert.match(payload.embeds[0].description, /^Observer stale for 2 min: every number is last known/);
});
test('pulse: miner version share appears only when engines are exposed', () => {
const d = data();
assert.equal(versionShare(d.live), null);
d.live.miners[0].engine = '0.3.14'; d.live.miners[1].engine = '0.3.14'; d.live.miners[2].engine = '0.3.13';
assert.equal(versionShare(d.live), '0.3.14 67%, 0.3.13 33%');
const { payload } = shapePulse(d, null);
assert.ok(payload.embeds[0].fields.some(f => f.name === 'Miner versions'));
assert.ok(payload.embeds[0].fields.length <= 8);
});
// ---------- digest and weekly ----------
test('digest: a monospace table with yesterday from the snapshot closest to 24 h ago', () => {
const cur = snapshot(data());
const yesterday = { ...cur, at: new Date(NOW.getTime() - 24 * 3600e3 - 600e3).toISOString(), height: cur.height - 80000, locked_index: cur.locked_index - 2800, paid_shards_total: cur.paid_shards_total - 400, circulating_ign: cur.circulating_ign - 450000 };
const { payload } = shapeDigest(data(), [yesterday, { ...cur, at: new Date(NOW.getTime() - 6 * 3600e3).toISOString() }]);
guardPayload(payload);
const d = payload.embeds[0].description;
assert.match(d, /```\n24 h/);
assert.match(d, /chain block\s+61,642\s+141,642/);
assert.match(d, /locks \+2,800/);
assert.match(d, /minted \+450,000 IGN/);
assert.equal(payload.embeds[0].thumbnail, undefined);
const none = shapeDigest(data(), []);
assert.match(none.payload.embeds[0].description, /No snapshot from 24 h ago yet/);
});
test('weekly: the reddit template as an embed with the ledger link and a last-week column', () => {
const first = shapeWeekly(data(), null, []);
guardPayload(first.payload);
const e = first.payload.embeds[0];
assert.equal(e.title, 'Devnet numbers, week of 5 Oct 2026');
assert.match(e.description, /Ledger: https:\/\/igneum\.network\/ledger/);
assert.match(e.description, /First weekly post/);
assert.equal(e.fields.length, 6);
const prev = { ...first.snapshot, height: 100000, locked_index: 4000, paid_shards_total: 1000 };
const second = shapeWeekly(data(), prev, ['Example incident sentence.']);
const text = second.payload.embeds[0].fields.map(f => f.value).join('\n');
assert.match(text, /height 141,642 \(last week 100,000\)/);
assert.match(text, /locks this week \+2,842/);
assert.match(text, /shards this week \+1,258/);
assert.match(text, /• Example incident sentence\./);
assert.match(text, /check\.rule_match 500 \/ 500/);
});
// ---------- the release ----------
const REL = {
version: '0.3.14',
windows: 'https://dl.igneum.network/public/igneum-miner-windows.exe', windowsSha: '08e75effc732f88bca11006b3b7cdf2bc533576f56819f79509fee47a8f5dd59',
mac: 'https://dl.igneum.network/public/igneum-miner-mac.dmg', macSha: 'e9e4c24db81a262a36b6bac11609ce6d24ed03220973da4142df935d41fc4e40',
hive: 'https://dl.igneum.network/public/igneum-miner-hive.tar.gz', hiveSha: 'c4699f2487580dcf06b5c22a36a2eb8383a3307b6137699c76c45eefebdd4c88',
nodeCommit: '4c6b129d', digest: 'b18ed271 (thirteen fields, unchanged)', now: NOW,
};
test('release: three downloads with full sha256, node commit, digest, up to five changed lines; a sha256 passes the 32-hex guard', () => {
const { payload } = shapeRelease({ ...REL, changed: ['One.', 'Two.', 'Three.', 'Four.', 'Five.', 'Six.'] });
guardPayload(payload);
const e = payload.embeds[0];
assert.equal(e.title, 'Igneum Miner 0.3.14');
assert.equal(e.fields.length, 6);
assert.match(e.fields[0].value, /\[Download\]\(https:\/\/dl\.igneum\.network\/public\/igneum-miner-windows\.exe\)\n`08e75eff[0-9a-f]{56}`/);
assert.equal((e.description.match(/^• /gm) || []).length, 5);
assert.doesNotMatch(e.description, /Six/);
});
test('release: refuses a missing value or a hash that is not sha256', () => {
assert.throws(() => shapeRelease({ ...REL, hive: '' }), /release needs hive/);
assert.throws(() => shapeRelease({ ...REL, macSha: 'abc' }), /macSha is not a sha256/);
});
test('release: changed lines from a plan section, table rows by first cell, bullets as they are, guarded lines dropped', () => {
const plan = `# Plan\n\n## 1. Why\n\nIntro.\n\n| Change | Where |\n|---|---|\n| A deep reorg reloads the newest generation; never a genesis replay | there |\n| \`exec_restart_state_root\`, the fourteenth field (block 27,276's root 0x${'e'.repeat(64)}, read on node 1 and confirmed by the fleet on both node kinds; set) | there |\n| A flag under /root was ignored silently | there |\n\n- A bullet line.\n\n## 2. Next\n\n| Not | this |\n`;
const warned = [];
const lines = changedLinesFromPlan(plan, '1. Why', 5, m => warned.push(m));
assert.deepEqual(lines, ['A deep reorg reloads the newest generation', 'exec_restart_state_root, the fourteenth field', 'A bullet line.']);
assert.equal(warned.length, 1);
assert.match(warned[0], /internal path/);
assert.throws(() => changedLinesFromPlan(plan, 'Missing'), /section not found/);
});
test('plainLine: strips code ticks, cuts at the first semicolon, drops a long parenthetical, never cuts mid-word', () => {
assert.equal(plainLine('`A thing` that **reloads** the generation; never a replay'), 'A thing that reloads the generation');
const long = `Start of a long line that goes on (${'x'.repeat(200)}) and ends here with more words`;
assert.equal(plainLine(long), 'Start of a long line that goes on and ends here with more words');
const words = Array.from({ length: 60 }, (_, i) => `word${i}`).join(' ');
const cut = plainLine(words);
assert.ok(cut.length <= 160);
assert.ok(words.startsWith(cut));
assert.ok(!cut.endsWith(' '));
});
// ---------- incidents ----------
test('incident open and resolve: UTC times, the three answers, the duration', () => {
const open = shapeIncidentOpen({ id: 'inc-1', at: new Date('2026-10-06T15:44:00Z'), what: 'What happened.', affected: 'Miners: fine. Provers: paused.', doing: 'Replaying.', now: NOW });
guardPayload(open.payload);
assert.equal(open.payload.embeds[0].title, 'Incident open');
assert.equal(open.payload.embeds[0].fields[0].value, '2026-10-06 15:44 UTC');
const res = shapeIncidentResolve({ id: 'inc-1', openedAt: '2026-10-06T15:44:00Z', at: new Date('2026-10-06T17:10:00Z'), cause: 'A cause.', fix: 'A rule.', what: 'What happened.', now: NOW });
guardPayload(res.payload);
assert.equal(res.payload.embeds[0].title, 'Incident resolved');
assert.equal(res.payload.embeds[0].fields[1].value, '1 h 26 min');
assert.throws(() => shapeIncidentOpen({ id: 'x', what: 'w', affected: '', doing: 'd' }), /needs affected/);
});
// ---------- the watcher ----------
const at = (base, s) => new Date(base.getTime() + s * 1000);
const paused = () => { const d = data(); d.stats.finality.active = false; return d; };
// clear data at +s seconds: finality active and the lock index advancing one checkpoint per 30 s (an unchanged index reads as paused)
const advancing = s => { const d = data(); const idx = 6842 + Math.floor(s / 30); d.stats.finality.latest_locked_index = idx; d.live.finality.latest_locked_index = idx; return d; };
test('watcher: a condition already firing at the first look is pre-existing: never opened, cleared silently; the next start opens', () => {
const state = { watch: {} };
assert.deepEqual(watchPass(paused(), state, NOW), [], 'first look: paused already, nothing observed to start');
assert.ok(state.watch.finality_paused.preexisting);
assert.deepEqual(watchPass(paused(), state, at(NOW, 900)), [], 'held 15 min, still nothing: no open to match a resolve');
assert.deepEqual(watchPass(advancing(1200), state, at(NOW, 1200)), [], 'clears silently');
assert.equal(state.watch.finality_paused.preexisting, null);
assert.deepEqual(watchPass(paused(), state, at(NOW, 1300)), [], 'a start the watcher saw: the hold begins');
const a = watchPass(paused(), state, at(NOW, 1300 + WATCH.finalityPausedS + 1));
assert.equal(a.length, 1); assert.equal(a[0].kind, 'open');
});
test('watcher: finality paused opens after five minutes, once, and resolves after two clear minutes', () => {
const state = { watch: {} };
assert.deepEqual(watchPass(data(), state, at(NOW, -60)), [], 'the first look is clear');
assert.deepEqual(watchPass(paused(), state, NOW), []);
assert.deepEqual(watchPass(paused(), state, at(NOW, 240)), []);
const a = watchPass(paused(), state, at(NOW, 301));
assert.equal(a.length, 1); assert.equal(a[0].kind, 'open'); assert.equal(a[0].key, 'finality_paused');
assert.match(a[0].what, /Finality paused/);
assert.match(a[0].affected, /Miners keep mining/);
assert.deepEqual(watchPass(paused(), state, at(NOW, 600)), [], 'no second open while one is open');
assert.deepEqual(watchPass(advancing(700), state, at(NOW, 700)), [], 'clear must hold');
const r = watchPass(advancing(700 + WATCH.clearHoldS), state, at(NOW, 700 + WATCH.clearHoldS));
assert.equal(r.length, 1); assert.equal(r[0].kind, 'resolve'); assert.equal(r[0].id, a[0].id);
assert.equal(state.watch.finality_paused.open, null);
});
test('watcher: a lock index that does not move for five minutes counts as paused even with active true', () => {
const state = { watch: {} };
watchPass(data(), state, NOW);
assert.deepEqual(watchPass(data(), state, at(NOW, 299)), []);
const a = watchPass(data(), state, at(NOW, 301));
// the hold starts when the stuck test first fires (at 301 s), so the open comes one threshold later
assert.deepEqual(a, []);
const b = watchPass(data(), state, at(NOW, 301 + WATCH.finalityPausedS));
assert.equal(b.length, 1); assert.match(b[0].what, /no new lock/);
});
test('watcher: proof lag over 15 minutes opens at once; observer silence needs three minutes; a failed fetch counts as silence', () => {
const state = { watch: {} };
watchPass(data(), state, at(NOW, -60));
const lag = data(); lag.live.proving.median_proof_lag_s = 1000;
const a = watchPass(lag, state, NOW);
assert.equal(a.length, 1); assert.equal(a[0].key, 'proof_lag'); assert.match(a[0].what, /17 min against a target under 60 s/);
const s2 = { watch: {} };
watchPass(data(), s2, at(NOW, -60));
const failed = { stats: { stale: true, age_s: null, finality: null }, live: {}, supply: {}, now: NOW, fetchFailed: true };
assert.deepEqual(watchPass(failed, s2, NOW), []);
const b = watchPass(failed, s2, at(NOW, WATCH.observerSilentS + 1));
assert.equal(b.length, 1); assert.equal(b[0].key, 'observer_silent'); assert.match(b[0].what, /did not answer/);
});
test('watcher: every automatic text passes the guard', () => {
for (const c of Object.values(WATCH_CONDITIONS)) {
const d = data(); d.stats.age_s = 200; d.live.proving.median_proof_lag_s = 1000;
guardText([c.what(d), c.affected, c.doing, c.cause, c.fix].join('\n'));
}
});
// ---------- guards and limits ----------
test('guard: refuses the founder, hosts, machine ids, paths, IPs, 32-hex tokens, token download paths, webhook URLs, mentions', () => {
const bad = {
'the project lead fixed it': /founder/, 'by [user] at night': /founder/, 'on Hetzner': /hosting/, 'RunPod box': /hosting/, 'host igneum-build-1': /host name/,
'from DESKTOP-ABC1234': /machine id/, 'target ae432dc7': /machine id/, 'box p2-3090-4': /machine id/,
'seed 213.173.110.229:11927': /IP address/, 'see /Users/x/file': /internal path/, 'under /srv/discord-hooks/env': /internal path/, 'in C:\\Users': /internal path/,
[`token ${'a1'.repeat(16)}`]: /32-hex/, 'https://dl.igneum.network/fleet-22adafa34bc2/fleet.json': /outside \/public/, 'https://dl.igneum.network/dl/abc/x.exe': /outside \/public/,
'https://discord.com/api/webhooks/1/x': /webhook URL/, 'hello @everyone': /mention/, 'ping <@123>': /mention/, 'at 19:00 BST': /internal reference/, 'on the MacBook': /forbidden-strings/, 'box.ts.net': /forbidden-strings/, 'LOG_INTAKE_KEY=x': /forbidden-strings/,
};
for (const [text, why] of Object.entries(bad)) assert.throws(() => guardText(text), why, text);
});
test('guard: passes a sha256, a public download URL, an igneum.network link, a 16-hex checkpoint hash and plain words', () => {
guardText(`sha ${'ab'.repeat(32)} public https://dl.igneum.network/public/igneum-miner-mac.dmg live https://igneum.network/live hash a56a00dfb657036d 4 boxes, height 3,848`);
});
test('limits: more than eight fields, a value over 1,024, a total over 6,000, a stray mention setting are refused', () => {
const big = embed({ title: 'T', fields: Array.from({ length: 9 }, (_, i) => ({ name: `f${i}`, value: 'v' })), now: NOW });
assert.equal(big.embeds[0].fields.length, 8, 'embed() itself caps at eight');
big.embeds[0].fields.push({ name: 'f9', value: 'v' });
assert.throws(() => guardPayload(big), /more than 8 fields/);
const long = embed({ title: 'T', fields: [{ name: 'f', value: 'x'.repeat(1025) }], now: NOW });
assert.throws(() => guardPayload(long), /field value over 1024/);
const total = embed({ title: 'T', description: 'd'.repeat(4000), fields: [{ name: 'a', value: 'y'.repeat(1024) }, { name: 'b', value: 'y'.repeat(1024) }], now: NOW });
assert.ok(embedLength(total.embeds[0]) > LIMITS.total);
assert.throws(() => guardPayload(total), /embed over 6000/);
const mention = embed({ title: 'T', fields: [{ name: 'a', value: 'b' }], now: NOW }); mention.allowed_mentions = { parse: ['everyone'] };
assert.throws(() => guardPayload(mention), /allowed_mentions/);
});
// ---------- idempotency, dry run, backoff ----------
test('poster: dry run writes the payload and the preview, a rerun under the same key is skipped, --force posts again', async () => {
const out = tmpDir();
const p = new Poster({ live: false, outDir: out, log: () => {} });
const { payload } = shapePulse(data(), null);
const r1 = await p.post('numbers', 'pulse:test', payload);
assert.equal(r1.skipped, false);
assert.ok(fs.existsSync(path.join(out, 'pulse_test.json')));
assert.ok(fs.existsSync(path.join(out, 'preview.html')));
const r2 = await p.post('numbers', 'pulse:test', payload);
assert.equal(r2.skipped, true);
const f = new Poster({ live: false, outDir: out, force: true, log: () => {} });
const r3 = await f.post('numbers', 'pulse:test', payload);
assert.equal(r3.skipped, false);
const html = fs.readFileSync(path.join(out, 'preview.html'), 'utf8');
assert.match(html, /Network pulse/);
assert.match(html, /#numbers/);
});
test('poster: live mode without a credentials file refuses to start, and never prints a URL', () => {
assert.throws(() => new Poster({ live: true, credFile: path.join(tmpDir(), 'none'), outDir: tmpDir() }), /no credentials file/);
});
test('poster: live mode posts once, stores the message id, skips the rerun; the state is written only after success', async () => {
const dir = tmpDir();
const cred = path.join(dir, 'discord'); fs.writeFileSync(cred, 'DISCORD_WEBHOOK_NUMBERS=https://discord.com/api/webhooks/1/secret\n', { mode: 0o600 });
const calls = [];
const fetchImpl = async (url, init) => { calls.push({ url, body: JSON.parse(init.body) }); return { ok: true, status: 200, headers: new Map(), json: async () => ({ id: '42', channel_id: '7' }) }; };
const p = new Poster({ live: true, credFile: cred, stateFile: path.join(dir, 'state.json'), outDir: dir, log: () => {}, fetchImpl });
const { payload } = shapePulse(data(), null);
const r = await p.post('numbers', 'pulse:live', payload);
assert.equal(r.posted, true); assert.equal(r.id, '42');
assert.equal(calls.length, 1); assert.match(calls[0].url, /\?wait=true$/);
assert.deepEqual(calls[0].body.allowed_mentions, { parse: [] });
const saved = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
assert.equal(saved.posts['pulse:live'].id, '42');
const again = new Poster({ live: true, credFile: cred, stateFile: path.join(dir, 'state.json'), outDir: dir, log: () => {}, fetchImpl });
assert.equal((await again.post('numbers', 'pulse:live', payload)).skipped, true);
assert.equal(calls.length, 1);
await assert.rejects(() => p.post('incidents', 'inc:x', payload), /no DISCORD_WEBHOOK_INCIDENTS/);
});
test('poster: a partial credentials file names its missing keys; the watcher advances its state and posts nothing without the incidents webhook', async () => {
const dir = tmpDir();
const cred = path.join(dir, 'discord'); fs.writeFileSync(cred, 'DISCORD_WEBHOOK_NUMBERS=https://discord.com/api/webhooks/1/secret\n', { mode: 0o600 });
const calls = [];
const fetchImpl = async (url, init) => { calls.push(url); return { ok: true, status: 200, headers: new Map(), json: async () => ({ id: '1' }) }; };
const logs = [];
const p = new Poster({ live: true, credFile: cred, stateFile: path.join(dir, 'state.json'), outDir: dir, log: m => logs.push(m), fetchImpl });
assert.deepEqual(p.missingKeys(), ['DISCORD_WEBHOOK_ANNOUNCEMENTS', 'DISCORD_WEBHOOK_INCIDENTS']);
assert.equal(p.has('numbers'), true); assert.equal(p.has('incidents'), false);
const { runWatch } = await import('./discord-hooks.mjs');
watchPass(data(), p.state, at(NOW, -60));
const lag = data(); lag.live.proving.median_proof_lag_s = 1000;
const r = await runWatch(p, lag);
assert.deepEqual(r, []);
assert.equal(calls.length, 0, 'nothing sent');
assert.match(logs.join('\n'), /open auto-proof_lag-\S+ not posted, no DISCORD_WEBHOOK_INCIDENTS/);
assert.ok(p.state.watch.proof_lag.open, 'the state still records the open, so the key landing later does not flood');
});
test('backoff: a 429 waits retry_after then doubles; success returns the id; six failures give up', async () => {
const sleeps = [];
let n = 0;
const fetchImpl = async () => { n++; if (n <= 2) return { ok: false, status: 429, headers: new Map([['retry-after', '2']]), json: async () => ({ retry_after: 2.5 }) }; return { ok: true, status: 200, headers: new Map(), json: async () => ({ id: '9' }) }; };
const r = await postWebhook('https://example.invalid/hook', { embeds: [] }, { fetchImpl, sleepImpl: async ms => sleeps.push(ms) });
assert.equal(r.id, '9'); assert.equal(n, 3);
assert.deepEqual(sleeps, [2500, 2500]);
const always = async () => ({ ok: false, status: 429, headers: new Map(), json: async () => ({}) });
const s2 = [];
await assert.rejects(() => postWebhook('https://example.invalid/hook', {}, { fetchImpl: always, sleepImpl: async ms => s2.push(ms) }), /after 6 tries/);
assert.deepEqual(s2, [1000, 2000, 4000, 8000, 16000]);
const bad = async () => ({ ok: false, status: 400, headers: new Map(), json: async () => ({}) });
await assert.rejects(() => postWebhook('https://example.invalid/hook', {}, { fetchImpl: bad }), /answered 400/);
});
// ---------- the scheduler and the clock ----------
test('scheduler: Monday 09:xx UK is pulse + digest + weekly, each once; other hours only the pulse slots; nothing twice', () => {
const monday9 = new Date('2026-10-12T08:05:00Z'); // 09:05 UK in BST
assert.equal(londonParts(monday9).weekday, 'Mon'); assert.equal(londonParts(monday9).hour, 9);
const due = dueNow(monday9, { posts: {} });
assert.deepEqual(due.map(d => d.kind), ['pulse', 'digest', 'weekly']);
assert.equal(due[0].key, 'pulse:2026-10-12:09');
const posted = { posts: Object.fromEntries(due.map(d => [d.key, { id: '1' }])) };
assert.deepEqual(dueNow(monday9, posted), []);
assert.deepEqual(dueNow(new Date('2026-10-13T09:30:00Z'), { posts: {} }), [], '10:30 UK is not a slot');
assert.deepEqual(dueNow(new Date('2026-10-13T20:59:00Z'), { posts: {} }).map(d => d.key), ['pulse:2026-10-13:21']);
assert.deepEqual(dueNow(new Date('2026-10-13T02:00:00Z'), { posts: {} }).map(d => d.key), ['pulse:2026-10-13:03']);
});
test('clock: the footer carries UK time with UTC in brackets, in summer and in winter', () => {
assert.equal(ukStamp(new Date('2026-10-06T18:51:00Z')), '6 Oct 2026, 19:51 UK (18:51 UTC)');
assert.equal(ukStamp(new Date('2026-11-06T18:51:00Z')), '6 Nov 2026, 18:51 UK (18:51 UTC)');
assert.equal(ukStamp(new Date('2026-10-25T00:30:00Z')), '25 Oct 2026, 01:30 UK (00:30 UTC)', 'the clocks go back at 01:00 UTC on 25 Oct 2026');
});
test('formatting: hash units, durations, deltas with signs', () => {
assert.equal(fmtHash(2562646398), '2.56 GH/s'); assert.equal(fmtHash(169e6), '169.0 MH/s'); assert.equal(fmtHash(null), 'n/a');
assert.equal(fmtDur(45), '45 s'); assert.equal(fmtDur(406.5), '7 min'); assert.equal(fmtDur(5160), '1 h 26 min'); assert.equal(fmtDur(3 * 86400), '3 days');
assert.equal(fmtDelta(10, 4), '+6'); assert.equal(fmtDelta(4, 10), '−6'); assert.equal(fmtDelta(4, 4), '±0'); assert.equal(fmtDelta(null, 4), 'n/a');
assert.equal(fmtChangePct(110, 100), '+10.0%'); assert.equal(fmtChangePct(90, 100), '−10.0%');
});
test('preview: renders every item as a Discord-like card with the channel and the length', () => {
const items = [{ channel: 'numbers', key: 'pulse:x', payload: shapePulse(data(), null).payload }, { channel: 'announcements', key: 'release:0.3.14', payload: shapeRelease({ ...REL, changed: ['One.'] }).payload }];
const html = renderPreview(items);
assert.equal((html.match(/class="msg"/g) || []).length, 2);
assert.match(html, /#announcements/);
assert.match(html, /<a href="https:\/\/dl\.igneum\.network\/public\/igneum-miner-windows\.exe">Download<\/a>/);
assert.match(html, /border-left-color:#f2541b/);
assert.doesNotMatch(html, /<script/);
});
// ---------- the hourly network feed ----------
test('feed: one small embed with height, hash rate, keys and the last lock from /api/live; no hash-origin field without the report', () => {
const { payload, snapshot: snap, hashOriginDate } = shapeFeed(data(), null);
guardPayload(payload);
const e = payload.embeds[0];
assert.equal(e.title, 'Network feed');
assert.deepEqual(e.fields.map(f => f.name), ['Height', 'Hash rate', 'Keys', 'Last lock']);
assert.match(e.fields[0].value, /^141,642\n/);
assert.match(e.fields[1].value, /2\.56 GH\/s/);
assert.match(e.fields[2].value, /56 active in 10 min \(a card runs several\)\n93 voters in the 30-day window/);
assert.match(e.fields[3].value, /checkpoint 6,842 at \d+\.\d% of weight/);
assert.match(e.footer.text, /every hour from \/api\/live/);
assert.equal(hashOriginDate, null);
assert.equal(snap.height, 141642);
assert.ok(embedLength(e) < 700, 'the feed stays small');
});
test('feed: the second feed computes blocks per second over its own window', () => {
const first = shapeFeed(data(), null);
const prev = { ...first.snapshot, at: new Date(NOW.getTime() - 3600e3).toISOString(), block_count: first.snapshot.block_count - 3600 };
const second = shapeFeed(data(), prev);
assert.match(second.payload.embeds[0].fields[0].value, /1\.00 blocks\/s over 1 h/);
});
test('feed: the hash-origin line comes from live.state.hash_origin, once per report date, and never estimates a missing field', () => {
const d = data();
assert.equal(hashOriginLine(d.live), null);
d.live.state.hash_origin = { date: '2026-10-07', keys: { with_block: 113, above_dust_30d: 95, top10_share: 0.45 }, fleet: { share: 0.384 }, pools: { attested: 0 } };
const ho = hashOriginLine(d.live);
assert.equal(ho.date, '2026-10-07');
assert.equal(ho.text, '113 keys found a block, 95 above dust, ten largest 45.0% of blocks, project fleet 38.4%, 0 attested pools. Full report in #numbers.');
const { payload, hashOriginDate } = shapeFeed(d, null);
assert.equal(hashOriginDate, '2026-10-07');
assert.equal(payload.embeds[0].fields[4].name, 'Hash origin, 2026-10-07');
delete d.live.state.hash_origin.fleet;
assert.doesNotMatch(hashOriginLine(d.live).text, /fleet/);
});
test('milestones: none on the first feed; every 100,000 blocks, 10,000 locks and 10,000 shards; the first crossing of a key count and a hash-rate step; each once', () => {
const base = { height: 99990, locked_index: 9998, paid_shards_total: 19990, miners_10m: 90, hashrate: 0.9e9 };
assert.deepEqual(feedMilestones(base, null), []);
const next = { height: 200010, locked_index: 10002, paid_shards_total: 20001, miners_10m: 260, hashrate: 1.2e9 };
const ms = feedMilestones(next, base);
assert.deepEqual(ms.map(m => m.key), ['milestone:height:100000', 'milestone:height:200000', 'milestone:locks:10000', 'milestone:shards:20000', 'milestone:keys:100', 'milestone:keys:250', 'milestone:hashrate:1000000000']);
assert.match(ms[0].text, /Chain block 100,000/);
assert.match(ms[6].text, /passed 1\.00 GH\/s/);
assert.deepEqual(feedMilestones(next, next), []);
for (const m of ms) guardPayload(shapeMilestone(m.text, NOW));
assert.deepEqual(feedMilestones({ height: null }, { height: 1 }), []);
assert.equal(FEED_MILESTONES.height, 100000);
});
test('feed: runFeed posts the feed, then the milestones, seeds the snapshot, and posts the hash-origin field once a day', async () => {
const dir = tmpDir();
const p = new Poster({ live: false, outDir: dir, stateFile: path.join(dir, 'state.json'), log: () => {} });
const d = data();
d.live.state.hash_origin = { date: '2026-10-06', keys: { with_block: 1, above_dust_30d: 1, top10_share: 1 } };
const r1 = await runFeed(p, d, 'feed:t:1');
assert.equal(r1.length, 1, 'no milestone on the seed');
assert.equal(p.state.feed_last.height, 141642);
assert.equal(p.state.feed_hash_origin_date, '2026-10-06');
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'feed_t_1.json'), 'utf8')).payload.embeds[0].fields.length, 5);
const d2 = data(); d2.now = at(NOW, 3600); d2.live.state.hash_origin = d.live.state.hash_origin; d2.stats.miners_10m = 120; d2.live.state.miners_10m = 120;
const r2 = await runFeed(p, d2, 'feed:t:2');
assert.equal(r2.length, 2, 'the feed and one milestone (keys 100)');
assert.ok(fs.existsSync(path.join(dir, 'milestone_keys_100.json')));
assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'feed_t_2.json'), 'utf8')).payload.embeds[0].fields.length, 4, 'the same report date is not repeated');
assert.equal((await runFeed(p, d2, 'feed:t:2'))[0].skipped, true);
});
test('feed: the scheduler adds the hourly feed only when asked; --via updates routes the feed through the updates webhook; an unknown via is refused', async () => {
const mon = new Date('2026-10-05T08:30:00Z'); // Monday 09:30 UK
assert.deepEqual(dueNow(mon, { posts: {} }).map(d => d.kind), ['pulse', 'digest', 'weekly']);
assert.deepEqual(dueNow(mon, { posts: {} }, { feed: true }).map(d => d.kind), ['feed', 'pulse', 'digest', 'weekly']);
assert.equal(dueNow(mon, { posts: {} }, { feed: true })[0].key, 'feed:2026-10-05:09');
const dir = tmpDir();
const cred = path.join(dir, 'discord');
fs.writeFileSync(cred, 'DISCORD_WEBHOOK_NUMBERS=https://discord.com/api/webhooks/1/a\nDISCORD_WEBHOOK_UPDATES=https://discord.com/api/webhooks/2/b\n', { mode: 0o600 });
const calls = [];
const fetchImpl = async (url) => { calls.push(url); return { ok: true, status: 200, headers: new Map(), json: async () => ({ id: '7' }) }; };
const plain = new Poster({ live: true, credFile: cred, stateFile: path.join(dir, 's1.json'), outDir: dir, log: () => {}, fetchImpl });
assert.equal(plain.has('feed'), false, 'no DISCORD_WEBHOOK_FEED, the feed is off');
assert.deepEqual(plain.missingKeys(), ['DISCORD_WEBHOOK_ANNOUNCEMENTS', 'DISCORD_WEBHOOK_INCIDENTS'], 'the feed key is optional');
const via = new Poster({ live: true, credFile: cred, stateFile: path.join(dir, 's2.json'), outDir: dir, log: () => {}, fetchImpl, via: { feed: 'updates' } });
assert.equal(via.has('feed'), true);
const { payload } = shapeFeed(data(), null);
await via.post('feed', 'feed:via', payload);
assert.match(calls[0], /webhooks\/2\/b/);
assert.throws(() => new Poster({ live: false, outDir: dir, via: { feed: 'numbers' } }), /unknown --via/);
});