diff --git a/gate-engine/co-occurrence/__tests__/clone-detector.test.mts b/gate-engine/co-occurrence/__tests__/clone-detector.test.mts index 4e01e68d..05d1c5e0 100644 --- a/gate-engine/co-occurrence/__tests__/clone-detector.test.mts +++ b/gate-engine/co-occurrence/__tests__/clone-detector.test.mts @@ -153,6 +153,24 @@ describe.skipIf(!HAS_JSCPD)('clone-detector --gate exit-code contract', () => { expect(run(['scan', '--gate', '--paths', tmp], {}).status).toBe(1); }); + it('exit 1 — a clone in .mts files blocks (regression: CODE_EXT dropped module-suffixed exts)', () => { + // devkit's own scanRoots are pure .mts; the old /\.(tsx?|jsx?)$/ filter discarded every + // jscpd hit in .mts/.mjs, so the gate reported 0 repo-wide — a silently vacuous gate. + const mts = mkdtempSync(join(tmpdir(), 'clone-mts-')); + writeFileSync(join(mts, 'a.mts'), `${SHARED}\nexport const A_ONLY = 1;\n`); + writeFileSync(join(mts, 'b.mts'), `${SHARED}\nexport const B_ONLY = 2;\n`); + expect(run(['scan', '--gate', '--paths', mts], {}).status).toBe(1); + rmSync(mts, { recursive: true, force: true }); + }); + + it('exit 1 — a clone in .mjs files blocks (jscpd tokenizes mjs/cjs as javascript)', () => { + const mjs = mkdtempSync(join(tmpdir(), 'clone-mjs-')); + writeFileSync(join(mjs, 'a.mjs'), `${SHARED}\nexport const A_ONLY = 1;\n`); + writeFileSync(join(mjs, 'b.mjs'), `${SHARED}\nexport const B_ONLY = 2;\n`); + expect(run(['scan', '--gate', '--paths', mjs], {}).status).toBe(1); + rmSync(mjs, { recursive: true, force: true }); + }); + it('exit 0 — no cross-file clone (clean)', () => { const clean = mkdtempSync(join(tmpdir(), 'clone-clean-')); writeFileSync(join(clean, 'solo.ts'), `${SHARED}\nexport const SOLO = 1;\n`); diff --git a/gate-engine/co-occurrence/clone-detector.mts b/gate-engine/co-occurrence/clone-detector.mts index ce493641..3399a525 100755 --- a/gate-engine/co-occurrence/clone-detector.mts +++ b/gate-engine/co-occurrence/clone-detector.mts @@ -63,8 +63,10 @@ const DEFAULTS = { ignore: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/__mocks__/**'], }; -// jscpd auto-detects formats by extension; we keep only source code clones. -const CODE_EXT = /\.(tsx?|jsx?)$/; +// jscpd auto-detects formats by extension; we keep only source code clones. Must cover the +// module-suffixed extensions (.mts/.cts/.mjs/.cjs — jscpd tokenizes all of them): devkit's own +// scanRoots are pure .mts, so excluding them made this gate report 0 repo-wide (vacuous). +const CODE_EXT = /\.([cm]?[tj]s|[tj]sx)$/; // jscpd bin resolution lives in the side-effect-free ./jscpd-bin.mts (shared with the prefix-cache // config-fingerprint, which folds the SAME resolution into the cache key). Read JSCPD_BIN here (not as