From 253c051dbfefc42d427f659eac48674f05e477ac Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 9 Oct 2026 17:46:02 -0400 Subject: [PATCH] test(carriers): pin CommonJS require candidates, regex after a block, and non-import from Review on dd14493e found four more ways the closure gate misjudges a carrier, each reproduced against that head: - require('./helper') resolves when only helper.cjs or helper.mjs is planned, but Node's CommonJS search tries only .js, .json and .node. - In `if (ok) {} /"/.test(text); require(...)` the regex after the block is read as division and its quote swallows the require. - `obj.\nif(x) / 2` is a method call, but the condition check only saw the character immediately before `if`, so the division read as a regex and swallowed a later require. Introduced by the previous fix. - A tagged template named `from` was recorded as an import; with exact import resolution it now fails a carrier that never loads that path. Introduced by the previous fix. Controls: an extensionless require still resolves to a .json file, and a template literal passed to import() is still a dependency. --- .../scripts/validate-carrier-closure.test.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/scripts/validate-carrier-closure.test.js b/tests/scripts/validate-carrier-closure.test.js index 22430c0f0..5c3c25a04 100644 --- a/tests/scripts/validate-carrier-closure.test.js +++ b/tests/scripts/validate-carrier-closure.test.js @@ -118,6 +118,32 @@ test('an extensionless require still resolves through the CommonJS extension sea assert.equal(result.status, 'success', JSON.stringify(result.failures)); })); +// An extensionless require searches only .js, .json and .node, then a +// directory's index; .cjs and .mjs must be named explicitly. +for (const extension of ['cjs', 'mjs']) { + test(`an extensionless require does not resolve to a .${extension} file`, () => withFixture(root => { + write(root, `skills/ecc-guide/scripts/helper.${extension}`, 'module.exports = 1;\n'); + plantSkillScript(root, "module.exports = require('./helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'failure'); + assert.ok(result.failures.every(failure => failure.reason === 'unplanned-target')); + })); +} + +test('an extensionless require resolves to a .json file', () => withFixture(root => { + write(root, 'skills/ecc-guide/scripts/helper.json', '{}\n'); + plantSkillScript(root, "module.exports = require('./helper');\n"); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); +})); + +test('a tagged template named from is not an import', () => withFixture(root => { + plantSkillScript(root, 'const from = parts => parts[0];\nmodule.exports = from`./helper`;\n'); + const result = validator().validate(root); + assert.equal(result.status, 'success', JSON.stringify(result.failures)); + assert.deepEqual(result.warnings, []); +})); + test('a backslash-separated relative require fails: Linux and macOS read it as one filename', () => withFixture(root => { write(root, 'skills/ecc-guide/scripts/lib/helper.js', 'module.exports = 1;\n'); plantSkillScript(root, "module.exports = require('./lib\\\\helper.js');\n"); @@ -188,6 +214,13 @@ const SCANNER_CASES = [ ['regex after a while condition', "while (more) /\"/.test(s); require('./after-while');", ['./after-while']], ['division after a method named if', "const v = a.if(x) / 2, q = \"/\"; require('./after-method');", ['./after-method']], ['division after a call inside a condition', "if (f(a) / 2 > 1) { q = \"/\"; } require('./after-nested');", ['./after-nested']], + ['regex after a block', "if (ok) {} /\"/.test(text); require('./after-block');", ['./after-block']], + ['division after a method named if across a line break', + "const n = obj.\nif(x) / 2, q = \"/\"; require('./after-spaced-method');", ['./after-spaced-method']], + ['division after an optional-chained method named while', + "const n = obj?. while(x) / 2, q = \"/\"; require('./after-optional');", ['./after-optional']], + ['a tagged template named from', 'const from = p => p[0]; const s = from`./helper`;', []], + ['a template import() is still a dependency', 'import(`./templated`);', ['./templated']], ]; for (const [label, source, expected] of SCANNER_CASES) {