From 164043d30152b38224e6ce4331d58f34e36333a2 Mon Sep 17 00:00:00 2001 From: Abhishek Sharma Date: Fri, 18 Sep 2026 07:33:22 -0700 Subject: [PATCH] fix(ci): clear the tap.testFiles drift that reds the mutation gate on every PR (#13814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ci): clear the tap.testFiles drift that reds the gate on every PR check-mutation-test-coverage --strict fails on a pristine checkout of release/v3.8.51 with no PR diff involved, so the mutation-test-coverage gate is red on every open PR regardless of what it changes. Six covering unit tests across four mutated modules were absent from stryker.conf.json tap.testFiles, which means their mutant kills were not being counted: accountFallback.ts daily-reset-tz-threading, noauth-model-lockout sse/services/auth.ts free-badge-provider-gate, noauth-model-lockout combo/comboPredicates.ts local-token-budget-429-skips-cooldown combo/rrState.ts daily-reset-tz-threading Four distinct files — two of them cover two modules each. Inserted into the alphabetical run, matching the file's existing convention; the list has a second unsorted appended group that is left alone. After: "No drift — every covering unit test is listed in tap.testFiles", exit 0. All four files pass (31 tests) so registering them does not introduce a failing mutation run. Noticed while reviewing #13743, which targets a fifth file that has already been registered by 25bc16d87e. * fix(ci): drop a dangling tap.testFiles entry and guard against new ones Merging the release line in surfaced that stryker.conf.json still names tests/unit/plugin-sandbox-permissions.test.ts, which does not exist — one dangling path out of 428 entries, pre-existing on the base rather than introduced here. check-mutation-test-coverage already guards one direction: a test that covers a mutated module but is missing from tap.testFiles. The other direction was silent. Stryker resolves the list into its sandbox, so an entry left behind after its test file is deleted or renamed costs coverage without failing loudly — the same class of drift this PR is about, arriving from the opposite side. --- .../stryker-tapfiles-drift-release-line.md | 2 + stryker.conf.json | 3 +- .../check-mutation-test-coverage.test.ts | 45 ++++++++++++++++--- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fixes/stryker-tapfiles-drift-release-line.md diff --git a/changelog.d/fixes/stryker-tapfiles-drift-release-line.md b/changelog.d/fixes/stryker-tapfiles-drift-release-line.md new file mode 100644 index 0000000000..bf3e37a56b --- /dev/null +++ b/changelog.d/fixes/stryker-tapfiles-drift-release-line.md @@ -0,0 +1,2 @@ +- **fix(ci):** register the four unit tests whose mutant kills were not counting — `daily-reset-tz-threading`, `noauth-model-lockout`, `free-badge-provider-gate` and `local-token-budget-429-skips-cooldown` — in `stryker.conf.json` `tap.testFiles`. `check-mutation-test-coverage --strict` reported 6 missing coverings across 4 mutated modules (`accountFallback`, `sse/services/auth`, `combo/comboPredicates`, `combo/rrState`) on the release line itself with no PR diff involved, so the `mutation-test-coverage` gate was red on every open PR regardless of its contents; the gate now reports no drift +- fix(ci): drop a `tap.testFiles` entry naming a deleted test file, and guard the direction the existing drift check never covered — an entry left behind after its test is removed or renamed costs mutation coverage silently, because Stryker resolves the list into its sandbox without failing on a dangling path ([#13814](https://github.com/diegosouzapw/OmniRoute/pull/13814)) diff --git a/stryker.conf.json b/stryker.conf.json index a821949e9f..d2e063b572 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -62,6 +62,7 @@ "tests/unit/combo-pin-implicit-allowlist.test.ts", "tests/unit/combo-predicates-epoch-cooldown.test.ts", "tests/unit/combo/execute-target-gates.test.ts", + "tests/unit/daily-reset-tz-threading.test.ts", "tests/unit/moonshot-quota-writeback.test.ts", "tests/unit/accountfallback-ratelimit-400-4976.test.ts", "tests/unit/adaptive-admission-route-matrix.test.ts", @@ -331,6 +332,7 @@ "tests/unit/antigravityUpstreamError.test.ts", "tests/unit/issue-13089-roundrobin-live-ws-events.test.ts", "tests/unit/ollama-404-model-lockout-11071.test.ts", + "tests/unit/noauth-model-lockout.test.ts", "tests/unit/non-streaming-client-translate.test.ts", "tests/unit/non-streaming-provider-leg.test.ts", "tests/unit/non-streaming-sse-terminal-typescan-4459.test.ts", @@ -353,7 +355,6 @@ "tests/unit/permanent-failure-hammering-other-providers.test.ts", "tests/unit/persist-429-cooldown-account-fallback.test.ts", "tests/unit/plan3-p0.test.ts", - "tests/unit/plugin-sandbox-permissions.test.ts", "tests/unit/plugins-route-error-sanitization.test.ts", "tests/unit/probe-gate-autodisable.test.ts", "tests/unit/probe-production-path.test.ts", diff --git a/tests/unit/build/check-mutation-test-coverage.test.ts b/tests/unit/build/check-mutation-test-coverage.test.ts index acaf7f5736..a392e22068 100644 --- a/tests/unit/build/check-mutation-test-coverage.test.ts +++ b/tests/unit/build/check-mutation-test-coverage.test.ts @@ -1,5 +1,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; import { moduleFragment, testImportsModule, @@ -25,7 +27,10 @@ test("testImportsModule matches static, dynamic and require imports of the modul ); // dynamic await import, even split across lines assert.equal( - testImportsModule(`const { y } = await import(\n "../../open-sse/handlers/chatCore/headers.ts"\n);`, frag), + testImportsModule( + `const { y } = await import(\n "../../open-sse/handlers/chatCore/headers.ts"\n);`, + frag + ), true ); // require() @@ -35,7 +40,10 @@ test("testImportsModule matches static, dynamic and require imports of the modul ); // unrelated module is not matched assert.equal( - testImportsModule(`import { a } from "@omniroute/open-sse/handlers/chatCore/idempotency";`, frag), + testImportsModule( + `import { a } from "@omniroute/open-sse/handlers/chatCore/idempotency";`, + frag + ), false ); // the fragment appearing only in a comment (not an import string) is NOT a match @@ -54,11 +62,20 @@ test("findCoverageDrift flags covering unit tests absent from tap.testFiles", () const tapTestFiles = ["tests/unit/chatcore-headers.test.ts"]; const unitTests = [ // covers headers, already in tap -> not drift - { path: "tests/unit/chatcore-headers.test.ts", content: `await import("../../open-sse/handlers/chatCore/headers.ts");` }, + { + path: "tests/unit/chatcore-headers.test.ts", + content: `await import("../../open-sse/handlers/chatCore/headers.ts");`, + }, // covers headers, NOT in tap -> drift - { path: "tests/unit/no-memory-header.test.ts", content: `const { isNoMemoryRequested } = await import("../../open-sse/handlers/chatCore/headers.ts");` }, + { + path: "tests/unit/no-memory-header.test.ts", + content: `const { isNoMemoryRequested } = await import("../../open-sse/handlers/chatCore/headers.ts");`, + }, // covers idempotency, NOT in tap -> drift - { path: "tests/unit/idempo.test.ts", content: `import { x } from "@omniroute/open-sse/handlers/chatCore/idempotency";` }, + { + path: "tests/unit/idempo.test.ts", + content: `import { x } from "@omniroute/open-sse/handlers/chatCore/idempotency";`, + }, // covers nothing mutated -> ignored { path: "tests/unit/unrelated.test.ts", content: `import { z } from "@/lib/foo";` }, ]; @@ -72,3 +89,21 @@ test("findCoverageDrift flags covering unit tests absent from tap.testFiles", () // comment-only mutate entries are skipped assert.equal("_a_comment_entry" in drift, false); }); + +test("every tap.testFiles entry names a file that still exists", () => { + // The existing drift check runs one way: a test that covers a mutated module + // but is missing from tap.testFiles. The other way is silent — an entry left + // behind after its test file is deleted or renamed. Stryker resolves the list + // into its sandbox, so a dangling path costs coverage without failing loudly. + const repoRoot = path.resolve(import.meta.dirname, "../../.."); + const conf = JSON.parse(fs.readFileSync(path.join(repoRoot, "stryker.conf.json"), "utf8")) as { + tap?: { testFiles?: string[] }; + }; + const entries = conf.tap?.testFiles ?? []; + assert.ok(entries.length > 0, "stryker.conf.json declares no tap.testFiles"); + + const missing = entries + .filter((entry) => !entry.includes("*")) + .filter((entry) => !fs.existsSync(path.join(repoRoot, entry))); + assert.deepEqual(missing, [], `tap.testFiles names ${missing.length} file(s) that do not exist`); +});