Files
OmniRoute/tests/unit/check-routing-error-guard.test.ts
Dizzle defa0f07b2 fix(routing): await stale provider-pin clears and gate swallowed errors plus fire-and-forget async (#13614)
Stale provider-pin (`clearStaleLKGP`) clears are no longer silent: the fire-and-forget promise carries a `.catch` that warns with combo, comboId and executionKey, and a `check:routing-error-guard` npm script keeps the inventory of swallowed catches in the routing hot path from growing.

Maintainer rework before merge (kept the idea, no default behavior change):
- The awaited DB writes in the fallback loop were reverted (they added latency and SQLite lock exposure on every skip); the clear stays non-blocking.
- The guard keys its allowlist by file + normalized catch body instead of line numbers (the PR's version broke on any edit) and is wired as an npm script only, not in CI; the unused stats counters were dropped.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:52:33 -03:00

134 lines
5.3 KiB
TypeScript

/**
* #13614 — scripts/check/check-routing-error-guard.mjs (npm run check:routing-error-guard).
* Frozen swallowed catches are keyed by file + body snippet, so line shifts never break
* the gate; void-async allowlist anchors must sit next to the site they cover.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const { catchSnippet, collectSwallowedCatches, evaluateSwallowedCatches, evaluateVoidAsyncSites } =
await import("../../scripts/check/check-routing-error-guard.mjs");
const FILE = "open-sse/services/combo/example.ts";
const file = (source: string, path = FILE) => ({ path, source });
const SWALLOW = "try {\n await work();\n} catch {\n pending = fallback;\n}\n";
test("a bare swallowed catch is a violation when not frozen", () => {
const { violations, stale } = evaluateSwallowedCatches([file(SWALLOW)], []);
assert.equal(violations.length, 1);
assert.match(violations[0], /example\.ts:3 :: swallowed-catch ::/);
assert.deepEqual(stale, []);
});
test("rethrowing catches, no-effect markers and chained .catch() are not swallows", () => {
const sources = [
"try {\n await work();\n} catch (err) {\n log.warn(err);\n throw err;\n}\n",
"try {\n clone = r.clone();\n} catch {\n // no-effect: clone fallback\n clone = r;\n}\n",
"const quota = await fetchQuota(id).catch(() => null);\n",
];
assert.deepEqual(collectSwallowedCatches(sources.map((s) => file(s))), []);
});
test("a frozen entry survives line shifts (keyed by snippet, not line number)", () => {
const frozen = [
{
file: FILE,
snippet: catchSnippet("\n pending = fallback;\n"),
count: 1,
reason: "fallback",
},
];
const shifted = "// a new line\n// another\n\n" + SWALLOW;
assert.deepEqual(evaluateSwallowedCatches([file(SWALLOW)], frozen), {
violations: [],
stale: [],
});
assert.deepEqual(evaluateSwallowedCatches([file(shifted)], frozen), {
violations: [],
stale: [],
});
});
test("counts: a second identical swallow is new, a removed one makes the entry stale", () => {
const frozen = [{ file: FILE, snippet: "pending = fallback;", count: 1, reason: "fallback" }];
const twice = evaluateSwallowedCatches([file(SWALLOW + SWALLOW)], frozen);
assert.equal(twice.violations.length, 1);
assert.match(twice.violations[0], /example\.ts:8 ::/);
const gone = evaluateSwallowedCatches([file("const ok = 1;\n")], frozen);
assert.deepEqual(gone.violations, []);
assert.equal(gone.stale.length, 1);
assert.match(gone.stale[0], /frozen 1, live 0/);
});
test("editing a frozen catch body re-flags it (the snippet no longer matches)", () => {
const frozen = [{ file: FILE, snippet: "pending = fallback;", count: 1, reason: "fallback" }];
const edited = SWALLOW.replace("pending = fallback;", "pending = otherFallback;");
const result = evaluateSwallowedCatches([file(edited)], frozen);
assert.equal(result.violations.length, 1);
assert.equal(result.stale.length, 1);
});
test("a frozen entry without a reason is rejected", () => {
const frozen = [{ file: FILE, snippet: "pending = fallback;", count: 1, reason: " " }];
const { violations } = evaluateSwallowedCatches([file(SWALLOW)], frozen);
assert.equal(violations.length, 1);
assert.match(violations[0], /entry needs a reason/);
});
const VOID_SITE =
"void (async () => {\n try {\n await persist();\n } catch (err) {\n log.warn('Failed to record Last Known Good Provider', err);\n }\n})();\n";
test("void async: an anchored allowlist entry covers the site", () => {
const allow = [
{ file: FILE, anchor: "Failed to record Last Known Good Provider", reason: "persist" },
];
assert.deepEqual(evaluateVoidAsyncSites([file(VOID_SITE)], allow), { violations: [], stale: [] });
});
test("void async: an unlisted site, a reasonless entry and an orphan entry all fail", () => {
const unlisted = evaluateVoidAsyncSites(
[file("void (async () => {\n await work();\n})();\n")],
[]
);
assert.equal(unlisted.violations.length, 1);
assert.match(unlisted.violations[0], /example\.ts:1 :: void-async ::/);
const reasonless = evaluateVoidAsyncSites(
[file(VOID_SITE)],
[{ file: FILE, anchor: "Failed to record Last Known Good Provider" }]
);
assert.match(reasonless.violations[0], /needs a reason/);
const orphan = evaluateVoidAsyncSites(
[file("const x = 1;\n")],
[{ file: "open-sse/services/combo/removed.ts", anchor: "gone", reason: "left over" }]
);
assert.deepEqual(orphan.violations, []);
assert.deepEqual(orphan.stale, ["open-sse/services/combo/removed.ts :: gone"]);
});
test("void async: an anchor elsewhere in the file does not cover an unrelated site", () => {
const source =
"void (async () => {\n await work();\n})();\n" +
"\n".repeat(40) +
"// Failed to record Last Known Good Provider\n";
const { violations } = evaluateVoidAsyncSites(
[file(source)],
[{ file: FILE, anchor: "Failed to record Last Known Good Provider", reason: "persist" }]
);
assert.equal(violations.length, 1);
});
test("wired as the check:routing-error-guard npm script (not a CI job)", () => {
const pkg = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;
};
assert.equal(
pkg.scripts["check:routing-error-guard"],
"node scripts/check/check-routing-error-guard.mjs"
);
});