Files
pezkuwi-subquery/noter/test/submit.test.mjs
T
SatoshiQaziMuhammed 1e8d5e3675 Stop one lost extrinsic from silencing the noter bot (#30)
Every report waits behind the previous one, and a submission only settled
when it reached a block. One the pool dropped, rejected or replaced never
settled, so every report queued after it waited forever. The watchdog meant
to catch a silent bot was armed only after the first scan returned, so when
that first scan was the one that hung there was no watchdog at all: the
production container logged its last line one minute after starting on
2026-10-13 and stayed up, silent, for more than a day.

- submit.js: a submission settles on a terminal status (Dropped, Invalid,
  Usurped, FinalityTimeout) or after SUBMIT_TIMEOUT_MS, and releases its
  status subscription.
- The watchdog is armed before connecting and counts from the process start
  until a scan completes, so a heartbeat left by an earlier process in the
  same container does not count for this one.
- The noter's own account is skipped: the chain refuses a noter's report
  about themselves, so it failed in every scan.
- Unit tests for both rules, run inside the noter image in CI.
2026-10-15 06:03:40 +04:00

86 lines
3.5 KiB
JavaScript

// node --test noter/test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { watchSubmission, isStale } from '../submit.js';
// A stand-in for an api-derive submittable: it hands the status callback to the test and
// records whether the subscription was released.
function fakeTx() {
const t = { emit: null, unsubscribed: false };
t.signAndSend = (_signer, cb) => {
t.emit = (flags, extra = {}) => cb({ status: { type: flags.type, ...flags }, ...extra });
return Promise.resolve(() => { t.unsubscribed = true; });
};
return t;
}
const opts = { decodeError: (e) => `decoded ${e}`, timeoutMs: 1_000 };
const tick = () => new Promise((r) => setImmediate(r));
for (const flag of ['isDropped', 'isInvalid', 'isUsurped', 'isFinalityTimeout']) {
test(`a submission that ends ${flag.slice(2)} settles instead of waiting forever`, async () => {
const tx = fakeTx();
const p = watchSubmission(tx, {}, opts);
await tick();
tx.emit({ isReady: true, type: 'Ready' });
tx.emit({ [flag]: true, type: flag.slice(2) });
await assert.rejects(p, new RegExp(flag.slice(2)));
assert.equal(tx.unsubscribed, true);
});
}
test('a submission that never reaches a block is given up after the timeout', async () => {
const tx = fakeTx();
const p = watchSubmission(tx, {}, { ...opts, timeoutMs: 50 });
await tick();
tx.emit({ isReady: true, type: 'Ready' });
await assert.rejects(p, /no block after 50 ms/);
assert.equal(tx.unsubscribed, true);
});
test('a submission in a block resolves with that block', async () => {
const tx = fakeTx();
const p = watchSubmission(tx, {}, opts);
await tick();
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } });
assert.equal(await p, '0xabc');
assert.equal(tx.unsubscribed, true);
});
test('a dispatch error in a block rejects with the decoded error', async () => {
const tx = fakeTx();
const p = watchSubmission(tx, {}, opts);
await tick();
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } }, { dispatchError: 'E' });
await assert.rejects(p, /decoded E/);
});
test('a submission the node refuses outright rejects', async () => {
const tx = { signAndSend: () => Promise.reject(new Error('1014: Priority is too low')) };
await assert.rejects(watchSubmission(tx, {}, opts), /Priority is too low/);
});
test('a later status cannot settle a submission twice', async () => {
const tx = fakeTx();
const p = watchSubmission(tx, {}, opts);
await tick();
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0x1' } });
tx.emit({ isDropped: true, type: 'Dropped' });
assert.equal(await p, '0x1');
});
test('a first scan that hangs goes stale, counted from the process start', () => {
assert.equal(isStale({ lastScanAt: NaN, startedAt: 0, now: 901, staleAfter: 900 }), true);
assert.equal(isStale({ lastScanAt: NaN, startedAt: 0, now: 899, staleAfter: 900 }), false);
});
test("an earlier process's heartbeat does not keep this one alive", () => {
// Heartbeat from long before this process started: still counted from the start.
assert.equal(isStale({ lastScanAt: 10, startedAt: 1_000, now: 1_500, staleAfter: 900 }), false);
assert.equal(isStale({ lastScanAt: 10, startedAt: 1_000, now: 1_901, staleAfter: 900 }), true);
});
test('a recent scan keeps the bot alive', () => {
assert.equal(isStale({ lastScanAt: 5_000, startedAt: 0, now: 5_500, staleAfter: 900 }), false);
assert.equal(isStale({ lastScanAt: 5_000, startedAt: 0, now: 5_901, staleAfter: 900 }), true);
});