RegExp.escape is ES2026 (first shipped in Node 24); pattern.cts called it unconditionally, and the build consumes the module (scripts/gen-loop-host-contract.cjs), so npm run build failed on Node 22 and the gsd-test linux-node22 lane could not reach run_tests. Fix: prefer the built-in when present, else an in-file metachar escape — still the sole owner of escaping (#3212 invariant; lint scope unchanged). Builtin captured at module load so runtime mutation cannot flip the path. Regression: tests/pattern.test.cjs section 4 — child-process probes neuter RegExp.escape before/after require and assert match-behavior equivalence. Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -202,3 +202,50 @@ describe('migration equivalence (row-9 sweep)', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Section 4: #3498 — Node-22 fallback (no RegExp.escape) ─────────────────
|
||||
|
||||
describe('escapeRegex without RegExp.escape (#3498 Node-22 fallback)', () => {
|
||||
// `RegExp.escape` is ES2026 (first shipped in Node 24). The gsd-test matrix
|
||||
// runs a linux-node22 lane and the BUILD consumes this module
|
||||
// (scripts/gen-loop-host-contract.cjs), so the seam must work when the
|
||||
// builtin is absent. Simulated in a child process: neuter RegExp.escape
|
||||
// BEFORE require (module-load capture must select the fallback), then assert
|
||||
// match-behavior correctness — this file's doctrine (pattern TEXT differs
|
||||
// between builtin and fallback; match behavior must not).
|
||||
const { runNode, OUTCOME } = require('./helpers/process-seam.cjs');
|
||||
const path = require('node:path');
|
||||
const LIB = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'pattern.cjs');
|
||||
|
||||
const PROBE = [
|
||||
"RegExp.escape = undefined;",
|
||||
"const { escapeRegex, literalPattern } = require(" + JSON.stringify(LIB) + ");",
|
||||
"const corpus = ['a.b*c', '^dollar$', '(group|alt)', '[b]{1,2}', 'back\\\\slash', 'plain', 'q+x?y', 'a-b-c', ''];",
|
||||
"for (const v of corpus) {",
|
||||
" if (!new RegExp(escapeRegex(v)).test(v)) { console.error('self-match fail: ' + JSON.stringify(v)); process.exit(1); }",
|
||||
" if (!literalPattern(v).test(v)) { console.error('literalPattern fail: ' + JSON.stringify(v)); process.exit(1); }",
|
||||
"}",
|
||||
"if (new RegExp(escapeRegex('a.b*c')).test('aXbZc')) { console.error('metachar reinterpreted'); process.exit(1); }",
|
||||
"if (new RegExp(escapeRegex('(a|b)')).test('a')) { console.error('alternation reinterpreted'); process.exit(1); }",
|
||||
"console.log('fallback-ok');",
|
||||
].join('\n');
|
||||
|
||||
test('builds and escapes correctly when RegExp.escape is absent (Node 22 semantics)', () => {
|
||||
const r = runNode(['-e', PROBE], { timeoutMs: 30_000 });
|
||||
assert.strictEqual(r.outcome, OUTCOME.EXITED, `probe must run: ${r.stderr}`);
|
||||
assert.strictEqual(r.exitCode, 0, `fallback path failed: ${r.stdout}\n${r.stderr}`);
|
||||
assert.match(r.stdout, /fallback-ok/);
|
||||
});
|
||||
|
||||
test('neutering AFTER require must not flip the path mid-process (capture at load)', () => {
|
||||
const PROBE2 = [
|
||||
"const { escapeRegex } = require(" + JSON.stringify(LIB) + ");",
|
||||
"RegExp.escape = undefined;",
|
||||
"if (!new RegExp(escapeRegex('a.b')).test('a.b')) { console.error('post-load neuter broke escaping'); process.exit(1); }",
|
||||
"console.log('capture-ok');",
|
||||
].join('\n');
|
||||
const r = runNode(['-e', PROBE2], { timeoutMs: 30_000 });
|
||||
assert.strictEqual(r.outcome, OUTCOME.EXITED, `probe must run: ${r.stderr}`);
|
||||
assert.strictEqual(r.exitCode, 0, `post-load capture failed: ${r.stdout}\n${r.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user