diff --git a/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md b/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md new file mode 100644 index 0000000000..9e58941cfc --- /dev/null +++ b/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md @@ -0,0 +1,4 @@ +- Added a unit test that fails seven days before any dated pack under `config/` + (`validUntil` and sibling keys) lapses, naming the file and key. The Alibaba + free-tier pack expired on 2026-08-27 and turned every PR red the next morning + with no commit involved; renewal now happens on someone's terms, not the clock's. diff --git a/scripts/check/lib/configExpiry.mjs b/scripts/check/lib/configExpiry.mjs new file mode 100644 index 0000000000..110a998c66 --- /dev/null +++ b/scripts/check/lib/configExpiry.mjs @@ -0,0 +1,90 @@ +/** + * scripts/check/lib/configExpiry.mjs + * + * Finds dated validity fields in JSON config packs so a test can fail BEFORE + * they lapse. Origin: config/alibaba-free-tier-allowlist.json carried + * `"validUntil": "2026-08-27"`; on 2026-08-28 the loader started (correctly) + * rejecting the pack and a test that asserted "the shipped pack loads" turned + * every PR and main red with no commit involved (#11866). A time bomb, not a + * regression — and the only kind of defect a diff review can never catch. + * + * Pure helpers; the repo-wide assertion lives in + * tests/unit/config-expiry-time-bomb.test.ts. + */ +import fs from "node:fs"; +import path from "node:path"; + +export const EXPIRY_KEY = + /^(validUntil|valid_until|validTo|valid_to|expiresAt|expires_at|expiry|expires)$/; +const DAY_MS = 86_400_000; + +/** + * Walks a parsed JSON value and returns every string-valued expiry field. + * @returns {{ file: string, keyPath: string, raw: string, expiresAt: number|null }[]} + */ +export function collectExpiryFields(value, file, keyPath = []) { + const out = []; + if (Array.isArray(value)) { + value.forEach((v, i) => out.push(...collectExpiryFields(v, file, [...keyPath, String(i)]))); + return out; + } + if (!value || typeof value !== "object") return out; + for (const [key, v] of Object.entries(value)) { + const kp = [...keyPath, key]; + if (EXPIRY_KEY.test(key) && typeof v === "string") { + const ms = Date.parse(v); + out.push({ file, keyPath: kp.join("."), raw: v, expiresAt: Number.isFinite(ms) ? ms : null }); + } else if (v && typeof v === "object") { + out.push(...collectExpiryFields(v, file, kp)); + } + } + return out; +} + +/** @returns {"expired"|"expiring"|"ok"|"unparseable"} */ +export function classifyExpiry(field, nowMs, warnDays = 7) { + if (field.expiresAt === null) return "unparseable"; + if (field.expiresAt < nowMs) return "expired"; + if (field.expiresAt < nowMs + warnDays * DAY_MS) return "expiring"; + return "ok"; +} + +/** All *.json under dir, recursively, skipping node_modules. Sorted for stable output. */ +export function walkJsonFiles(dir) { + const out = []; + const stack = [dir]; + while (stack.length > 0) { + const current = stack.pop(); + let entries; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const e of entries) { + const full = path.join(current, e.name); + if (e.isDirectory()) { + if (e.name !== "node_modules") stack.push(full); + } else if (e.isFile() && e.name.endsWith(".json")) { + out.push(full); + } + } + } + return out.sort(); +} + +/** + * Scans every JSON file under `dir`; `file` in the result is relative to `dir` + * with forward slashes, so allowlists can key on it portably. + */ +export function scanConfigExpiry(dir) { + return walkJsonFiles(dir).flatMap((f) => { + let parsed; + try { + parsed = JSON.parse(fs.readFileSync(f, "utf8")); + } catch { + return []; // not this scanner's job to validate JSON + } + return collectExpiryFields(parsed, path.relative(dir, f).split(path.sep).join("/")); + }); +} diff --git a/tests/unit/config-expiry-time-bomb.test.ts b/tests/unit/config-expiry-time-bomb.test.ts new file mode 100644 index 0000000000..7740e09e5c --- /dev/null +++ b/tests/unit/config-expiry-time-bomb.test.ts @@ -0,0 +1,135 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + classifyExpiry, + collectExpiryFields, + scanConfigExpiry, +} from "../../scripts/check/lib/configExpiry.mjs"; + +/** + * Time bombs: config packs with a `validUntil` (or sibling key) that lapse with + * no commit involved. The Alibaba free-tier pack expired on 2026-08-27 and from + * the 28th every PR and main carried a red Unit Tests shard (#11866). Nothing a + * diff review could have caught. + * + * This suite fails SEVEN DAYS BEFORE any pack under config/ lapses, naming the + * file and key, so renewal happens on someone's terms instead of the clock's. + */ +const ROOT = join(import.meta.dirname, "../.."); +const CONFIG_DIR = join(ROOT, "config"); +const DAY = 86_400_000; +const WARN_DAYS = 7; + +/** + * Packs known to be expired/expiring, each pinned to the issue that owns the + * renewal decision. An entry whose pack is no longer expiring FAILS below as a + * stale allowlist entry — remove it when the pack is renewed. + */ +const ALLOWLIST: Record = { + "alibaba-free-tier-allowlist.json": + "#11866 — validUntil 2026-08-27 has passed; the loader already falls back to the embedded list, and renewing the curated free-tier pack is an operator data decision, not a test fix", +}; + +const NOW = Date.UTC(2026, 7, 28); // 2026-08-28, fixed: this suite must not itself depend on the clock +const day = (offset: number) => new Date(NOW + offset * DAY).toISOString().slice(0, 10); + +test("collectExpiryFields: finds nested and array-nested expiry keys, ignores non-string values", () => { + const fields = collectExpiryFields( + { + validUntil: day(3), + nested: { expiresAt: day(30), other: "x" }, + list: [{ expiry: day(-1) }, { expires: 12345 }], + expires_at: "not a date", + }, + "pack.json" + ); + assert.deepEqual( + fields.map((f) => [f.keyPath, f.expiresAt === null ? null : "date"]), + [ + ["validUntil", "date"], + ["nested.expiresAt", "date"], + ["list.0.expiry", "date"], + ["expires_at", null], + ] + ); +}); + +test("classifyExpiry: expired / expiring inside the warning window / ok / unparseable", () => { + const f = (raw: string) => ({ + file: "p", + keyPath: "validUntil", + raw, + expiresAt: Number.isFinite(Date.parse(raw)) ? Date.parse(raw) : null, + }); + assert.equal(classifyExpiry(f(day(-1)), NOW, WARN_DAYS), "expired"); + assert.equal( + classifyExpiry(f(day(0)), NOW, WARN_DAYS), + "expiring", + "lapsing today is already too late to be 'ok'" + ); + assert.equal(classifyExpiry(f(day(6)), NOW, WARN_DAYS), "expiring"); + assert.equal(classifyExpiry(f(day(8)), NOW, WARN_DAYS), "ok"); + assert.equal(classifyExpiry(f("never"), NOW, WARN_DAYS), "unparseable"); +}); + +test("scanConfigExpiry: walks a config tree, skips node_modules and invalid JSON, keys files portably", () => { + const dir = mkdtempSync(join(tmpdir(), "cfg-expiry-")); + try { + mkdirSync(join(dir, "sub"), { recursive: true }); + mkdirSync(join(dir, "node_modules", "dep"), { recursive: true }); + writeFileSync(join(dir, "a.json"), JSON.stringify({ validUntil: day(3) })); + writeFileSync(join(dir, "sub", "b.json"), JSON.stringify({ deep: { expiresAt: day(40) } })); + writeFileSync( + join(dir, "node_modules", "dep", "c.json"), + JSON.stringify({ validUntil: day(-5) }) + ); + writeFileSync(join(dir, "broken.json"), "{ not json"); + writeFileSync(join(dir, "notes.txt"), JSON.stringify({ validUntil: day(-5) })); + const found = scanConfigExpiry(dir).map((f) => `${f.file}:${f.keyPath}`); + assert.deepEqual(found, ["a.json:validUntil", "sub/b.json:deep.expiresAt"]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test(`repo: no pack under config/ lapses within ${WARN_DAYS} days unless its renewal is tracked`, (t) => { + const fields = scanConfigExpiry(CONFIG_DIR); + // Positive anchor: the scanner must be seeing SOMETHING, or a renamed key + // would silently turn this whole suite into a no-op. + assert.ok( + fields.length >= 1, + "expected at least one dated pack under config/ (the Alibaba allowlist) — if the key was renamed, extend EXPIRY_KEY" + ); + + const failures: string[] = []; + const seenAllowlisted = new Set(); + for (const f of fields) { + const status = classifyExpiry(f, Date.now(), WARN_DAYS); + const tracked = ALLOWLIST[f.file]; + if (status === "unparseable") { + t.diagnostic(`${f.file} ${f.keyPath}="${f.raw}" is not a date — not monitored`); + continue; + } + if (status === "ok") continue; + if (tracked) { + seenAllowlisted.add(f.file); + t.diagnostic(`${f.file} ${f.keyPath}=${f.raw} is ${status} — tracked: ${tracked}`); + continue; + } + failures.push( + `${f.file} → ${f.keyPath}=${f.raw} is ${status}: renew the pack (or track it in ALLOWLIST with its issue)` + ); + } + for (const file of Object.keys(ALLOWLIST)) { + if (!seenAllowlisted.has(file)) { + failures.push( + `stale ALLOWLIST entry: ${file} is no longer expired/expiring — remove it (${ALLOWLIST[file]})` + ); + } + } + assert.deepEqual(failures, [], failures.join("\n")); +});