Files
msd-core/tests/eslint-rules.test.cjs
Tom Boucher ab69b9ce56 enhance(#3987): guard slug re-derivation and the swallowed-precondition shape — §8.5 was guardable after all (#3999)
* feat(#3987): guard slug re-derivation, and record why the swallow shape cannot be guarded

Epic #3473's Decision 1 requires the wrong call site be UNREPRESENTABLE. #3984
measured that two of the nine §8 rules had no guard at all and recorded both as
"Shipped - test-covered". This closes one of them, proves the other cannot be
closed the same way, and corrects two false claims I merged yesterday.

1. §8.3 - scripts/lint-slug-derivation-drift.cjs.

   generateSlugInternal (src/core-utils.cts) is the canonical owner; #3883 removed
   11 inline copies. Nothing prevented a twelfth: no slug guard existed in
   scripts/ or eslint-rules/.

   The detector is STATEMENT-scoped and matches the shape the real copies took -
   one statement carrying BOTH .replace(<negated class>, '-') and
   .replace(/^-+|-+$/, ''). Statement scoping is what buys the precision: the
   loose LINE-level form yields 18 hits with 7 unrelated, a material
   false-positive rate. Measured on the tree: 5 flags, 2 TRUE, 3 SANCTIONED,
   0 FALSE.

   The three sanctioned sites are allowlisted with a reason each, following
   lint-phase-enumeration-drift's form rather than a bare denylist. The owner
   itself is listed explicitly even though it escapes by construction - an
   implicit escape is a latent bug, and the next person to touch line 192 would
   not know the guard depended on it.

2. Both TRUE positives were live defects, not style.

   scripts/qa-smell-ratchet.cjs reproduced the canonical formula including the
   60-cap but trimmed BEFORE truncating - the #2849 bug - and never
   transliterated. The divergence is total, not cosmetic:

     canonical  "privet-mir-privet-mir-privet-mir-privet-mir-privet-mir-prive"
     inline     "tail"

   Cyrillic collapsed to nothing and only the ASCII remainder survived, so the
   ratchet was keying on wrong identifiers for any non-ASCII input.

   tests/planning-inspect.test.cjs carried a helper whose comment claimed parity
   with getPhaseDirFromPhaseId. That function now transliterates; the helper did
   not, so the test asserted against a stale formula while looking correct. Both
   now route through the seam.

3. §8.5 - measured, and deliberately NOT shipped.

   A candidate detector (swallowing catch + errno-retry-set test in the same
   function) gives 26 flags across 11 functions: 0 TRUE, 26 FALSE. Every one is
   best-effort unlink/rm/close cleanup, lost-rename-race backoff, or a deliberate
   null fallback. The file-scoped variant is worse at 71.

   Worse than the noise: the only known true instance was removed by #3885, so
   there is NO POSITIVE CONTROL - the guard cannot be shown capable of failing,
   which this repo requires of every drift guard. Shipping it would add a guard
   nobody can trust and nobody can test.

   The ADR now records the measurement and the reason, keeps §8.5 at
   "Shipped - test-covered", and points at the #1884 regression test as what
   actually enforces it. An honest "not detectable at acceptable precision" beats
   a guard that only ever passes.

4. Two claims I merged into the ADR yesterday were wrong.

   §8.9 said 17 of 19 subsumed children have a test citing their issue number,
   and that #3364 and #3812 have none. Both halves are false, and the claim came
   from a NUMBER-GREP - inside an amendment whose own subject is that a text match
   is not a fact.

     #3364 IS cited: tests/runtime-marker-resolution.test.cjs:107,
       T3 installMarkerResolvesWhenEnvAndConfigAbsent_3897 (#3364), asserting at
       :115-119.
     #3812 IS covered: tests/gen-state-md-docs.test.cjs:374, asserting at :382.

   Corrected to 19 of 19.

   #3812 does carry a real finding, though a different one: it is PARTIALLY
   DELIVERED on a CLOSED issue. The shipped fix declares cardinality for
   frontmatter keys, but #3812's stated acceptance was about the
   ## Current Position BODY section, and docs/reference/state-md.md:196-208 still
   has no normative single-valued/overwrite sentence and no pointer to
   ## Performance Metrics for history. Recorded in the ADR and left for #3812 to
   re-open - fixing it here would bury a scope question inside an unrelated PR.

Note on B6: this ADDS a guard, and B6 said the net count must fall. #3951 already
amended that clause - a guard ledger is a claim about COVERAGE, not count - which
is what makes adding this one honest rather than contradictory.

Verified: the guard flags 0 on the fixed tree, and PROVES IT CAN FAIL - a fresh
inline copy planted in src/ makes it exit 1 naming the exact statement. All three
sanctioned sites were confirmed exempt BY the allowlist, not by accident of the
pattern, by re-attributing each to a non-exempt path and watching it flag.
build:lib, lint and lint:ci all exit 0.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3987): add the changeset fragment

Doc-only, so it carries forward from the verified sha rather than costing a
second matrix run.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3987): §8.5 IS guardable — I was wrong, and the guard found a live defect

Two orthogonal reviews. The correctness review overturned my central judgment,
and it was right.

1. I concluded §8.5 was "not detectable at acceptable precision" and recorded
   that in the ADR. False.

   My evidence was 26 flags / 0 TRUE / 26 FALSE. The reviewer pointed out what I
   had not: all 26 false positives are CLEANUP verbs - rmSync 54, unlinkSync 43,
   closeSync 17, chmodSync 12 - and the obvious narrower predicate was never
   tried. A swallowed cleanup is legitimate best-effort. A swallowed CREATION is
   a precondition silently lost, which is exactly the #1884 shape.

   Measured properly, in three stages:
     swallowing catch                                     911
     + try-block calls a CREATION verb                     24
     + enclosing function references a *_ERRNOS set         0

   0 flags, 0 false positives. The `*_ERRNOS` naming key is empirically total -
   all 10 retry/tolerate sets in src/ follow it.

   My second claim was worse. I wrote that no positive control exists because
   #3885 removed the only true instance, so the guard "cannot be shown capable of
   failing". That is self-refuting: this very PR's slug guard proves-it-can-fail
   on a synthetic tree, and the pre-#3885 blob is available as exactly such a
   fixture. It is now the control, and it works in both directions - the rule
   flags 0c43d853e^:src/planning-workspace.cts at line 210, the line the fix
   commit's own message cites, and reports zero on the post-fix code.

   I stopped at the first negative result on the option that meant less work.

   Shipped as eslint-rules/no-swallowed-precondition.cjs, wired into the existing
   src/**/*.cts ESLint block rather than a scripts/lint-*-drift.cjs: no script in
   scripts/ requires typescript/espree/acorn, and scripts/ ships to consumers, so
   a .cts-parsing standalone guard would add a devDep at consumer runtime. The
   ESLint block already parses .cts for free.

2. The guard immediately found a live defect of the same class.

   src/capability-lock.cts swallowed a mkdirSync on the lock directory, then
   acquireLock classified the follow-on failure as `code !== 'EEXIST' → return
   null`. A real EACCES/EROFS makes openSync(lockPath,'wx') fail ENOENT, which is
   not EEXIST - so a fatal filesystem error was laundered into "lock
   unavailable". Same defect as #1884, different laundering target.

   Fixed the way #3885 fixed #1884: the creation failure propagates. Regression
   test proven fail-first by hand - with the fix stashed, EACCES was laundered to
   null; restored, it throws.

   The strict rule does NOT catch this shape (its errno classification is an
   inline literal, not a named set). The rule is deliberately left strict: the
   broadened form had 2 false positives - capability-lock.cts:408, the deliberate
   EEXIST steal protocol, and commonjs-marker.cts:131, which returns a distinct
   documented outcome. The gap is noted in code rather than papered over with a
   noisy predicate.

3. The security review found the slug guard's exemption FAILED OPEN.

   currentFunction was never reset, and only a column-0 `function` declaration
   updated it, so exemption bled from an allowlisted declaration to the next one.
   generateSlugInternal exempted 50 lines for an 11-line function. A
   re-derivation planted anywhere in that window was silently exempt - the same
   fail-open shape that produced a blocker in #3897, and an allowlist is a
   SUBTRACTION so a mismatch fails open by construction.

   Extent is now tracked by real brace depth, and a test plants a violation after
   each allowlisted function's real closing brace and asserts it IS flagged.

4. Also from the security review: the guard was a CI-DoS and narrower than I
   claimed.

   Its unbounded [^\]]* was re-scanned from every `.replace(/[^` start: 54.3s on
   a 1.28MB line. It imported MAX_REGEX_LITERAL_LEN and never called
   readRegexLiteralAt - the bounded tokenizer that exists for exactly this. Now
   routed through it with a 2MB file cap: ~200ms.

   15 of 25 genuine re-derivations evaded. Widened to catch replaceAll, {1,},
   \s*-wrapped classes, escaped ], literal new RegExp(...), five trim spellings,
   .split().join(), and multi-line .replace( args - still 0 false positives.
   Two forms still evade and are documented as deliberate gaps with negative
   tests: the two-statement/temp-var form and new RegExp built from a variable.
   Both need data flow, and guessing at it is how a guard becomes noisy.

   Also fixed: // inside a string truncated the line, a ; inside the collapse
   regex split the statement (a one-character bypass), and SCAN_EXT omitted
   .mjs/.tsx/.jsx.

5. A regression I introduced, caught by the same review.

   qa-smell-ratchet.cjs top-level-required a build output that is not
   git-tracked, so the script hard-failed MODULE_NOT_FOUND before build:lib -
   including for --help, which previously had no build dependency. The require is
   now lazy at the point of use.

6. Four of my own tests were vacuous or weak.

   T9's input yielded an identical string under the buggy formula, so it passed
   on the implementation it was meant to catch. T12 compared maxLen null vs 60 on
   an 18-char name, where they agree trivially. T9-T12 all asserted
   generateSlugInternal directly, so they would pass unchanged if both call-site
   fixes were reverted. And prove-it-can-fail was scoped to scanRepo, never the
   CLI - dropping main()'s exit-code line would have kept every row green.

   All rewritten with discriminating inputs, per-call-site rows that red when the
   fix is reverted, 59/60/61 boundaries, an entirely-non-alphanumeric row, and a
   CLI row asserting the real subprocess exit code and both sanitizeForReport
   sites.

Verified: both guards flag 0 on the tree and both prove they can fail. The
swallow rule's control is confirmed in both directions - pre-#1884 shape flagged,
post-#3885 shape clean. build:lib, lint and lint:ci all exit 0.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(#3987): record that §8.5 IS guardable, and correct a correction that made a ledger worse

Three ADR corrections, two of them to text this branch wrote hours ago.

§8.5 advances to Enforced. Its previous entry said the rule was not detectable
at acceptable precision. That was wrong twice: the 26 false positives were
uniformly CLEANUP verbs, which is a reason to narrow the predicate rather than
abandon it, and the claim that no positive control exists was self-refuting - the
pre-#3885 blob is available as a fixture and this repo's own guards prove-it-can-
fail on synthetic trees. Narrowed to creation verbs plus a *_ERRNOS reference:
911 -> 24 -> 0 flags, 0 false positives, control confirmed in both directions.
The entry keeps the wrong reasoning visible, because a high false-positive count
being evidence the predicate is wrong - not evidence the rule is unguardable - is
the transferable part, and the first negative result is most seductive when it is
also the answer that means less work.

§8.9's correction is itself corrected. The original 17-of-19 claim was CORRECT
for the predicate it stated; this branch silently swapped cited -> covered and
declared 19 of 19. #3812 appears in zero test files. Changing what a word means
to make a ledger read better is a worse failure than the miscount it claimed to
repair. Both predicates are now reported separately - 18 of 19 cited, 19 of 19
covered - because §8.9 asks for a test NAMING each child, so 18 is the number
that answers it. #3812 is also re-opened for real, rather than the first draft's
promise that it could be.

§8.3 stays Shipped - test-covered rather than advancing. The slug guard catches
the copy-paste class and a dozen variants, but two forms still evade by decision
(temp-var split, new RegExp from a variable) because both need data flow. Naming
them keeps the status honest: the wrong call site is much harder to write, not
unrepresentable.

Closes #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#3987): backfill changeset pr number

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3987): replace my own wall-clock assertion, and close the guard that let me write it

CI went red on ubuntu shard 2/3. The failing test was mine, and the failure was
the test, not the code.

  a 1.28MB line ... scans in well under a second (was 54.3s pre-fix)  7368ms

It asserted ELAPSED TIME. ~200ms locally, 7.4s on a shared CI runner. The bound
introduced for the MAJOR-2 DoS fix works - 7.4s against a 54.3s pre-fix baseline
is the fix doing its job - but an absolute wall-clock threshold on shared
hardware is a race, not an assertion. CLAUDE.md says so directly: "Clock Seams:
Do not assert on wall-clock time." I wrote the anti-pattern the project bans, in
a PR about guards.

Raising the threshold would only move the flake. The row now asserts a
DETERMINISTIC bound instead: an instrumentation seam on drift-scan.cjs counts
readRegexLiteralAt calls and characters examined, and the test asserts
charsExamined stays under an absolute ceiling. Measured on the same 1.28MB
fixture: 120,000 calls, 48,000,000 chars - two orders under the ceiling. The
pathological fixture is kept; only the thing being asserted changed.

Proven to still discriminate: with MAX_REGEX_LITERAL_LEN raised to simulate the
unbounded pre-fix behavior, the same fixture does not complete in 120 seconds,
versus ~0.3s bounded. It is a real regression test, not a tautology.

Then the second half, which is the same defect class as the rest of this PR.

  eslint-rules/no-elapsed-assertion.cjs matched only the EXACT identifiers
  ^(elapsed|duration|took|ms)$.

I used `elapsedMs`. It evaded the rule entirely. tookMs, durationMs,
elapsedTime and msElapsed evade the same way. A guard that cannot see the
violation it exists to catch is exactly what this PR is about - it just happened
to be an existing rule rather than one of the two I came here for, and it was
found because I committed the violation it should have blocked.

Widened to /^(?:elapsed|duration|took|ms)(?:[A-Z]\w*)?$/ plus a narrow
start/endMs delta pair. Deliberately NOT a blanket *Ms suffix: a first draft did
that and produced 2 false positives on `timeoutMs` in
plan-phase-stall-detection, which is a configured timeout and not a measurement.
Verified negative on params, items, forms, terms, dirnames, timeoutMs,
cacheTtlMs and staleAfterMs.

Measured over the five files carrying camelCase timing identifiers: 0 true
positives beyond my own, so nothing else needed rewriting. The rule's own test
file gains a row asserting `elapsedMs` flags, proven to fail against the
pre-widening rule - the same prove-it-can-fail standard both new guards in this
PR are held to.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3987): a comment I added leaked a Claude reference into every runtime install

The runner went red with 4 failures in tests/install.test.cjs:

  Leaking: .hermes/scripts/lib/drift-scan.cjs
  Leaking: .qwen/scripts/lib/drift-scan.cjs

The instrumentation seam added for the deterministic bound carried a comment
naming CLAUDE.md as the source of the no-wall-clock-assertions rule. scripts/
SHIPS to consumers, so that comment was installed verbatim into hermes and qwen
trees, and the install suite scans for exactly this - a Claude-specific reference
reaching a non-Claude runtime.

The rule is real and worth citing; the filename is not portable. The comment now
says "this repo's test rules" and states the rule inline, which is what a reader
of an installed tree actually needs anyway.

Worth noting what caught it: not lint, and not the two guards this PR adds - the
install suite's full-tree scan, which exists precisely because a shipped file is
read by runtimes that have never heard of CLAUDE.md. Same lesson as the rest of
this PR from the other direction: the check that matters is the one that can see
the surface where the defect actually lands.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3987): a test fixture swallowed 46 git exit codes and produced a silent false negative

CI red on ubuntu shard 3/3:

  tests/health-validation.test.cjs:2029
  expected exactly one W024, got [{"code":"W006", ...}]

Not caused by this branch, and the evidence is decisive rather than a hunch: the
SIBLING test at :2039 builds the IDENTICAL fixture with the identical
commitsAhead and asserts the same thing, and it PASSED in the same process, same
file, same run. Same input, both outcomes - which rules out logic, ordering,
sharding and environment, and leaves a per-invocation nondeterministic failure
inside one fixture build.

The mechanism is an unchecked exit code, 46 times over. The W024 fixture performs
~46 runGit spawns and never checks a single one. runGit returns failures as DATA
and never throws, so one silently-failed `git commit` yields 19 commits instead
of 20, or a silently-empty `git rev-parse HEAD` yields a blank state_head. Either
drops readStateHeadFreshness below the advisory threshold, W024 never fires, and
only W006 remains.

Reproduced exactly: 20 commits -> ["W006","W024"]; 19 -> ["W006"]; blank
state_head -> ["W006"] - byte-identical to the CI assertion dump.

The arithmetic is what hid it. At threshold-1 and threshold+1 a lost commit still
produces the asserted answer; only the exactly-at-threshold cases sit one commit
from a false negative. Two of the seven tests are in that position, and CI hit
one. That is why it had never been seen before, and why it surfaced now: this
branch adds three test files, which reshuffles the cost-weighted shard partition
and moved this file into a chunk where the latent flake fired.

My files were checked as suspects first and cleared: all fixtures mkdtemp-unique,
no process.chdir, no .planning/ writes, no git spawns, and node --test gives
per-file process isolation regardless.

Fixed at the cause, not the symptom. A mustGit wrapper throws on a non-zero exit
with the command, exit code and stderr, and all nine call sites route through it.
The fixture now asserts its OWN preconditions before the assertion under test
runs - the seed head is non-empty, and `git rev-list --count <seed>..HEAD` equals
the requested commitsAhead - so a fixture that did not build what it claims fails
loudly as a FIXTURE ERROR naming got-versus-asked, instead of quietly handing a
weaker input to the assertion.

Proven: dropping one commit now raises
  FIXTURE ERROR: requested commitsAhead=19 but git rev-list --count reports 18
where it previously produced a silent ["W006"] pass-for-the-wrong-reason. 64/64
tests in that block pass unperturbed.

Deliberately NOT done: no threshold change, no retry, no loosened assertion, no
skip. The assertion was correct; the input was silently wrong.

Worth naming, because it is the same shape from the other side: this PR ships
eslint-rules/no-swallowed-precondition.cjs, whose entire subject is a swallowed
precondition failure being laundered into a plausible downstream outcome. This
fixture is that defect in test code - the swallowed git failure was laundered
into a legitimate-looking "W024 did not fire". The rule does not cover test
fixtures, so the connection is noted at the fix site rather than enforced.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#3987): two tests wrote to committed files; the shard packing decided when that mattered

CI red on windows-latest shard 1/3 only:

  "gen-exit-code-registry: CLI" > "a --write run redirected to a tmpdir leaves
  every committed artifact untouched"
  AssertionError: hooks artifact must be untouched

The Linux runner passed the same sha at 40425/40425. It is Linux-only, so a
Windows-scheduling defect is structurally invisible to it.

Root cause, established by measurement rather than inference.

tests/cli-exit.test.cjs appended a corruption marker to the REAL COMMITTED
hooks/lib/exit-code-registry.js, held it corrupted across a full subprocess, and
restored it in a finally. tests/exit-code-registry.test.cjs reads that same real
file before and after its own subprocess and asserts byte equality. If it samples
while the other test holds the file corrupted, it fails. The landmine is
pre-existing, from 2ea5efc15 (#3911).

What this branch changed is WHEN the two run together. scripts/run-tests.cjs
shards by cost-weighted LPT over the sorted unit list, so adding three test files
repacks the bins:

  merge-base c3e667df3 (838 files): cli-exit -> shard 1, exit-code-registry -> shard 3
  HEAD       03b342601 (841 files): BOTH in shard 1, same argv chunk, one
                                    node --test process, concurrent

Co-location is necessary but not sufficient - Linux shard 1/3 also had both and
passed. Windows loses because TEST_CONCURRENCY defaults to 2 there against 4
elsewhere, spawn cost is ~10x, and the sibling corruptor holds one of only two
slots through a ~90s tsc compile. That turns a sub-second overlap into seconds.

Not a path-separator or case-sensitivity issue, and not CRLF - .gitattributes
pins * text=auto eol=lf. Redirection was not at fault either: ensureScriptsOut
derives all five --out flags correctly and gen-exit-code-registry.cjs honours
them with no __dirname escape.

Fixed at the cause: no test writes to a committed file any more. Both corruptors
now copy to a mkdtempSync tmpdir, corrupt the COPY, and point the generator at
it. Repinning or reordering the shards would have turned CI green while leaving
the landmine armed for the next reshuffle.

That required closing an inconsistency between two sibling generators.
gen-exit-code-registry.cjs already accepts
--out/--scripts-out/--hooks-out/--dts-out/--sh-out and honours them under
--check; gen-hooks-cli-exit.cjs hardcoded OUTPUT_PATH and had no flag surface at
all, so its corruptor could not be redirected anywhere. It now takes --out in the
same style, honoured by both --write and --check, and is a no-op when absent -
verified: a bare --check on the default path still exits 0.

ensureScriptsOut moved to tests/helpers/exit-code-artifact-flags.cjs and both
test files import it. Hand-rolling a second copy of the flag derivation would
have been a re-derivation of exactly the kind this PR ships a guard against.

Verified: both tests still detect corruption (proven by defeating the check and
watching them red, with a positive control showing an uncorrupted copy exits 0);
SHA-256 of hooks/lib/exit-code-registry.js and hooks/lib/cli-exit.js identical
before and after running both rewritten bodies, and git reports nothing under
hooks/ modified - that is the property that was violated. A repo-wide search for
the corrupt-then-restore-in-finally shape against hooks/ found no other
instances.

One detail worth recording: the tmpdir test keeps --declaration pointed at the
real committed declaration rather than copying it, because the generated banner
embeds path.relative(REPO_ROOT, declarationPath) - copying it would produce a
false drift unrelated to the injected corruption. The declaration is read-only on
that path and never written.

Refs #3987

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:40:02 -04:00

3562 lines
123 KiB
JavaScript

'use strict';
// docs-guard-exempt: 'docs/readme.md' appears only inside literal RuleTester
// fixture `code` strings (sample source text fed to no-source-grep for AST
// linting) — this file never itself reads a real docs/ file off disk.
/**
* eslint-rules.test.cjs
*
* RuleTester unit tests for the local ESLint rules:
* - local/no-source-grep
* - local/no-magic-sleep-in-tests
* - local/no-elapsed-assertion
* - local/no-raw-rmsync-in-tests
* - local/no-adhoc-markdown-parsing
* - local/require-subprocess-timeout
* - local/require-registered-exit
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { RuleTester, ESLint } = require('eslint');
const path = require('node:path');
const fc = require('fast-check');
const noSourceGrep = require('../eslint-rules/no-source-grep.cjs');
const noMagicSleepInTests = require('../eslint-rules/no-magic-sleep-in-tests.cjs');
const noElapsedAssertion = require('../eslint-rules/no-elapsed-assertion.cjs');
const noRawRmsyncInTests = require('../eslint-rules/no-raw-rmsync-in-tests.cjs');
const noTautologicalAssert = require('../eslint-rules/no-tautological-assert.cjs');
const noAdhocMarkdownParsing = require('../eslint-rules/no-adhoc-markdown-parsing.cjs');
const noDuplicateFoldMarker = require('../eslint-rules/no-duplicate-fold-marker.cjs');
const requireSubprocessTimeout = require('../eslint-rules/require-subprocess-timeout.cjs');
const requireRegisteredExit = require('../eslint-rules/require-registered-exit.cjs');
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 2022,
sourceType: 'commonjs',
},
});
// ─── no-source-grep ──────────────────────────────────────────────────────────
describe('no-source-grep rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noSourceGrep.create, 'function');
});
test('valid: readFileSync on .md file is allowed', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(__dirname, '..', 'docs', 'readme.md'), 'utf-8');
content.includes('hello');
`,
filename: 'tests/foo.test.cjs',
},
{
code: `
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'workflows', 'config.json'), 'utf-8');
content.includes('key');
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('invalid: readFileSync on .cjs source file followed by .includes()', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'io.cjs'), 'utf-8');
src.includes('someFunction');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('invalid: readFileSync on .cjs source file followed by .match()', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'foo.cjs'), 'utf-8');
src.match(/pattern/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('valid: allow-test-rule annotation adjacent to the read exempts that site (#3508: site-scoped, not file-wide)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
// The marker sits directly above the read+search it suppresses.
code: `
const fs = require('fs');
const path = require('path');
// allow-test-rule: pending migration
const src = fs.readFileSync(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'io.cjs'), 'utf-8'); src.includes('someFunction');
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: require() of a .cjs file is allowed (not readFileSync)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const mod = require('../gsd-core/bin/lib/io.cjs');
mod.someMethod();
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
});
// ─── no-source-grep widening (#3502 / Phase 3 of #3464) ─────────────────────
//
// One RuleTester case per row of .gsd/phase/chore-3464-widen-source-grep/
// 50-test-matrix.md. Row numbers in test names refer to that matrix.
describe('no-source-grep rule — widening (#3502)', () => {
test('row 1: baseline literal .cjs read + .includes() (happy regression)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
src.includes('x');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 2: .cts source read + .match() (gap B)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const ROOT = '/repo';
const src = fs.readFileSync(path.join(ROOT, 'src', 'verification.cts'), 'utf-8');
src.match(/x/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 3: .mts source read + .match() (gap B)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const ROOT = '/repo';
const src = fs.readFileSync(path.join(ROOT, 'src', 'x.mts'), 'utf-8');
src.match(/x/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 4: .mjs source read + .match() (gap B)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const ROOT = '/repo';
const src = fs.readFileSync(path.join(ROOT, 'src', 'x.mjs'), 'utf-8');
src.match(/x/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 5: .matchAll() on a tracked read (gap A)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
src.matchAll(/x/g);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 6: regex.test(tracked) (gap A, argument-side detection)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const re = /x/;
re.test(src);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 7: /lit/.test(tracked) (gap A, argument-side detection)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
/x/.test(src);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 8: .split() / .replace() probes (gap A)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
src.split('\\n');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
src.replace(/x/, '');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 9: two-hop derived variable (gap C)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
function strip(x) { return x; }
const a = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const b = strip(a);
b.match(/x/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 10: three-hop derived variable — at the depth bound (gap C, boundary)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const a = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const b = a;
const c = b;
c.includes('x');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 11: hop chain beyond the configured depth is a documented limit (gap C, boundary)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const a = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const b = a;
const c = b;
const d = c;
d.includes('x');
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 12: shadowed same-name param — false-positive guard (gap D)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const fn = (src) => src.replace(/x/, 'y');
fn('unrelated');
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 13: same name, sibling block scopes — false-positive guard (gap D)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
{
const c = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
}
{
const c = 'x';
c.includes('y');
}
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 14: .md literal read + .includes() (negative space, unchanged)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(__dirname, '..', 'workflows', 'a.md'), 'utf-8');
content.includes('x');
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 15: .json literal read + .match() (negative space, unchanged)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.json'), 'utf-8');
content.match(/x/);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 16: dynamic path variable → .includes() — deliberately not flagged (rejected widening)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
function readIt(p) {
const content = fs.readFileSync(p, 'utf-8');
content.includes('x');
}
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 17: tracked read, no text search (negative space, unchanged)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const data = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'));
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 18: require() of a .cjs (negative space, unchanged)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const mod = require('../lib/a.cjs');
mod.someMethod();
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 19: a marker adjacent to the read+search suppresses it (#3508: site-scoped, not file-wide)', () => {
// The raw marker text is assembled via string concatenation so this
// FILE's own bytes never contain a contiguous "allow" + "-test-rule:"
// token (scripts/lint-allow-test-rule-refs.cjs does a raw whole-file
// substring scan). At RuleTester-run time the concatenation resolves to
// a real single-line comment, which the rule under test honors normally.
// The marker sits directly above the read+search (site-scoped, #3508),
// not merely somewhere earlier in the file (the pre-#3508 file-wide form
// this row originally exercised).
const marker = '// ' + 'allow' + '-test-rule: split marker for row 19, see #3502';
const code = [
"const fs = require('fs');",
"const path = require('path');",
marker,
"const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); src.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('row 20: marker text inside a string literal (not a comment) does not suppress', () => {
// Same split-marker technique as row 19, applied to a STRING literal
// (not a comment) — this row exists to prove the rule's suppression
// check only honors an actual comment, per the #3465 discriminator.
const stringMarkerLine = "const note = '" + 'allow' + "-test-rule: this is just data, not a directive';";
const code = [
"const fs = require('fs');",
"const path = require('path');",
stringMarkerLine,
"const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');",
'src.includes(note);',
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
});
// ─── no-source-grep site-scoped suppression (#3508 / Phase 4 of #3464) ──────
//
// One RuleTester case per row of
// .gsd/phase/chore-3464-site-scoped-suppression/50-test-matrix.md, rows 1-12.
// Row 4 is the one that actually proves the defect is closed: file-wide
// amnesty is gone, so a marker adjacent to one violation must NOT reach an
// unrelated violation later in the same file. Rows 3 and 6 are the
// compatibility guards (prose between marker and read; marker + zero
// violations) that must keep working or this would break the 277
// marker-bearing files that rely on file-level markers being a documented
// no-op when there's nothing to suppress.
//
// Marker text is always assembled via string concatenation (`AT` below) so
// THIS file's raw bytes never contain a contiguous "allow" + "-test-rule:"
// token — same fixture-host discipline as the row 19/20 cases above
// (scripts/lint-allow-test-rule-refs.cjs does a raw whole-file substring
// scan and must not newly count this file).
//
// NOTE: row 9's fixture length is tied to MAX_MARKER_LOOKAHEAD_LINES (8) in
// eslint-rules/no-source-grep.cjs — if that constant changes, this fixture's
// filler-line count must change with it.
// Row 12 ("marker with no #NNN") is explicitly a script-level check, not a
// RuleTester case (test-matrix.md marks it "(script, not RuleTester)") —
// it's covered by `node scripts/lint-allow-test-rule-refs.cjs` instead.
describe('no-source-grep rule — site-scoped suppression (#3508)', () => {
const AT = 'allow' + '-test-rule:';
test('row 1: marker directly above the read+search is suppressed (site-scoped)', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('row 2: marker trailing on the same line as the search is suppressed', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x'); // ${AT} reason (#1)`,
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('row 3: marker with prose lines between it and the read is still suppressed (repo real-style guard)', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
'// continuation prose line one explaining the reason',
'// continuation prose line two continuing the explanation',
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('row 4: marker adjacent to V1 does NOT reach an unrelated V2 later in the file (the defect this phase closes)', () => {
const filler = Array.from({ length: 40 }, (_, i) => `// unrelated filler line ${i + 1}, pushing V2 well past the lookahead bound`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason for V1 (#1)`,
"const s1 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s1.includes('x');",
...filler,
"const s2 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'b.cjs'), 'utf-8'); s2.includes('y');",
];
const code = lines.join('\n');
const v2Line = lines.length; // s2's line is the last line of the fixture
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
// Exactly ONE error, reported at V2 -- V1 stays suppressed, and the
// marker's reach does NOT extend to the unrelated V2 40 lines later.
errors: [{ messageId: 'noSourceGrep', line: v2Line }],
},
],
});
});
test('row 5: marker far above a violation with no marker text of its own is not suppressed', () => {
const filler = Array.from({ length: 100 }, (_, i) => `// unrelated filler line ${i + 1}`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
...filler,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
];
const code = lines.join('\n');
const violationLine = lines.length;
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep', line: violationLine }],
},
],
});
});
test('row 6: file with a marker and zero violations stays green (the 277 inert-marker files compatibility guard)', () => {
const code = [
`// ${AT} reason (#1)`,
"const fs = require('fs');",
"const path = require('path');",
"const content = fs.readFileSync(path.join(__dirname, '..', 'docs', 'readme.md'), 'utf-8');",
"content.includes('hello');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('row 7: no marker, one violation is flagged (baseline unchanged)', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 8: two violations, two adjacent markers -- per-site marking works', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason for V1 (#1)`,
"const s1 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s1.includes('x');",
`// ${AT} reason for V2 (#1)`,
"const s2 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'b.cjs'), 'utf-8'); s2.includes('y');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('row 9: marker beyond the lookahead bound does not suppress (the bound is where it claims)', () => {
// MAX_MARKER_LOOKAHEAD_LINES is 8 in eslint-rules/no-source-grep.cjs.
// 9 filler comment lines between the marker and the read pushes the gap
// to 10 lines (> 8), just past the bound.
const filler = Array.from({ length: 9 }, (_, i) => `// filler comment line ${i + 1}`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
...filler,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
];
const code = lines.join('\n');
const violationLine = lines.length;
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep', line: violationLine }],
},
],
});
});
test('row 10: marker text inside a fixture string (not a real comment) is not a directive', () => {
// The marker-looking text lives inside a STRING LITERAL in the linted
// fixture, never as a `//` comment -- ESLint's comment AST (what the
// rule inspects) never sees string-literal contents, so this must not
// suppress the real, unmarked violation below it (the #3465 lesson).
const code = [
"const fs = require('fs');",
"const path = require('path');",
`const note = 'not a directive: ${AT} fake reason';`,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes(note);",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('row 11: marker citing #NNN on the same line still suppresses (citation contract unaffected)', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason for this read (#3508)`,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8'); s.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
// ─── read-site suppression (adversarial-review fix, ITEM 1) ────────────
//
// A violation is fundamentally about a read+search PAIR. Before this fix,
// a marker adjacent to the readFileSync() call (the intuitive annotation
// spot) failed to suppress once the search happened on a later line,
// because the readFileSync assignment line itself is "real code" and
// broke comment-purity on the marker->search lookahead path. The rule now
// also checks a marker's site-scoping against the ORIGINATING read call's
// own line, independent of the marker->search path.
test('valid: marker directly above the read, search on the very next (non-comment) line', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
"const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
"src.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('valid: marker directly above the read, search several comment-pure lines later (read-line real code no longer breaks the marker->search path)', () => {
const code = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
"const src = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
'// comment-pure line one',
'// comment-pure line two',
'// comment-pure line three',
"src.includes('x');",
].join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('invalid: marker above the read suppresses that pair, but an unrelated tracked variable searched further down is still flagged', () => {
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason for V1 (#1)`,
"const s1 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
"s1.includes('x');",
"const s2 = fs.readFileSync(path.join(__dirname, '..', 'lib', 'b.cjs'), 'utf8');",
"s2.includes('y');",
];
const code = lines.join('\n');
const v2Line = lines.length; // s2.includes(...) is the last line
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep', line: v2Line }],
},
],
});
});
test('invalid: marker far from both the read and the search is still flagged', () => {
const filler = Array.from({ length: 20 }, (_, i) => `// unrelated filler line ${i + 1}`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
...filler,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
"s.includes('x');",
];
const code = lines.join('\n');
const violationLine = lines.length; // s.includes(...) is the last line
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep', line: violationLine }],
},
],
});
});
test('boundary: marker exactly MAX_MARKER_LOOKAHEAD_LINES (8) above the read is suppressed via the read-site path', () => {
// 7 comment-pure filler lines between the marker and the read puts the
// read exactly 8 lines below the marker -- the inclusive boundary.
const filler = Array.from({ length: 7 }, (_, i) => `// filler comment line ${i + 1}`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
...filler,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
"s.includes('x');",
];
const code = lines.join('\n');
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [{ code, filename: 'tests/foo.test.cjs' }],
invalid: [],
});
});
test('boundary: marker one line beyond MAX_MARKER_LOOKAHEAD_LINES (9) above the read is not suppressed', () => {
// 8 comment-pure filler lines between the marker and the read puts the
// read 9 lines below the marker -- one past the inclusive boundary.
const filler = Array.from({ length: 8 }, (_, i) => `// filler comment line ${i + 1}`);
const lines = [
"const fs = require('fs');",
"const path = require('path');",
`// ${AT} reason (#1)`,
...filler,
"const s = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf8');",
"s.includes('x');",
];
const code = lines.join('\n');
const violationLine = lines.length; // s.includes(...) is the last line
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep', line: violationLine }],
},
],
});
});
});
// ─── no-source-grep hop-propagation value-shape (adversarial-review fix) ────
//
// minTrackedHop() used to walk EVERY Identifier under a derivation's RHS
// and treat any bare reference to a tracked variable as propagating,
// regardless of whether the derived VALUE could still carry text (e.g.
// `.length`). These rows cover the value-shape gate that replaced that
// blind walk: propagate only through derivations that plausibly still
// carry the source file's text; do not propagate through scalar-producing
// shapes (member access, numeric/boolean methods, comparisons, Number()
// et al).
describe('no-source-grep rule — hop-propagation value-shape (adversarial-review fix)', () => {
test('valid: .length derivation does not propagate (reported false-positive repro)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const len = raw.length;
if (/^\\d+$/.test(len)) {}
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('invalid: numeric-returning method derivation does not cascade to a second error', () => {
// raw.indexOf('x') is itself already flagged directly (indexOf is one
// of the TEXT_METHODS this rule flags on a tracked receiver, unrelated
// to hop propagation). The important assertion here is that there is
// exactly ONE error, not two: the numeric result of .indexOf() must
// NOT stay tracked, so String(n).includes('1') is not a second finding.
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const n = raw.indexOf('x');
String(n).includes('1');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('invalid: boolean-returning method derivation does not cascade to a second error', () => {
// Same shape as above with a boolean-returning method: raw.includes('x')
// is itself already flagged directly. The boolean result must NOT stay
// tracked, so String(ok).includes('true') is not a second finding.
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const ok = raw.includes('x');
String(ok).includes('true');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('valid: comparison of a tracked derivation does not propagate', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const same = raw.length === 0;
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('invalid: string-returning method derivation still propagates and is caught', () => {
// raw.replace(...) is flagged directly (replace is a TEXT_METHOD, same
// as the indexOf/includes rows above) AND the string-returning result
// (b) correctly stays tracked, so b.includes('y') is a second, distinct
// finding. Two errors total, both real.
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const b = raw.replace(/x/, '');
b.includes('y');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }, { messageId: 'noSourceGrep' }],
},
],
});
});
test('invalid: template-literal derivation still propagates and is caught', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
const b = \`\${raw}\`;
b.match(/y/);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
test('invalid: direct .includes() on the tracked source read is unchanged (no regression)', () => {
ruleTester.run('no-source-grep', noSourceGrep, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const path = require('path');
const raw = fs.readFileSync(path.join(__dirname, '..', 'lib', 'a.cjs'), 'utf-8');
raw.includes('x');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noSourceGrep' }],
},
],
});
});
});
// ─── no-magic-sleep-in-tests ─────────────────────────────────────────────────
describe('no-magic-sleep-in-tests rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noMagicSleepInTests.create, 'function');
});
test('valid: setTimeout used outside tests (no-op since rule only applies to *.test.cjs)', () => {
// Rule only applies to *.test.cjs files; a non-test filename is always valid
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
valid: [
{
code: `
const delay = new Promise(resolve => setTimeout(resolve, 100));
`,
filename: 'scripts/some-script.cjs',
},
],
invalid: [],
});
});
test('invalid: Atomics.wait() in test file', () => {
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
valid: [],
invalid: [
{
code: `
const shared = new SharedArrayBuffer(4);
const arr = new Int32Array(shared);
Atomics.wait(arr, 0, 0, 100);
`,
filename: 'tests/some.test.cjs',
errors: [{ messageId: 'atomicsWaitSleep' }],
},
],
});
});
test('invalid: setTimeout used for synchronization in Promise in test file', () => {
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
valid: [],
invalid: [
{
code: `
async function waitABit() {
await new Promise(resolve => setTimeout(resolve, 50));
}
`,
filename: 'tests/some.test.cjs',
errors: [{ messageId: 'setTimeoutSync' }],
},
],
});
});
test('valid: setTimeout with callback (not synchronization pattern) in test file', () => {
// A setTimeout with no second arg or with a callback that does real work
// is allowed. The rule only flags the await-new-Promise(setTimeout) pattern.
ruleTester.run('no-magic-sleep-in-tests', noMagicSleepInTests, {
valid: [
{
code: `
function doSomethingLater(cb) {
setTimeout(cb, 100);
}
`,
filename: 'tests/some.test.cjs',
},
],
invalid: [],
});
});
});
// ─── no-elapsed-assertion ─────────────────────────────────────────────────────
describe('no-elapsed-assertion rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noElapsedAssertion.create, 'function');
});
test('valid: assert on non-timing property', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [
{
code: `
const assert = require('node:assert/strict');
const result = { count: 5 };
assert.equal(result.count, 5);
`,
filename: 'tests/foo.test.cjs',
},
{
code: `
const assert = require('node:assert/strict');
assert.ok(result.success);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('invalid: assert on .elapsed property', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
const result = { elapsed: 150 };
assert.ok(result.elapsed < 200);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('invalid: assert on .duration property', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.equal(stats.duration, 100);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('invalid: assert on .took property', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(result.took < 500);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('invalid: assert on .ms property', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(result.ms > 0);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('invalid: assert.equal with timing comparison', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.equal(result.elapsed > 0, true);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
// ─── #3987: camelCase/suffixed evasion (elapsedMs escaped the exact-name
// regex; CI caught the resulting flake instead of lint catching the
// anti-pattern) ───────────────────────────────────────────────────────
test('invalid: assert on .elapsedMs property (the exact identifier that evaded the pre-widening exact-name regex)', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
const result = { elapsedMs: 150 };
assert.ok(result.elapsedMs < 200);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('invalid: assert on tookMs/durationMs/msElapsed/elapsedTime/startMs/endMs — camelCase family the widened rule must catch', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [],
invalid: [
{
code: `assert.ok(x.tookMs < 500);`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
{
code: `assert.ok(x.durationMs > 0);`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
{
code: `assert.ok(x.msElapsed > 0);`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
{
code: `assert.ok(x.elapsedTime < 1000);`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
{
code: `assert.ok(x.endMs - x.startMs < 100);`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noElapsedAssertion' }],
},
],
});
});
test('valid: non-timing camelCase identifiers containing "ms" as a plain substring do not flag (params/items/forms/terms/dirnames — and a configured-bound timeoutMs)', () => {
ruleTester.run('no-elapsed-assertion', noElapsedAssertion, {
valid: [
{ code: `assert.equal(params.length, 2);`, filename: 'tests/foo.test.cjs' },
{ code: `assert.equal(items.length, 0);`, filename: 'tests/foo.test.cjs' },
{ code: `assert.ok(forms.valid);`, filename: 'tests/foo.test.cjs' },
{ code: `assert.equal(terms.length, 3);`, filename: 'tests/foo.test.cjs' },
{ code: `assert.equal(dirnames.length, 1);`, filename: 'tests/foo.test.cjs' },
{
// A configured bound (deterministic pass-through), not a measured
// wall-clock elapsed value — must not be caught by the widening.
code: `assert.equal(seen[0].timeoutMs, HOOK_FANOUT_TIMEOUT_MS);`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
});
// ─── no-raw-rmsync-in-tests ──────────────────────────────────────────────────
describe('no-raw-rmsync-in-tests rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noRawRmsyncInTests.create, 'function');
});
// ── INVALID cases (must error) ────────────────────────────────────────────
test('invalid: fs.rmSync() in a test file', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
fs.rmSync(tmpDir, { recursive: true, force: true });
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noRawRmSync' }],
},
],
});
});
test('invalid: computed member fs["rmSync"]() in a test file', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
fs['rmSync'](d, { recursive: true, force: true });
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noRawRmSync' }],
},
],
});
});
test('invalid: destructured rmSync from require("fs") in a test file', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [],
invalid: [
{
code: `
const { rmSync } = require('fs');
rmSync(d, { recursive: true, force: true });
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noRawRmSync' }],
},
],
});
});
test('invalid: aliased const del = fs.rmSync; del() in a test file', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [],
invalid: [
{
code: `
const fs = require('fs');
const del = fs.rmSync;
del(d, { recursive: true, force: true });
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noRawRmSync' }],
},
],
});
});
test('invalid: allow-test-rule annotation no longer suppresses this rule (Defect 1 fixed)', () => {
// A file with // allow-test-rule: <source-grep reason> must still error
// on raw rmSync calls. The file-level annotation is for no-source-grep only.
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [],
invalid: [
{
code: `
// allow-test-rule: source-text-is-the-product
const fs = require('fs');
fs.rmSync(d, { recursive: true, force: true });
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'noRawRmSync' }],
},
],
});
});
// ── VALID cases (must NOT error) ──────────────────────────────────────────
test('valid: helpers.cleanup() in a test file (no error)', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [
{
code: `
const { cleanup } = require('../helpers.cjs');
cleanup(tmpDir);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: bare rmSync() that is NOT fs-derived (local function) is not flagged', () => {
// A locally defined function named rmSync must not be flagged — the rule
// only tracks names that were bound from require("fs").
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [
{
code: `
const rmSync = () => {};
rmSync(d);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
// NOTE: The inline `// eslint-disable-next-line local/no-raw-rmsync-in-tests -- reason`
// escape hatch is handled entirely by ESLint's own disable-comment mechanism and
// cannot be unit-tested here via RuleTester (RuleTester runs the rule under a
// different internal namespace so the comment's rule-id doesn't match). The escape
// hatch works correctly when ESLint processes real files via `npx eslint`.
test('valid: fs.rmSync() in a non-test file (rule is inert outside *.test.cjs)', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [
{
code: `
const fs = require('fs');
fs.rmSync(tmpDir, { recursive: true, force: true });
`,
filename: 'scripts/foo.cjs',
},
],
invalid: [],
});
});
test('valid: member access / assignment without calling (not a CallExpression)', () => {
ruleTester.run('no-raw-rmsync-in-tests', noRawRmsyncInTests, {
valid: [
{
code: `
const fs = require('fs');
const orig = fs.rmSync;
fs.rmSync = orig;
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
});
// ─── no-tautological-assert ──────────────────────────────────────────────────
describe('no-tautological-assert rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noTautologicalAssert.create, 'function');
});
// ── VALID cases (must NOT error) ──────────────────────────────────────────
test('valid: assert.ok with a non-literal identifier argument', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(result);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.strictEqual with mixed literal/identifier arguments', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.strictEqual(actual, true);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.strictEqual with identifier and numeric literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.strictEqual(x, 5);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.ok with a CallExpression argument', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(fn());
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.deepStrictEqual with two identifier arguments', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.deepStrictEqual(got, expected);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.strictEqual with two different identifier arguments', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.strictEqual(a, b);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
// ── INVALID cases (must error) ────────────────────────────────────────────
test('invalid: assert.ok(true) — always-truthy boolean literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(true);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert(true) — bare assert with always-truthy boolean literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert');
assert(true);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert.ok(1) — always-truthy non-zero numeric literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(1);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert.ok("always") — always-truthy non-empty string literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok('always');
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert.ok([]) — always-truthy array literal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok([]);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert.ok(cond || true) — logical OR whose right side is true', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(cond || true);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert.strictEqual(true, true) — identical boolean literals', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.strictEqual(true, true);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalEquality' }],
},
],
});
});
test('invalid: assert.equal(1, 1) — identical numeric literals', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.equal(1, 1);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalEquality' }],
},
],
});
});
// ── Fix #3: true || cond (left-side true) ────────────────────────────────
test('invalid: assert.ok(true || x) — left side is literal true (always short-circuits)', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.ok(true || x);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
test('invalid: assert(true || y) — bare assert, left side is literal true', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert');
assert(true || y);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalTruthiness' }],
},
],
});
});
// ── Fix #4: empty [] / {} deep-equality ──────────────────────────────────
test('invalid: assert.deepStrictEqual([], []) — two empty arrays are always deep-equal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.deepStrictEqual([], []);
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalEquality' }],
},
],
});
});
test('invalid: assert.deepStrictEqual({}, {}) — two empty objects are always deep-equal', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [],
invalid: [
{
code: `
const assert = require('node:assert/strict');
assert.deepStrictEqual({}, {});
`,
filename: 'tests/foo.test.cjs',
errors: [{ messageId: 'tautologicalEquality' }],
},
],
});
});
// ── Conservative: non-empty arrays/objects must NOT be flagged ────────────
test('valid: assert.deepStrictEqual([1], [2]) — non-empty arrays with different content are not flagged', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.deepStrictEqual([1], [2]);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: assert.deepStrictEqual(got, expected) — identifier arguments are not flagged', () => {
ruleTester.run('no-tautological-assert', noTautologicalAssert, {
valid: [
{
code: `
const assert = require('node:assert/strict');
assert.deepStrictEqual(got, expected);
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
});
// ─── no-adhoc-markdown-parsing ───────────────────────────────────────────────
describe('no-adhoc-markdown-parsing rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof noAdhocMarkdownParsing.create, 'function');
});
// ── #3951 B6(b): filename-gate reach — src/**/*.cts, subdirectories included ──
// The gate used to be `/(?:^|\/)src\/[^/]+\.cts$/` (flat-only), which
// silently exempted 28 files in src/ subdirectories
// (health-diagnostic-rules/, installer-migrations/, observability/,
// host-integration-adapters/, vendor/) even though the eslint.config.mjs
// registration (src/**/*.cts) already covers them. These three rows pin
// that the gate and the registration agree — a subdirectory path is
// linted, a flat src/ path keeps working, and a path outside src/ stays
// exempt.
test('invalid: a table-regex fingerprint under a src/ SUBDIRECTORY is linted (gate reach)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'src/health-diagnostic-rules/some-check.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: the same fingerprint under a FLAT src/*.cts path still is linted (regression, not exempt)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: the same fingerprint OUTSIDE src/+tests/+scripts/ is NOT linted (gate and registration must agree)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'gsd-core/bin/lib/foo.cjs',
},
],
invalid: [],
});
});
// ── #3951 Rung B: filename-gate reach — tests/**/*.cjs and scripts/**/*.cjs ──
// The gate self-restricted to src/**/*.cts only. eslint.config.mjs also
// registers the rule on tests/**/*.cjs and scripts/**/*.cjs (Rung B); these
// rows pin that the gate and the registration agree for BOTH new globs —
// a path each registration covers must not be silently skipped by the
// gate, and a path outside all three globs stays exempt (mirrors the
// src/ subdirectory rows above, which pinned the same contract for #3951
// B6(b)).
test('invalid: a table-regex fingerprint under tests/**/*.cjs is linted (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'tests/some.test.cjs',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('invalid: a table-regex fingerprint under a tests/ SUBDIRECTORY is linted (gate reach)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'tests/fixtures/some.test.cjs',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('invalid: a table-regex fingerprint under scripts/**/*.cjs is linted (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellPattern = /\|[^|]*\|/;`,
filename: 'scripts/some-tool.cjs',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
// ── POSITIVE cases: flag fence-block-strip and section-collect ────────────
test('invalid: fence-block-strip regex with triple-backtick and multiline body', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// /```[\s\S]*?```/ — triple-backtick + [\s\S] body → flagged as fenceRegex
code: String.raw`const stripFences = /` + '```' + String.raw`[\s\S]*?` + '```' + '/;',
filename: 'src/some-module.cts',
errors: [{ messageId: 'fenceRegex' }],
},
],
});
});
test('invalid: fence-block-strip regex with triple-tilde and multiline body', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// /~~~[\s\S]*?~~~/ — triple-tilde + [\s\S] body → flagged as fenceRegex
code: String.raw`const stripTildes = /~~~[\s\S]*?~~~/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'fenceRegex' }],
},
],
});
});
test('invalid: section-collect regex with heading capture, multiline body, heading lookahead', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// /(##\s*X\n)([\s\S]*?)(?=\n##|$)/ — the classic section-collect fingerprint
code: String.raw`const pat = /(##\s*X\n)([\s\S]*?)(?=\n##|$)/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'sectionCollect' }],
},
],
});
});
// ── NEGATIVE cases: single-line fence tests and heading matches NOT flagged ─
test('valid: bare single-line fence-opener /^```/ is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: 'const fenceRegex = /^' + '```' + '/;',
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: /^\\s*(?:```|~~~)/ fence-line test is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const isFenceLine = /^\s*(?:` + '```' + String.raw`|~~~)/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: /^#\\s+/ single-line title-find is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const titleRe = /^#\s+/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: /^###\\s+(.+?)\\s*$/ single-line heading-category match is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const headingRe = /^###\s+(.+?)\s*$/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: /^(#{1,6})\\s+(.*)/ single-line heading match is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const headingM = line.match(/^(#{1,6})\s+(.*)/);`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: seam usage (no regex, just an import reference) is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `
const { collectSection } = require('./markdown-sectionizer');
const result = collectSection(content, 'Introduction');
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: annotated fence-block-strip with allow-adhoc-markdown is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
// Trailing annotation on the same line suppresses the finding
code:
'const stripFences = /```' +
String.raw`[\s\S]*?` +
'`' +
'``/; // allow-adhoc-markdown: pre-seam write path; pending migration #1372',
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// #3951 Rung B: the gate's reach is src/**/*.cts, tests/**/*.cjs and
// scripts/**/*.cjs — the same fingerprints under those three roots are now
// linted, and the negative space (a path outside all three) stays exempt.
test('invalid: fence-block-strip and section-collect fingerprints under tests/ and scripts/ are now flagged (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// Same fence-block-strip regex under tests/**/*.cjs → now linted
code: String.raw`const stripFences = /~~~[\s\S]*?~~~/;`,
filename: 'tests/some.test.cjs',
errors: [{ messageId: 'fenceRegex' }],
},
{
// Same section-collect regex under scripts/**/*.cjs → now linted
code: String.raw`const p = /(##\s*X\n)([\s\S]*?)(?=\n##|$)/;`,
filename: 'scripts/helper.cjs',
errors: [{ messageId: 'sectionCollect' }],
},
],
});
});
test('valid: the same fence-block-strip fingerprint OUTSIDE src/+tests/+scripts/ stays NOT flagged (negative space preserved)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const stripFences = /~~~[\s\S]*?~~~/;`,
filename: 'gsd-core/bin/lib/foo.cjs',
},
],
invalid: [],
});
});
// ── TABLE-REGEX (ADR-2143 §7) ──────────────────────────────────────────────
test('invalid: table-row/cell regex with escaped pipe and negated-pipe cell class', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// /\|[^|]*\|/ — the classic hand-rolled table-row/cell scan fingerprint
code: String.raw`const rowRe = /\|[^|]*\|/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('invalid: table-cell regex with escaped-pipe class variant [^\\|]', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellRe = /\|\s*([^\|]+)\s*\|/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: parseMarkdownTable() seam call is NOT flagged (no regex literal)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `
const { parseMarkdownTable } = require('./markdown-table');
const result = parseMarkdownTable(sectionText);
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: escaped pipe alone (no negated-pipe cell class) is NOT flagged', () => {
// A bare delimiter probe like /^\|/ or /\|\|/ has an escaped pipe but no
// [^|] cell-capture class — not a table-row/cell scan, so it must stay
// conservative and not fire.
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const isPipeDelim = /^\|/;`,
filename: 'src/some-module.cts',
},
{
code: String.raw`const orDelim = /a\|b/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: annotated table-regex with allow-adhoc-markdown is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const rowRe = /\|[^|]*\|/; // allow-adhoc-markdown: not a table scan, protocol-marker probe`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// #3951 Rung B: table-regex under scripts/**/*.cjs is now linted (gate/
// registration parity); the same fingerprint outside src/+tests/+scripts/
// stays exempt (negative space preserved).
test('invalid: table-regex under scripts/**/*.cjs is now flagged (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const rowRe = /\|[^|]*\|/;`,
filename: 'scripts/helper.cjs',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: the same table-regex fingerprint OUTSIDE src/+tests/+scripts/ stays NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const rowRe = /\|[^|]*\|/;`,
filename: 'gsd-core/bin/lib/foo.cjs',
},
],
invalid: [],
});
});
// ── TABLE-REGEX via new RegExp(<Literal-string | TemplateLiteral>) (#2143 Phase 4) ─
test('invalid: new RegExp(<string literal>) matching the table fingerprint', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// new RegExp('\|[^|]*\|') — doubled backslashes cook to a literal \|
code: String.raw`const rowRe = new RegExp('\\|[^|]*\\|');`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('invalid: new RegExp(<template literal>) whose STATIC quasis match the table fingerprint (dynamic segment ignored)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// new RegExp(`^(\|\s*${phase}\.?\s[^|]*(?:\|[^\n]*))$`) — the exact
// roadmap.cts/phase.cts tableRowPattern shape, dynamic ${phase} in the middle.
code: 'const tableRowPattern = new RegExp(`^(\\\\|\\\\s*${phase}\\\\.?\\\\s[^|]*(?:\\\\|[^\\\\n]*))$`, \'im\');',
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: new RegExp(`## Phase ${x}`) — dynamic heading pattern, static quasis are not table/section (non-table)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: 'const headingRe = new RegExp(`## Phase ${x}`, \'i\');',
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: new RegExp(someIdentifier) — pattern built elsewhere and referenced by variable is out of scope for this check', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `
const pattern = buildRowPattern();
const rowRe = new RegExp(pattern, 'im');
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// ── new RegExp(identifier) resolved via resolveVariableInit scope walk (#2245 recall-hole fix) ─
test('invalid: new RegExp(identifier) resolves a const-declared identifier whose pattern matches the table fingerprint', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// const tablePattern = '\|[^|]*\|' (doubled backslashes cook to a literal \|),
// then new RegExp(tablePattern) — the pattern is built one hop away via a
// const-declared identifier instead of inline, which used to escape the
// fingerprint check entirely (the recall hole this fix closes).
code: String.raw`
const tablePattern = '\\|[^|]*\\|';
const rowRe = new RegExp(tablePattern);
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: new RegExp(identifier) resolves a const-declared identifier whose pattern is NOT table-shaped', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `
const headingPattern = '## Phase';
const headingRe = new RegExp(headingPattern);
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: new RegExp(identifier) does NOT resolve a function-parameter identifier (documented boundary)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`
function buildRowRegex(tablePattern) {
return new RegExp(tablePattern);
}
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: annotated new RegExp(...) table-regex with allow-adhoc-markdown is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const rowRe = new RegExp('\\|[^|]*\\|'); // allow-adhoc-markdown: protocol-marker probe, not a table scan`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// #3951 Rung B: a new RegExp(...) table-regex under tests/**/*.cjs is now
// linted; the same fingerprint outside src/+tests/+scripts/ stays exempt.
test('invalid: new RegExp(...) table-regex under tests/**/*.cjs is now flagged (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const rowRe = new RegExp('\\|[^|]*\\|');`,
filename: 'tests/some.test.cjs',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: the same new RegExp(...) table-regex fingerprint OUTSIDE src/+tests/+scripts/ stays NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const rowRe = new RegExp('\\|[^|]*\\|');`,
filename: 'gsd-core/bin/lib/foo.cjs',
},
],
invalid: [],
});
});
// ── ADHOC-REPLACE-MUTATION: .replace() on a roadmap/state receiver (#2143 Phase 4) ─
test('invalid: roadmapContent.replace(<inline table-fingerprint literal>, ...) trips both the CallExpression and Literal detectors', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`roadmapContent.replace(/\|[^|]*\|/, 'x');`,
filename: 'src/some-module.cts',
// CallExpression is the outer/enter-first node; its Literal argument
// (visited next, on descent) is a second, independent finding.
errors: [{ messageId: 'adhocReplaceMutation' }, { messageId: 'tableRegex' }],
},
],
});
});
test('invalid: stateContent.replace(<variable holding a table-fingerprint regex>, ...) resolves the variable via scope', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`
const rowRe = /\|[^|]*\|/;
stateContent.replace(rowRe, 'x');
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }, { messageId: 'adhocReplaceMutation' }],
},
],
});
});
test('valid: withSection(...) call is NOT a .replace() and is never flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `
const { withSection } = require('./markdown-sectionizer');
const result = withSection(content, 'x', (body) => body);
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: foo.replace(/x/, "y") — neither the receiver name nor the pattern match, not flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `foo.replace(/x/, 'y');`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: state.replace(/x/, "y") — matching receiver name but non-matching pattern, not flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: `state.replace(/x/, 'y');`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: allow-adhoc-markdown suppresses an inline ad-hoc .replace() mutation', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`roadmapContent.replace(/\|[^|]*\|/, 'x'); // allow-adhoc-markdown: pre-seam write path`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// #3951 Rung B: an ad-hoc .replace() mutation under scripts/**/*.cjs is now
// linted (both the CallExpression and its Literal argument fire); the same
// fingerprint outside src/+tests/+scripts/ stays exempt.
test('invalid: .replace() ad-hoc mutation under scripts/**/*.cjs is now flagged (gate/registration parity)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`roadmapContent.replace(/\|[^|]*\|/, 'x');`,
filename: 'scripts/helper.cjs',
errors: [{ messageId: 'adhocReplaceMutation' }, { messageId: 'tableRegex' }],
},
],
});
});
test('valid: the same .replace() ad-hoc mutation fingerprint OUTSIDE src/+tests/+scripts/ stays NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`roadmapContent.replace(/\|[^|]*\|/, 'x');`,
filename: 'gsd-core/bin/lib/foo.cjs',
},
],
invalid: [],
});
});
// ── TABLE-REGEX widening: [^|\n] and escaped-pipe-plus-others classes (#2880) ──
test('invalid: content.replace(<inline [^|\\n] cell-class regex>) — the exact shape that evaded the rule before #2880', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
// /\|[^|\n]*\|/ — pipe-excluding cell class ALSO excludes newline; this
// is the src/state-document.cts shape that the sole-member-class check
// missed prior to the #2880 widening.
code: String.raw`content.replace(/\|[^|\n]*\|/, 'x');`,
filename: 'src/state-document.cts',
// CallExpression is the outer/enter-first node (adhocReplaceMutation);
// its Literal argument (visited next, on descent) is the second,
// independent tableRegex finding — same ordering as the established
// roadmapContent.replace(...) case above.
errors: [{ messageId: 'adhocReplaceMutation' }, { messageId: 'tableRegex' }],
},
],
});
});
test('invalid: factory function returning new RegExp(<template literal with [^|\\n] cell class>)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: 'function buildRowPattern() {\n return new RegExp(`\\\\|[^|\\\\n]*\\\\|`, \'im\');\n}',
filename: 'src/state-document.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('invalid: cell class with the pipe escaped alongside another excluded member [^\\|\\n]', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const cellRe = /\|[^\|\n]*\|/;`,
filename: 'src/state-document.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: negated class with NO pipe at all is not a cell scan (e.g. /^[^\\n]*$/)', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`content.replace(/^[^\n]*$/, 'x');`,
filename: 'src/state-document.cts',
},
],
invalid: [],
});
});
test('invalid: body.replace(...) — non-matching receiver name (bounded withSection callback) suppresses ONLY adhocReplaceMutation; the regex literal itself is still an independent tableRegex finding', () => {
// The ADHOC-REPLACE-MUTATION check is scoped to receivers matching
// /roadmap|state|reqContent|content/i — "body" (the withSection callback
// parameter name) does not match, so no adhocReplaceMutation fires here.
// But the standalone Literal visitor inspects EVERY regex literal in the
// file regardless of call-site context, so the pipe-excluding-class regex
// is still caught as a bare tableRegex finding either way.
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`body.replace(/\|[^|\n]*\|/, 'x');`,
filename: 'src/state-document.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('valid: allow-adhoc-markdown suppresses the widened [^|\\n] shape', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`content.replace(/\|[^|\n]*\|/, 'x'); // allow-adhoc-markdown: reason`,
filename: 'src/state-document.cts',
},
],
invalid: [],
});
});
// ── TABLE-REGEX narrowing: negated class excluding pipe + something ELSE
// is a different (non-table) idiom, not flagged (#2880 FIX 4) ───────────
test('valid: [^\\s|] (pipe excluded alongside \\s, not a pure line-terminator class) is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const re = /[^\s|]+\|cmd/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: [^"|] (pipe excluded alongside a quote) is NOT flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code: String.raw`const re = /\|[^"|]*\|/;`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('invalid: [^|\\r\\n] (pipe plus only line-terminator escapes) IS flagged', () => {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code: String.raw`const rowRe = /\|[^|\r\n]*\|/;`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
});
test('performance: a 256000-char adversarial regex-literal source does not hang the rule (ReDoS regression)', () => {
// The previous regex-based fingerprint, /\[\^[^\]]*\\?\|[^\]]*\]/, was
// quadratic on failure — an unclosed negated class of this size took
// ~23s. The single-pass scanner must stay linear. The adversarial text is
// embedded directly inside a single string-literal argument to
// `new RegExp(...)` (not built via `+` at the source-code level under
// test) so `getNewRegExpSource` actually resolves it and the scanner
// walks the full 256000-char unclosed negated class.
const bigPipeRun = '|'.repeat(256000);
const code = `const re = new RegExp('\\\\|[^${bigPipeRun}');`;
// The 256000-char input is the regression guard for the O(n^2) scan fixed
// in #2880: the pre-fix regex took ~23s on this input. There is deliberately
// no elapsed-time assertion (banned by local/no-elapsed-assertion and flaky
// by nature) — if the quadratic path is ever reintroduced this test stops
// completing, which surfaces as a suite timeout rather than a silent pass.
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [
{
code,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// ── property test: negated-pipe-class scanner (hasQualifyingNegatedPipeClass) ──
test('property: single-pass negated-class scanner verdict matches an independent reference implementation', () => {
// hasQualifyingNegatedPipeClass is a closure private to the rule's
// `create(context)` — it cannot be called directly, so it is exercised
// through the public surface: `new RegExp(<string literal>)` feeds
// `arg.value` through UNCHANGED as the "effective regex source" (see
// getNewRegExpSource), so any generated string, however malformed as a
// real regex, reaches the scanner byte-for-byte via JSON.stringify(...).
// A guaranteed literal `\|` is prepended so isTableRegexSource's OTHER
// gate (`src.includes('\\|')`) is always satisfied — the property is then
// solely a probe of the negated-class scanner's own verdict, matching the
// instruction to test the scanner in isolation.
//
// Reference implementation (independent tokenizer, NOT a copy of the
// scanner under test): tokenize `src` once into {esc, text} units,
// tracking escapes; then walk the tokens with a MONOTONIC cursor: on
// finding a `[` char-token immediately followed by a `^` char-token,
// consume forward to the first unescaped `]` (or to the end if there is
// none) as a single committed unit, decide qualification for that unit,
// and resume scanning strictly AFTER whatever was consumed — a class
// candidate found INSIDE an already-consumed (opened) class body is
// never separately reconsidered. Qualifies iff the collected body
// contains a pipe (bare `|` or escaped `\|`) AND every other member is
// one of the escapes `\n`, `\r`, `\t`.
function referenceHasQualifyingNegatedPipeClass(src) {
const tokens = [];
let i = 0;
while (i < src.length) {
if (src[i] === '\\') {
const next = i + 1 < src.length ? src[i + 1] : '';
tokens.push({ esc: true, text: next });
i += 2;
}
else {
tokens.push({ esc: false, text: src[i] });
i += 1;
}
}
let t = 0;
while (t < tokens.length) {
const opensClass = !tokens[t].esc && tokens[t].text === '['
&& t + 1 < tokens.length && !tokens[t + 1].esc && tokens[t + 1].text === '^';
if (!opensClass) {
t += 1;
continue;
}
let u = t + 2;
const members = [];
let closed = false;
while (u < tokens.length) {
const tok = tokens[u];
if (!tok.esc && tok.text === ']') {
closed = true;
u += 1;
break;
}
members.push(tok);
u += 1;
}
if (closed) {
let hasPipe = false;
let isPure = true;
for (const m of members) {
if (!m.esc && m.text === '|') {
hasPipe = true;
continue;
}
if (m.esc && m.text === '|') {
hasPipe = true;
continue;
}
if (m.esc && (m.text === 'n' || m.text === 'r' || m.text === 't')) continue;
isPure = false;
}
if (hasPipe && isPure) return true;
}
// Monotonic advance: whether this candidate qualified, failed, or
// ran off the end unclosed, never re-enter the bytes just consumed.
t = closed ? u : tokens.length;
}
return false;
}
// Composed of random characters PLUS randomly inserted `[^...]` classes
// with and without pipes (some closed, some not; some qualifying, some
// not) so both the "flagged" and "not flagged" verdicts are well
// exercised — a purely uniform character soup almost never assembles a
// well-formed `[^...|...]` class by chance.
const pipeMemberArb = fc.constantFrom('|', '\\|');
const pureFillerArb = fc.constantFrom('\\n', '\\r', '\\t');
const impureFillerArb = fc.constantFrom('a', 'Z', '1', '\\s', '\\d', '\\w', '\\\\', '-', ' ', '\\]', '\\[');
const classMemberArb = fc.oneof(pipeMemberArb, pureFillerArb, impureFillerArb);
const classBodyArb = fc.array(classMemberArb, { minLength: 1, maxLength: 3 }).map((members) => members.join(''));
const classChunkArb = fc
.record({ body: classBodyArb, closed: fc.boolean() })
.map(({ body, closed }) => '[^' + body + (closed ? ']' : ''));
const noiseCharArb = fc.constantFrom('[', ']', '^', 'x', 'y', '0', '9', ' ', '.', '-', '(', ')');
const escapeChunkArb = fc
.tuple(fc.constant('\\'), fc.constantFrom('n', 'r', 't', '|', 's', 'd', '\\', '[', ']', '^', 'a'))
.map(([bs, c]) => bs + c);
const chunkArb = fc.oneof(
{ weight: 5, arbitrary: classChunkArb },
{ weight: 2, arbitrary: escapeChunkArb },
{ weight: 2, arbitrary: noiseCharArb },
);
const srcArb = fc.array(chunkArb, { minLength: 0, maxLength: 5 }).map((chunks) => chunks.join(''));
fc.assert(
fc.property(srcArb, (fuzzed) => {
const src = '\\|' + fuzzed;
const expected = referenceHasQualifyingNegatedPipeClass(src);
const code = `const re = new RegExp(${JSON.stringify(src)});`;
if (expected) {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [],
invalid: [
{
code,
filename: 'src/some-module.cts',
errors: [{ messageId: 'tableRegex' }],
},
],
});
}
else {
ruleTester.run('no-adhoc-markdown-parsing', noAdhocMarkdownParsing, {
valid: [{ code, filename: 'src/some-module.cts' }],
invalid: [],
});
}
}),
{ numRuns: 200, seed: 2880 },
);
});
});
// ─── no-duplicate-fold-marker ────────────────────────────────────────
describe('no-duplicate-fold-marker rule', () => {
const REPO_ROOT = path.join(__dirname, '..');
/** Build a source string whose line numbers are the array indices + 1. */
const src = (...lines) => lines.join('\n');
const FOLD_A_B1 = '__foldDescribe("folded:a (consolidation epic #1969 B1 #1970)", () => {});';
const FOLD_A_B5 = '__foldDescribe("folded:a (consolidation epic #1969 B5 #1975)", () => {});';
const FOLD_B_B1 = '__foldDescribe("folded:b (consolidation epic #1969 B1 #1970)", () => {});';
test('rule module exports a create function', () => {
assert.strictEqual(typeof noDuplicateFoldMarker.create, 'function');
});
// ── Row 1: the #3271 regression, asserted against the real tree ────────────
//
// The unit cases below prove the rule can fire. THIS proves the tree it
// guards is actually clean — it is the assertion that was red before the 25
// duplicated regions were deleted (18 in install.test.cjs, 5 in
// install-minimal-hooks.test.cjs, 2 in install-write-confinement.test.cjs).
//
// Driven through the real ESLint API over the production glob rather than a
// hand-rolled scan of file text: a readFileSync + .match() scan of a .cjs
// path is exactly the shape `local/no-source-grep` bans in tests/**.
test('regression #3271: the real tests/ tree has no duplicate fold markers', async () => {
const eslint = new ESLint({
cwd: REPO_ROOT,
overrideConfigFile: true,
overrideConfig: {
files: ['tests/**/*.cjs'],
plugins: { local: { rules: { 'no-duplicate-fold-marker': noDuplicateFoldMarker } } },
languageOptions: { ecmaVersion: 2022, sourceType: 'commonjs' },
rules: { 'local/no-duplicate-fold-marker': 'error' },
},
});
const results = await eslint.lintFiles(['tests/**/*.cjs']);
// Filter to THIS rule: an ad-hoc config also surfaces "rule not found" for
// inline eslint-disable directives naming rules it does not register.
const violations = results.flatMap((r) =>
r.messages
.filter((m) => m.ruleId === 'local/no-duplicate-fold-marker')
.map((m) => `${path.relative(REPO_ROOT, r.filePath)}:${m.line} ${m.message}`),
);
// Non-vacuous: if the glob silently matched nothing, the empty result below
// would be meaningless.
assert.ok(results.length > 100, `expected the tests/ glob to match many files, got ${results.length}`);
assert.deepStrictEqual(violations, [], `duplicate folded suites found:\n${violations.join('\n')}`);
});
test('the rule is registered at error for tests/**/*.cjs in the real config', async () => {
const eslint = new ESLint({ cwd: REPO_ROOT });
const config = await eslint.calculateConfigForFile(
path.join(REPO_ROOT, 'tests', 'install.test.cjs'),
);
assert.deepStrictEqual(config.rules['local/no-duplicate-fold-marker'], [2]);
});
// ── Occurrence-count boundary: 1 (clean) / 2 (one report) / 3 (two) ────────
test('valid: a single folded marker in a file', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [{ code: src(FOLD_A_B1), filename: 'tests/host.test.cjs' }],
invalid: [],
});
});
test('invalid: the same folded marker twice in one file', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(FOLD_A_B1, FOLD_A_B1),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 2 },
],
},
],
});
});
test('invalid: three occurrences report the 2nd and 3rd', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(FOLD_A_B1, FOLD_A_B1, FOLD_A_B1),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 2 },
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 3 },
],
},
],
});
});
test('valid: two distinct folded markers', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [{ code: src(FOLD_A_B1, FOLD_B_B1), filename: 'tests/host.test.cjs' }],
invalid: [],
});
});
// ── Negative space (10-diagnosis.md) ──────────────────────────────────────
// #3271's own reproduction regex (`folded:[a-z0-9-]*`) stops at `.` and
// collides these two genuinely distinct suites, which coexist in
// tests/model-resolver.test.cjs. A guard written to that key would red the
// build on `next` forever.
test('valid: dot-suffixed marker is distinct from its prefix (model-resolver #3271 false positive)', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'__foldDescribe("folded:feat-443-effort-fast-mode.integration (consolidation epic #1969 B8 #1977)", () => {});',
'__foldDescribe("folded:feat-443-effort-fast-mode (consolidation epic #1969 B8 #1977)", () => {});',
),
filename: 'tests/model-resolver.test.cjs',
},
],
invalid: [],
});
});
// tests/review-default-reviewers-workflow.test.cjs reuses the fold alias for
// an ordinary describe block. Those carry no uniqueness obligation.
test('valid: __foldDescribe titles without a folded: prefix are ignored', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
"__foldDescribe('#1936: OpenCode reviewer empty-output hardening', () => {});",
"__foldDescribe('#1936: OpenCode reviewer empty-output hardening', () => {});",
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
test('valid: a file with no fold markers', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [{ code: src('describe("ordinary", () => {});'), filename: 'tests/host.test.cjs' }],
invalid: [],
});
});
test('valid: plain describe with a folded: title is not the fold convention', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'describe("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
'describe("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
// Documented non-goal, pinned so the behavior is deliberate rather than
// accidental: the rule keys on the callee identifier being literally
// __foldDescribe. Every one of the 365 fold sites calls it directly.
test('valid: a call through a further alias of the fold alias is not tracked', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'const d = __foldDescribe;',
'd("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
'd("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
test('valid: a member-expression call named __foldDescribe is not the fold alias', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'helpers.__foldDescribe("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
'helpers.__foldDescribe("folded:a (consolidation epic #1969 B1 #1970)", () => {});',
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
// The same marker in two different HOST files is not intra-file duplication.
// RuleTester lints each entry as its own file, so this also proves the
// per-file state is rebuilt rather than shared across files.
test('valid: the same marker in two different files is not an intra-file duplicate', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{ code: src(FOLD_A_B1), filename: 'tests/host-one.test.cjs' },
{ code: src(FOLD_A_B1), filename: 'tests/host-two.test.cjs' },
],
invalid: [],
});
});
// ── Ordering / identity ───────────────────────────────────────────────────
test('invalid: interleaved duplicates each report against their own first occurrence', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(FOLD_A_B1, FOLD_B_B1, FOLD_A_B1, FOLD_B_B1),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 3 },
{ messageId: 'duplicateFoldMarker', data: { marker: 'b', firstLine: '2' }, line: 4 },
],
},
],
});
});
// The batch label is provenance, not identity — a re-fold under a different
// batch must not evade the guard. This is the exact shape of #3271: #1975
// re-applied #1970's blocks.
test('invalid: a duplicate marker is reported even when the batch label differs', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(FOLD_A_B1, FOLD_A_B5),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 2 },
],
},
],
});
});
// ── Title shapes that cannot be resolved statically ───────────────────────
test('invalid: substitution-free template-literal fold titles are resolved', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(
'__foldDescribe(`folded:a (consolidation epic #1969 B1 #1970)`, () => {});',
'__foldDescribe(`folded:a (consolidation epic #1969 B1 #1970)`, () => {});',
),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker: 'a', firstLine: '1' }, line: 2 },
],
},
],
});
});
test('valid: non-literal fold titles are skipped without throwing', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'const name = "folded:a";',
'const x = "a";',
'__foldDescribe(name, () => {});',
'__foldDescribe(name, () => {});',
'__foldDescribe(`folded:${x} (epic)`, () => {});',
'__foldDescribe(`folded:${x} (epic)`, () => {});',
'__foldDescribe(42, () => {});',
'__foldDescribe(42, () => {});',
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
test('valid: __foldDescribe with no arguments does not throw', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{ code: src('__foldDescribe();', '__foldDescribe();'), filename: 'tests/host.test.cjs' },
],
invalid: [],
});
});
test('valid: an empty marker after the folded: prefix is not tracked', () => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [
{
code: src(
'__foldDescribe("folded: (consolidation epic #1969 B1 #1970)", () => {});',
'__foldDescribe("folded: (consolidation epic #1969 B1 #1970)", () => {});',
),
filename: 'tests/host.test.cjs',
},
],
invalid: [],
});
});
// Property: marker identity is the whole whitespace-delimited token.
//
// This is the generative form of the #3271 correctness question. An
// implementation that keyed on the issue's `[a-z0-9-]*` slice would truncate
// at `.` and pass arm 1 while failing arm 2 on any pair like
// (`a.integration`, `a`) — which is exactly the tests/model-resolver.test.cjs
// false positive. The alphabet deliberately includes `.` and `_` so those
// pairs are generated, not hoped for.
//
// `fc` is already imported at the top of this file and used by the
// no-adhoc-markdown-parsing suite; this follows the same
// fc.property-driving-ruleTester shape.
test('property: a marker is identified by its whole token, so distinct markers never collide', () => {
const markerArb = fc
.array(fc.constantFrom('a', 'z', 'q', '0', '9', '-', '.', '_'), { minLength: 1, maxLength: 12 })
.map((chars) => chars.join(''));
const fold = (marker) =>
`__foldDescribe("folded:${marker} (consolidation epic #1969 B1 #1970)", () => {});`;
// Arm 1: the SAME marker twice is always reported exactly once, against
// the first occurrence.
fc.assert(
fc.property(markerArb, (marker) => {
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [],
invalid: [
{
code: src(fold(marker), fold(marker)),
filename: 'tests/host.test.cjs',
errors: [
{ messageId: 'duplicateFoldMarker', data: { marker, firstLine: '1' }, line: 2 },
],
},
],
});
}),
{ numRuns: 150, seed: 3271 },
);
// Arm 2: two DISTINCT markers never collide, however they differ.
fc.assert(
fc.property(markerArb, markerArb, (a, b) => {
fc.pre(a !== b);
ruleTester.run('no-duplicate-fold-marker', noDuplicateFoldMarker, {
valid: [{ code: src(fold(a), fold(b)), filename: 'tests/host.test.cjs' }],
invalid: [],
});
}),
{ numRuns: 150, seed: 3271 },
);
});
});
// ─── require-subprocess-timeout ────────────────────────────────────
describe('require-subprocess-timeout rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof requireSubprocessTimeout.create, 'function');
});
// ── INVALID cases (must error) ────────────────────────────────────────────
test('invalid: execFileSync("git", args, { cwd }) — object-literal options with no timeout key', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [],
invalid: [
{
code: `
const { execFileSync } = require('node:child_process');
const args = ['status'];
const cwd = '/repo';
execFileSync('git', args, { cwd });
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'requireSubprocessTimeout' }],
},
],
});
});
test('invalid: execSync("npm ci", { encoding: "utf8" }) — object-literal options with no timeout key', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [],
invalid: [
{
code: `
const { execSync } = require('node:child_process');
execSync('npm ci', { encoding: 'utf8' });
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'requireSubprocessTimeout' }],
},
],
});
});
test('invalid: spawnSync with a dotted childProcess.spawnSync callee and no timeout', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [],
invalid: [
{
code: `
const childProcess = require('node:child_process');
childProcess.spawnSync('git', ['log'], { cwd: '/repo', encoding: 'utf-8' });
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'requireSubprocessTimeout' }],
},
],
});
});
test('invalid: execFileSync with NO options argument at all — categorically no timeout', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [],
invalid: [
{
code: `
const { execFileSync } = require('node:child_process');
execFileSync('git', ['status']);
`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'requireSubprocessTimeout' }],
},
],
});
});
// ── VALID cases (must NOT error) ──────────────────────────────────────────
test('valid: execFileSync("git", args, { cwd, timeout: 30000 }) — timeout key present', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [
{
code: `
const { execFileSync } = require('node:child_process');
const args = ['status'];
const cwd = '/repo';
execFileSync('git', args, { cwd, timeout: 30000 });
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: options as a pre-built identifier — execFileSync("git", args, opts) is not traced', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [
{
code: `
const { execFileSync } = require('node:child_process');
const args = ['status'];
const opts = { cwd: '/repo', timeout: 30000 };
execFileSync('git', args, opts);
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: same unbounded call under a tests/** filename — rule is inert outside src/*.cts', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [
{
code: `
const { execFileSync } = require('node:child_process');
execFileSync('git', ['status'], { cwd: '/repo' });
`,
filename: 'tests/foo.test.cjs',
},
],
invalid: [],
});
});
test('valid: allow-unbounded-subprocess suppression comment on the call line', () => {
ruleTester.run('require-subprocess-timeout', requireSubprocessTimeout, {
valid: [
{
code: `
const { execFileSync } = require('node:child_process');
execFileSync('git', ['status'], { cwd: '/repo' }); // allow-unbounded-subprocess: bounded by caller's own watchdog
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
});
// ─── require-registered-exit (#3910, epic #3889 Phase 6) ──────────────────
//
// See .gsd/phase/enhance-3910-ban-raw-terminator/50-test-matrix.md for the
// enumerated input-class matrix these tests implement.
describe('require-registered-exit rule', () => {
test('rule module exports a create function', () => {
assert.strictEqual(typeof requireRegisteredExit.create, 'function');
});
// ── Positive control: one per registered glob (matrix rows 1-4) ──────────
test('invalid: process.exit() at top level — src/**/*.cts glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `process.exit(0);`,
filename: 'src/some-module.cts',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
test('invalid: process.exit() at top level — scripts/**/*.cjs glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `process.exit(1);`,
filename: 'scripts/some-script.cjs',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
test('invalid: process.exit() at top level — hooks/**/*.js glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `process.exit(2);`,
filename: 'hooks/some-hook.js',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
test('invalid: process.exit() at top level — gsd-core/bin/**/*.cjs glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `process.exit(1);`,
filename: 'gsd-core/bin/gsd-tools.cjs',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
// ── Negative control: process.exitCode must NEVER be flagged (matrix rows 5-8) ──
//
// Required negative control: conflating process.exitCode (the CORRECT
// drain-then-exit pattern) with process.exit() is what inflated this
// epic's original raw-exit census 2x.
test('valid: process.exitCode = 1 is not flagged — src/**/*.cts glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{ code: `process.exitCode = 1;`, filename: 'src/some-module.cts' },
],
invalid: [],
});
});
test('valid: process.exitCode = 1 is not flagged — scripts/**/*.cjs glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{ code: `process.exitCode = 1;`, filename: 'scripts/some-script.cjs' },
],
invalid: [],
});
});
test('valid: process.exitCode = 1 is not flagged — hooks/**/*.js glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{ code: `process.exitCode = 1;`, filename: 'hooks/some-hook.js' },
],
invalid: [],
});
});
test('valid: process.exitCode = 1 is not flagged — gsd-core/bin/**/*.cjs glob', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{ code: `process.exitCode = 1;`, filename: 'gsd-core/bin/gsd-tools.cjs' },
],
invalid: [],
});
});
// ── Allowlist boundary: the ONE sanctioned terminator (matrix rows 9-11) ──
test('valid: process.exit() lexically inside a function named terminateNow is allowlisted', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{
code: `
function terminateNow(outcome, payload) {
try {
process.exit(0);
} catch (err) {
process.exit(1);
}
}
`,
filename: 'src/cli-exit.cts',
},
],
invalid: [],
});
});
test('invalid: near-miss — same shape, function named something else IS flagged', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `
function notTerminateNow(outcome, payload) {
process.exit(0);
}
`,
filename: 'src/cli-exit.cts',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
test('invalid: a top-level process.exit() is flagged even when an unrelated terminateNow exists elsewhere in the same file (allowlist is structural nesting, not file-wide)', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `
function terminateNow() {
// unrelated to the top-level exit below
}
process.exit(0);
`,
filename: 'src/cli-exit.cts',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
// ── Independence (matrix rows 12-13) ──────────────────────────────────────
test('valid: a bare (non-process) exit(...) call is not flagged', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{
code: `
function exit(code) { return code; }
exit(0);
`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('valid: computed member access process["exit"](0) is not flagged (documented boundary, name-based matching only)', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{ code: `process['exit'](0);`, filename: 'src/some-module.cts' },
],
invalid: [],
});
});
// ── Finding 5: KNOWN LIMITS, pinned — the rule does NOT catch these evasions
// today. These tests do not endorse the patterns; they pin the CURRENT
// behavior so that a future change which starts catching one of them is a
// visible, deliberate diff (an intentionally-failing pinning test) rather
// than a silent behavior change discovered later. See the rule's header
// doc comment for the same limits documented for a human reader.
test('KNOWN LIMIT (pinned, not endorsed): aliasing process.exit to a local binding evades detection', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{
code: `const e = process.exit; e(1);`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('KNOWN LIMIT (pinned, not endorsed): process.exit.call(...) evades detection', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{
code: `process.exit.call(null, 1);`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
test('KNOWN LIMIT (pinned, not endorsed): process.exit.apply(...) evades detection', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [
{
code: `process.exit.apply(null, [1]);`,
filename: 'src/some-module.cts',
},
],
invalid: [],
});
});
// ── Allowlist is basename-AND-name gated, not name-only (finding: any file
// named terminateNow would otherwise inherit the allowlist for free) ───────
test('invalid: a function named terminateNow in a file that is NOT cli-exit.cts is still flagged', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `
function terminateNow(outcome, payload) {
process.exit(0);
}
`,
filename: 'src/some-other-module.cts',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
test('invalid: a function named terminateNow in gsd-core/bin/gsd-tools.cjs (not cli-exit.cts) is still flagged', () => {
ruleTester.run('require-registered-exit', requireRegisteredExit, {
valid: [],
invalid: [
{
code: `
function terminateNow(outcome, payload) {
process.exit(0);
}
`,
filename: 'gsd-core/bin/gsd-tools.cjs',
errors: [{ messageId: 'rawProcessExit' }],
},
],
});
});
// ── Finding 4: registration proof, not just filename-agnostic rule logic ──
//
// The RuleTester cases above vary `filename` directly, which RuleTester
// never resolves against eslint.config.mjs — they prove the rule's AST
// logic, not that it is actually WIRED to the four globs. This proves
// wiring: it resolves the real config for one representative path per
// glob and asserts the rule is enabled there. This test fails if a glob
// registration is ever removed from eslint.config.mjs (verified live:
// temporarily deleting the gsd-core/bin/**/*.cjs registration flipped
// this test red before it was restored).
test('the rule is registered at error for one representative path per glob in the real config', async () => {
const REPO_ROOT = path.join(__dirname, '..');
const eslint = new ESLint({ cwd: REPO_ROOT });
const representativePaths = [
path.join(REPO_ROOT, 'src', 'cli-exit.cts'),
path.join(REPO_ROOT, 'scripts', 'affected-tests-lib.cjs'),
path.join(REPO_ROOT, 'hooks', 'gsd-check-update.js'),
path.join(REPO_ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs'),
];
for (const p of representativePaths) {
// Sequential config resolution (not a hot loop) — no-await-in-loop is
// not registered on this glob, so no disable directive is needed here.
const config = await eslint.calculateConfigForFile(p);
assert.deepStrictEqual(
config.rules['local/require-registered-exit'],
[2],
`expected local/require-registered-exit to be registered at error for ${path.relative(REPO_ROOT, p)}`,
);
}
});
});