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.
This commit is contained in:
montjeffrey
2026-10-09 17:46:02 -04:00
parent dd14493e98
commit 253c051dbf
@@ -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) {