mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
config/alibaba-free-tier-allowlist.json carried "validUntil": "2026-08-27". On the 28th the loader started rejecting it — correctly, that is the design — and a test that asserted "the shipped pack loads" turned every PR and main red with no commit involved (#11866). A time bomb: the one class of defect a diff review can never catch, because there is no diff. scripts/check/lib/configExpiry.mjs walks config/**/*.json for validUntil / validTo / expiresAt / expiry / expires (and snake_case forms), parses the dates, and classifies each as expired / expiring (< 7 days) / ok / unparseable. The repo-wide test fails on expired or expiring packs unless the file is in a small allowlist keyed to the issue that owns the renewal — and fails the OTHER way when an allowlisted pack is no longer expiring, so entries cannot go stale. A positive anchor requires at least one dated pack to be found, so a renamed key cannot silently turn the suite into a no-op. The Alibaba pack is allowlisted against #11866: whether the curated free-tier list still matches reality is an operator data decision, not a test fix. Removing that entry makes the suite fail as intended (verified).
91 lines
3.0 KiB
JavaScript
91 lines
3.0 KiB
JavaScript
/**
|
|
* 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("/"));
|
|
});
|
|
}
|