From db93fd7d4ec06d9b96df17f9d10ed8aba8ed7acd Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 17 May 2026 13:42:10 +0100 Subject: [PATCH 1/2] test(backend): make the glob regression check Windows-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version called execFileSync('npx', ...). On Windows runners (and any Windows host where npx is installed as npx.cmd), node's child_process.spawn does not auto-pick the .cmd shim, so the call fails with spawnSync npx ENOENT. CI on develop went red for "Windows without plugins" because of this — Linux passed. Resolve mocha's JS entry directly via require.resolve and run it under the current node process. No shell, no .cmd resolution, identical behaviour on every platform. Also normalise the absolute paths mocha prints to POSIX-relative form so the toContain() assertions match on both Linux (which emits forward slashes) and Windows (backslashes). Verified locally that the test still fails when the package.json glob is reverted to the broken tests/backend/specs/**.ts pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/backend-tests-glob.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/tests/backend-new/specs/backend-tests-glob.test.ts b/src/tests/backend-new/specs/backend-tests-glob.test.ts index 97ce7bd4842..92a6287750e 100644 --- a/src/tests/backend-new/specs/backend-tests-glob.test.ts +++ b/src/tests/backend-new/specs/backend-tests-glob.test.ts @@ -13,21 +13,20 @@ import {execFileSync} from 'child_process'; import {readFileSync} from 'fs'; -import {join} from 'path'; +import {join, sep} from 'path'; import {describe, it, expect} from 'vitest'; const srcRoot = join(__dirname, '..', '..', '..'); const pkg = JSON.parse(readFileSync(join(srcRoot, 'package.json'), 'utf8')); // Strip `cross-env NAME=value` prefixes and the leading binary name so we -// can pass the remaining tokens straight to npx mocha --dry-run. +// invoke mocha directly with the rest of the script's arguments. const tokens = String(pkg.scripts.test).split(/\s+/); while (tokens[0] && /^[A-Z_][A-Z0-9_]*=/.test(tokens[0])) tokens.shift(); if (tokens[0] === 'cross-env') { tokens.shift(); while (tokens[0] && /^[A-Z_][A-Z0-9_]*=/.test(tokens[0])) tokens.shift(); } -// Drop the binary itself (mocha) — npx will re-add it. if (tokens[0] === 'mocha') tokens.shift(); const REQUIRED = [ @@ -38,15 +37,23 @@ const REQUIRED = [ describe('backend test glob', () => { it('discovers nested specs under tests/backend/specs/{api,admin}/', () => { + // Resolve mocha's JS entry directly and run it under the current node. + // Going through `npx` (or even via the package.json bin shim) breaks on + // Windows runners where the resolver doesn't auto-pick `.cmd`/`.bat`. + const mochaBin = require.resolve('mocha/bin/mocha.js'); const out = execFileSync( - 'npx', ['mocha', '--dry-run', '--list-files', ...tokens], + process.execPath, [mochaBin, '--dry-run', '--list-files', ...tokens], {cwd: srcRoot, encoding: 'utf8', env: {...process.env, NODE_ENV: 'production'}}, ); - // mocha --list-files prints absolute paths. Normalise to repo-relative. - const seen = out.split('\n') + // mocha --list-files prints absolute paths with platform separators. + // Normalise to repo-relative POSIX paths so the assertions match on + // both Linux and Windows runners. + const prefix = `${srcRoot}${sep}`; + const seen = out.split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean) - .map((l) => l.replace(`${srcRoot}/`, '')); + .map((l) => (l.startsWith(prefix) ? l.slice(prefix.length) : l)) + .map((l) => l.split(sep).join('/')); for (const required of REQUIRED) { expect(seen, `mocha test glob missed ${required}`).toContain(required); } From 5910b6260f448677a5bf59845726a95dd6e25a9c Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 17 May 2026 14:03:01 +0100 Subject: [PATCH 2/2] test(backend): use path.relative for cross-platform glob normalization Qodo flagged the prefix-strip + sep-split approach as brittle. On Windows runners mocha can emit paths with mixed separators or different drive-letter casing, in which case startsWith() misses the prefix and the assertions fail against absolute paths. path.relative(srcRoot, abs) handles drive-letter casing and mixed separators consistently, then a final replace([\\/]) -> '/' yields POSIX-relative paths regardless of how mocha printed them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../backend-new/specs/backend-tests-glob.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tests/backend-new/specs/backend-tests-glob.test.ts b/src/tests/backend-new/specs/backend-tests-glob.test.ts index 92a6287750e..41a3ccfcc7f 100644 --- a/src/tests/backend-new/specs/backend-tests-glob.test.ts +++ b/src/tests/backend-new/specs/backend-tests-glob.test.ts @@ -13,7 +13,7 @@ import {execFileSync} from 'child_process'; import {readFileSync} from 'fs'; -import {join, sep} from 'path'; +import {isAbsolute, join, relative} from 'path'; import {describe, it, expect} from 'vitest'; const srcRoot = join(__dirname, '..', '..', '..'); @@ -47,13 +47,16 @@ describe('backend test glob', () => { ); // mocha --list-files prints absolute paths with platform separators. // Normalise to repo-relative POSIX paths so the assertions match on - // both Linux and Windows runners. - const prefix = `${srcRoot}${sep}`; + // both Linux and Windows runners. path.relative handles drive-letter + // casing and mixed separators consistently; absolute lines that fall + // outside srcRoot (shouldn't happen with --recursive on srcRoot, but + // be defensive) are passed through untouched and would fail the + // toContain() check loudly rather than silently. const seen = out.split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean) - .map((l) => (l.startsWith(prefix) ? l.slice(prefix.length) : l)) - .map((l) => l.split(sep).join('/')); + .map((l) => (isAbsolute(l) ? relative(srcRoot, l) : l)) + .map((l) => l.split(/[\\/]/).join('/')); for (const required of REQUIRED) { expect(seen, `mocha test glob missed ${required}`).toContain(required); }