From 6f4de4a194d850460a3b96f0a0b7933ab3afca90 Mon Sep 17 00:00:00 2001 From: Chris Vancoillie Date: Tue, 15 Sep 2026 16:18:46 +0200 Subject: [PATCH 1/2] fix: correct jest moduleNameMapper regex escaping and release-gates test hang - .jest.config.cjs: double-escape the moduleNameMapper regex key so .js$ matches literally instead of matching any-char+js (which was incorrectly stripping extensions off .cjs/.mjs requires and breaking module resolution for ~22 test suites) - release-gates.test.js: git tag with no -m blocks on $EDITOR under tag.gpgsign=true; force unsigned commits/tags in the throwaway fixture repo - release-gates.test.js: reset cwd to the real starting directory instead of chdir("/"), which corrupted the shared Jest worker cwd for whatever test file ran next Fixes #3340 --- .jest.config.cjs | 6 ++- CHANGELOG.md | 2 + .../gates/__tests__/release-gates.test.js | 38 ++++++++++++++----- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/.jest.config.cjs b/.jest.config.cjs index c4e81792e8..442d8b37a7 100644 --- a/.jest.config.cjs +++ b/.jest.config.cjs @@ -40,7 +40,11 @@ module.exports = { '/scripts/agents/includes/sync-version.js', ], moduleNameMapper: { - '^(\.{1,2}/.*)\.js$': '$1', + // Double backslashes: this key is a JS string, and JS silently + // drops a backslash before an unrecognised escape like \. — a + // single-escaped '\.js$' compiles to the regex .js$ (wildcard + // dot), which also matches .cjs/.mjs, not just literal .js. + '^(\\.{1,2}/.*)\\.js$': '$1', }, moduleFileExtensions: ['js', 'ts', 'jsx', 'tsx', 'json'], coverageDirectory: process.env.JEST_COVERAGE_DIR || './coverage', diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce7118a80..a7a09ff3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Jest Module Resolution Wildcard Bug (#3340)** — `.jest.config.cjs`'s `moduleNameMapper` used a single-escaped `'\.js$'` regex key; JS string parsing silently dropped the backslash, compiling to a wildcard-dot regex that also matched `.cjs`/`.mjs` requires and stripped their real extension. Fixed by double-escaping (`'\\.js$'`) so only literal `.js` matches. Unblocked ~22 previously-failing test suites that couldn't load at all. ([Issue #3340](https://github.com/lightspeedwp/.github/issues/3340)) +- **release-gates.test.js Indefinite Hang** — Two bugs compounded: (1) `git tag v1.0.0` with no `-m` opens `$EDITOR` and blocks forever under a global `tag.gpgsign=true` config (forces annotated tags); (2) several `describe` blocks reset `cwd` via `process.chdir("/")` instead of the real starting directory, corrupting the shared Jest worker's cwd for whatever test file runs next (surfaced as `EACCES` in `metrics-collection-orchestrator.test.js`'s relative-path `mkdir`). Fixed by forcing unsigned commits/tags in the test's throwaway repo and resetting to the captured original cwd. ([Issue #3340](https://github.com/lightspeedwp/.github/issues/3340)) - **Dependabot Scope Fix** — Added /website npm scanning and area:dependencies labels to dependabot.yml; added on develop in #1059 but never back-ported. ([PR #3316](https://github.com/lightspeedwp/.github/pull/3316)) - **Remaining Dependabot Alerts** — Added npm scanning for 3 more unscanned lockfiles; overrode transitive lodash-es to a patched version. ([PR #3335](https://github.com/lightspeedwp/.github/pull/3335)) - **Lint Debt (#3322)** — 268 of 271 lint errors were vendored skill assets ESLint was never told to ignore; excluded them and fixed a stray installed-file mutation. Real errors now 0. ([PR #3337](https://github.com/lightspeedwp/.github/pull/3337)) diff --git a/agents/release/gates/__tests__/release-gates.test.js b/agents/release/gates/__tests__/release-gates.test.js index b92db1918e..21fbd389b9 100644 --- a/agents/release/gates/__tests__/release-gates.test.js +++ b/agents/release/gates/__tests__/release-gates.test.js @@ -21,6 +21,13 @@ const ReleaseGates = require("../release-gates.cjs"); // Test fixtures and utilities const TMP_DIR = path.join(os.tmpdir(), "release-gates-test"); +// Jest reuses one worker process across multiple test files. Resetting to +// "/" (filesystem root) instead of the real starting directory leaves the +// cwd there for whichever test file runs next in that worker, breaking any +// test that resolves a relative path (e.g. metrics-collection-orchestrator's +// default ".github/reports/metrics" storage dir, which then hits EACCES +// trying to mkdir under /). +const ORIGINAL_CWD = process.cwd(); function setupTestRepo() { if (fs.existsSync(TMP_DIR)) { @@ -33,6 +40,11 @@ function setupTestRepo() { execSync("git init"); execSync('git config user.email "test@example.com"'); execSync('git config user.name "Test User"'); + // Force plain, unsigned commits/tags for this throwaway repo, regardless + // of the developer's global git config (e.g. tag.gpgsign=true forces + // annotated tags, which then require a message and block on $EDITOR). + execSync("git config commit.gpgsign false"); + execSync("git config tag.gpgsign false"); // Create initial VERSION file fs.writeFileSync("VERSION", "1.0.0", "utf-8"); @@ -75,7 +87,7 @@ describe("GATE 1: Pre-flight Checks", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass with valid pre-flight state", () => { @@ -132,7 +144,7 @@ describe("GATE 2: Agentic Reasoning Score", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass with valid changelog (score >= 0.80)", () => { @@ -213,7 +225,7 @@ describe("GATE 3: Version Consistency", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass with valid semver (X.Y.Z)", () => { @@ -280,7 +292,7 @@ describe("GATE 4: Tag Uniqueness", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass when tag does not exist", () => { @@ -290,7 +302,7 @@ describe("GATE 4: Tag Uniqueness", () => { }); test("Should fail when tag already exists", () => { - execSync("git tag v1.0.0"); + execSync("git tag --no-sign v1.0.0"); const gates = new ReleaseGates(); gates.gate4TagUniqueness(); expect(gates.results.gate4_tag_unique.passed).toBe(false); @@ -313,6 +325,10 @@ describe("GATE 5: Authorization", () => { setupTestRepo(); }); + afterEach(() => { + process.chdir(ORIGINAL_CWD); + }); + test("Should pass for authorized actors", () => { process.env.GITHUB_ACTOR = "ash"; const gates = new ReleaseGates(); @@ -354,7 +370,7 @@ describe("GATE 6: Integrity Filter", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass when gitleaks not available", () => { @@ -374,6 +390,10 @@ describe("GATE 7: Approval Enforcement", () => { setupTestRepo(); }); + afterEach(() => { + process.chdir(ORIGINAL_CWD); + }); + test("Should auto-approve patch releases", () => { process.env.INPUT_SCOPE = "patch"; const gates = new ReleaseGates(); @@ -414,7 +434,7 @@ describe("All Gates Integration", () => { }); afterEach(() => { - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should pass all gates for valid patch release", () => { @@ -514,7 +534,7 @@ describe("Error Handling", () => { gates.gate1Preflight(); const details = gates.results.gate1_preflight.details.join("\n"); expect(details).toMatch(/not on develop/i); - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); test("Should suggest fixes", () => { @@ -524,6 +544,6 @@ describe("Error Handling", () => { gates.runAllGates(); const log = gates.getResults(); expect(log.passed).toBe(false); - process.chdir("/"); + process.chdir(ORIGINAL_CWD); }); }); From 308b625ba237a21e998f78acceba3a710c0363f7 Mon Sep 17 00:00:00 2001 From: Chris Vancoillie Date: Tue, 15 Sep 2026 16:37:06 +0200 Subject: [PATCH 2/2] fix: guard metrics-collection-orchestrator main() and harden Error Handling cwd reset - metrics-collection-orchestrator.cjs: guard the top-level main() call with require.main === module. The file is required directly by its own test suite; without the guard, requiring it for testing ran a real collection attempt and called process.exit(1) on failure, killing the entire Jest process (this was failing PR CI's check job) - release-gates.test.js: Error Handling tests called setupTestRepo() inline and only reset cwd after the final assertion, so a thrown error or failed expect skipped the reset (CodeRabbit finding). Converted to beforeEach/afterEach like every other gate block --- .../workflows/metrics-collection-orchestrator.cjs | 13 +++++++++---- .../release/gates/__tests__/release-gates.test.js | 12 ++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/scripts/workflows/metrics-collection-orchestrator.cjs b/.github/scripts/workflows/metrics-collection-orchestrator.cjs index b4b43cbfb8..0f41d39d3e 100755 --- a/.github/scripts/workflows/metrics-collection-orchestrator.cjs +++ b/.github/scripts/workflows/metrics-collection-orchestrator.cjs @@ -255,9 +255,14 @@ async function main() { process.exit(0); } -main().catch((error) => { - console.error("Fatal error:", error); - process.exit(1); -}); +// Only run as a CLI entry point — this file is also `require()`d directly +// by its own test suite, which must not trigger a real collection run or +// call process.exit() (that would kill the whole Jest process). +if (require.main === module) { + main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); + }); +} module.exports = { MetricsCollectionOrchestrator }; diff --git a/agents/release/gates/__tests__/release-gates.test.js b/agents/release/gates/__tests__/release-gates.test.js index 21fbd389b9..e278028fca 100644 --- a/agents/release/gates/__tests__/release-gates.test.js +++ b/agents/release/gates/__tests__/release-gates.test.js @@ -527,23 +527,27 @@ describe("Security", () => { // ============================================================================ describe("Error Handling", () => { - test("Should provide meaningful error messages", () => { + beforeEach(() => { setupTestRepo(); + }); + + afterEach(() => { + process.chdir(ORIGINAL_CWD); + }); + + test("Should provide meaningful error messages", () => { execSync("git checkout -b main"); const gates = new ReleaseGates(); gates.gate1Preflight(); const details = gates.results.gate1_preflight.details.join("\n"); expect(details).toMatch(/not on develop/i); - process.chdir(ORIGINAL_CWD); }); test("Should suggest fixes", () => { - setupTestRepo(); fs.unlinkSync("VERSION"); const gates = new ReleaseGates(); gates.runAllGates(); const log = gates.getResults(); expect(log.passed).toBe(false); - process.chdir(ORIGINAL_CWD); }); });