From 54caa54151a22df7b1fed68a55fb7cee722a91f8 Mon Sep 17 00:00:00 2001 From: kylerankin Date: Sun, 13 Sep 2026 06:37:09 +0000 Subject: [PATCH] test: cover safeNum and loadHistory in fetch-hive-history.js The merged #947 suite already pins extractMetrics, the sole reader of the live hive payload. This closes the remaining gaps the issue named: - safeNum: finite numbers pass, everything else (NaN, +/-Infinity, strings, null/undefined, objects) coerces to undefined. - loadHistory: fresh start returns the seeded default, a valid file parses, and a corrupt file degrades to the default instead of throwing. loadHistory now takes an optional path (default OUTPUT_FILE) so it can be driven at a temp file without touching the tracked seed, and safeNum + loadHistory are added to module.exports. Closes projectbluefin/documentation#947. Signed-off-by: kylerankin --- scripts/fetch-hive-history.js | 8 +-- scripts/fetch-hive-history.test.js | 78 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/scripts/fetch-hive-history.js b/scripts/fetch-hive-history.js index 4e901af0..43394978 100644 --- a/scripts/fetch-hive-history.js +++ b/scripts/fetch-hive-history.js @@ -412,10 +412,10 @@ async function fetchContributorWeeklyStats(repos = FALLBACK_FACTORY_REPOS) { return finalizeContributorStats(acc); } -function loadHistory() { +function loadHistory(file = OUTPUT_FILE) { try { - if (fs.existsSync(OUTPUT_FILE)) { - return JSON.parse(fs.readFileSync(OUTPUT_FILE, "utf8")); + if (fs.existsSync(file)) { + return JSON.parse(fs.readFileSync(file, "utf8")); } } catch { // ignore corrupt file — start fresh @@ -571,8 +571,10 @@ module.exports = { createStatsAccumulator, extractMetrics, finalizeContributorStats, + loadHistory, MAX_WEEKLY_SERIES, MAX_WEEKS, registryHeaders, + safeNum, trackedProjectRepos, }; diff --git a/scripts/fetch-hive-history.test.js b/scripts/fetch-hive-history.test.js index c1be8025..7b293276 100644 --- a/scripts/fetch-hive-history.test.js +++ b/scripts/fetch-hive-history.test.js @@ -1,5 +1,8 @@ const test = require("node:test"); const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { tmpdir } = require("node:os"); const { accumulateRepoStats, @@ -7,8 +10,10 @@ const { createStatsAccumulator, extractMetrics, finalizeContributorStats, + loadHistory, MAX_WEEKS, registryHeaders, + safeNum, trackedProjectRepos, } = require("./fetch-hive-history.js"); @@ -434,3 +439,76 @@ test("extractMetrics survives wrong-typed containers without throwing", () => { assert.equal(metrics.mergedToday, undefined); assert.equal(metrics.medianMergeMins, undefined); }); + +// safeNum is the coercion gate behind every numeric field: it turns anything +// that is not a finite number into undefined so a chart plots a gap, not a +// garbage value. loadHistory is the read path for the tracked seed file — a +// regression here either silently drops history or crashes the run. + +test("safeNum keeps only finite numbers", () => { + assert.equal(safeNum(42), 42); + assert.equal(safeNum(0), 0); + assert.equal(safeNum(3.14), 3.14); +}); + +test("safeNum rejects non-numbers, NaN and +/- Infinity", () => { + for (const v of [ + NaN, + Infinity, + -Infinity, + "12", + null, + undefined, + {}, + [], + true, + { a: 1 }, + ]) { + assert.equal(safeNum(v), undefined, `expected undefined for ${String(v)}`); + } +}); + +test("loadHistory returns the seeded default when no file exists", () => { + const missing = path.join(tmpdir(), `hive-missing-${process.pid}.json`); + assert.equal(fs.existsSync(missing), false); + + const history = loadHistory(missing); + + assert.deepEqual(history, { + entries: [], + contributors: {}, + contributorsByRepo: {}, + contributorStats: {}, + contributorWeekStarts: [], + lastContributorFetch: null, + lastWeeklyStatsFetch: null, + }); +}); + +test("loadHistory parses a valid history file", () => { + const file = path.join(tmpdir(), `hive-valid-${process.pid}.json`); + const seed = { + entries: [{ t: 1, acmmLevel: 3 }], + contributors: { a: 5 }, + contributorWeekStarts: [1735689600], + }; + fs.writeFileSync(file, JSON.stringify(seed), "utf8"); + try { + assert.deepEqual(loadHistory(file), seed); + } finally { + fs.rmSync(file); + } +}); + +test("loadHistory starts fresh on a corrupt file instead of throwing", () => { + const file = path.join(tmpdir(), `hive-corrupt-${process.pid}.json`); + fs.writeFileSync(file, "{ not: valid json,,, ", "utf8"); + try { + const history = loadHistory(file); + assert.deepEqual(history.entries, []); + assert.deepEqual(history.contributors, {}); + assert.deepEqual(history.contributorWeekStarts, []); + } finally { + fs.rmSync(file); + } +});