From 837ae89055f6d308633c72fca4d6b6b41290b347 Mon Sep 17 00:00:00 2001 From: Alec Muffett Date: Sun, 31 May 2026 17:43:22 +0400 Subject: [PATCH 1/2] fix(parse): drop non-finite amounts in parseLegacyShape A raw Infinity in the legacy flat-map shape (value is a bare number) was assigned to amount without finite filtering. Infinity > 0 is true, so the row survived and poisoned totalPrimary, producing a non-finite snapshot total. Route the value through num(), which clamps non-finite values to 0 so the existing !(amount > 0) guard discards the row. Surfaced by the fast-check property parseSnapshot: output always has required keys with correct types (seed -1679627146, counterexample [Infinity]) flaking the Web-static verify gate on PRs #49/#50. --- web-static/sharechain-explorer/src/pplns/parse.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web-static/sharechain-explorer/src/pplns/parse.ts b/web-static/sharechain-explorer/src/pplns/parse.ts index 73428977a..b31e96711 100644 --- a/web-static/sharechain-explorer/src/pplns/parse.ts +++ b/web-static/sharechain-explorer/src/pplns/parse.ts @@ -222,7 +222,11 @@ function parseLegacyShape(obj: Record): PplnsSnapshot { let amount: number; let mergedRaw: unknown[] = []; if (typeof v === 'number') { - amount = v; + // Drop non-finite values (Infinity, -Infinity, NaN); num() clamps + // them to 0 so the !(amount > 0) guard below discards the row. A + // raw Infinity here would otherwise poison totalPrimary. Regression: + // tests/unit/pplns-parse-properties.test.ts non-finite cases. + amount = num(v); } else if (v !== null && typeof v === 'object') { const o = v as Record; amount = num(o.amount); From 8df43247063f1df7793e65fbb44e3c4b65511257 Mon Sep 17 00:00:00 2001 From: Alec Muffett Date: Sun, 31 May 2026 17:45:06 +0400 Subject: [PATCH 2/2] test(parse): pin non-finite regression cases for parseSnapshot Explicit deterministic cases for Infinity / -Infinity / NaN across all three input shapes (legacy bare-number array, legacy object amount, new miner amount). Locks the fast-check counterexample [Infinity] (seed -1679627146) regardless of the random seed, so the Web-static verify gate no longer flakes on this class of input. --- .../tests/unit/pplns-parse-properties.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/web-static/sharechain-explorer/tests/unit/pplns-parse-properties.test.ts b/web-static/sharechain-explorer/tests/unit/pplns-parse-properties.test.ts index f9c7e9590..160eef1e4 100644 --- a/web-static/sharechain-explorer/tests/unit/pplns-parse-properties.test.ts +++ b/web-static/sharechain-explorer/tests/unit/pplns-parse-properties.test.ts @@ -274,3 +274,52 @@ test('parseMinerDetail: well-formed input produces typed output', () => { } }), { numRuns: 300 }); }); + +// ── Regression: non-finite numbers (Infinity / -Infinity / NaN) ────── +// Pinned counterexamples for the fast-check flake on PRs #49/#50 +// (seed -1679627146, shrunk input [Infinity]). A bare Infinity in the +// legacy flat-map shape was assigned to amount without finite filtering, +// so it survived the amount > 0 guard and poisoned totalPrimary. These +// explicit cases lock the three non-finite shapes regardless of seed. + +test('parseSnapshot: non-finite amounts are dropped, total stays finite', () => { + // Array shape: Object.entries([x]) -> ["0", x], hits the legacy + // bare-number branch — the exact path the property test shrank to. + for (const raw of [[Infinity], [-Infinity], [NaN]]) { + const snap = parseSnapshot(raw); + assert.ok(Number.isFinite(snap.totalPrimary), + `totalPrimary not finite for ${String(raw[0])}`); + assert.equal(snap.totalPrimary, 0); + assert.equal(snap.miners.length, 0); + // Required-keys contract (mirrors the property assertions at L107-112). + assert.equal(typeof snap.totalPrimary, 'number'); + assert.ok(Array.isArray(snap.mergedChains)); + assert.ok(snap.mergedTotals !== null && typeof snap.mergedTotals === 'object'); + assert.equal(typeof snap.schemaVersion, 'string'); + assert.ok(Array.isArray(snap.miners)); + } +}); + +test('parseSnapshot: non-finite legacy-object amounts are dropped', () => { + // Legacy { addr: { amount } } shape with a non-finite amount field. + for (const bad of [Infinity, -Infinity, NaN]) { + const snap = parseSnapshot({ miner1: { amount: bad } }); + assert.ok(Number.isFinite(snap.totalPrimary)); + assert.equal(snap.totalPrimary, 0); + assert.equal(snap.miners.length, 0); + } +}); + +test('parseSnapshot: non-finite new-shape miner amounts are dropped', () => { + // New shape: a miner row whose amount is non-finite must not appear + // and must not poison the totalPrimary fallback sum. + for (const bad of [Infinity, -Infinity, NaN]) { + const snap = parseSnapshot({ + total_primary: 0, + miners: [{ address: 'a', amount: bad, pct: 0 }], + }); + assert.ok(Number.isFinite(snap.totalPrimary)); + assert.equal(snap.totalPrimary, 0); + assert.equal(snap.miners.length, 0); + } +});