mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-10-20 18:56:24 +00:00
The first scan after the Zagros People upgrade re-confirmed all 98 accounts, one transaction per account, a block each. It ran past the 15-minute staleness window and the watchdog killed it halfway; the next interval had meanwhile started a second scan beside it, which read the same accounts before the first one's reports landed and sent them again (124 submissions for 98 accounts). On mainnet every tracked citizen's re-confirmation falls due in the same scan, every three days. - A scan group's updates go out as one utility.forceBatch. The accounts are unrelated, so one refused report no longer takes the others with it; each refused item is logged with its decoded error. - The heartbeat records progress -- a group read and, if it had anything to report, reported -- instead of a finished scan. A group that cannot be read, or whose report is refused outright, is not progress, so a mute noter still goes stale. - A periodic scan does not start while the previous one is running.
130 lines
5.3 KiB
JavaScript
130 lines
5.3 KiB
JavaScript
// node --test noter/test
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { watchSubmission, isStale, failedBatchItems } 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);
|
|
});
|
|
|
|
test("a block's events reach onInBlock before the submission resolves", async () => {
|
|
const tx = fakeTx();
|
|
let seen = null;
|
|
const p = watchSubmission(tx, {}, { ...opts, onInBlock: (events) => { seen = events; } });
|
|
await tick();
|
|
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } }, { events: ['e'] });
|
|
assert.equal(await p, '0xabc');
|
|
assert.deepEqual(seen, ['e']);
|
|
});
|
|
|
|
test('a dispatch error does not reach onInBlock', async () => {
|
|
const tx = fakeTx();
|
|
let called = false;
|
|
const p = watchSubmission(tx, {}, { ...opts, onInBlock: () => { called = true; } });
|
|
await tick();
|
|
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } }, { dispatchError: 'E' });
|
|
await assert.rejects(p, /decoded E/);
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
const ev = (section, method, data = []) => ({ event: { section, method, data } });
|
|
|
|
test('a failed batch item is reported by its position among the calls', () => {
|
|
const events = [
|
|
ev('stakingScore', 'StakingDetailsReceived'),
|
|
ev('utility', 'ItemCompleted'),
|
|
ev('utility', 'ItemFailed', ['NoterCannotAttestSelf']),
|
|
ev('stakingScore', 'StakingDetailsReceived'),
|
|
ev('utility', 'ItemCompleted'),
|
|
ev('utility', 'ItemFailed', ['NotAuthorized']),
|
|
ev('utility', 'BatchCompletedWithErrors'),
|
|
ev('system', 'ExtrinsicSuccess'),
|
|
];
|
|
assert.deepEqual(failedBatchItems(events), [
|
|
{ index: 1, error: 'NoterCannotAttestSelf' },
|
|
{ index: 3, error: 'NotAuthorized' },
|
|
]);
|
|
});
|
|
|
|
test('a batch whose every item landed reports no failure', () => {
|
|
const events = [ev('utility', 'ItemCompleted'), ev('utility', 'ItemCompleted'), ev('utility', 'BatchCompleted')];
|
|
assert.deepEqual(failedBatchItems(events), []);
|
|
});
|