fix(review): single eslint home, threshold co-location, helper reuse, docs clarity

Review-pass fixes: lint:ci composes npm run lint (one eslint invocation
home); the scripts/ coverage floor moves to package.json
(test:coverage:scripts-floor) so both thresholds live together; the ratchet
test uses helpers.createTempDir; TESTING-SUITES.md clarifies what the
Windows scoped lane runs and why feat-*/enh-* files are exempt from the
bug-* ratchet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colin
2026-06-09 23:40:50 -04:00
parent a869df2acf
commit 9db7958a70
4 changed files with 28 additions and 15 deletions

View File

@@ -104,9 +104,10 @@ jobs:
- name: Build runtime lib (required by lint scripts)
run: npm run build:lib
# Single orchestrated lint entry point (npm run lint:ci) so local and CI
# always run the identical set. ESLint's --cache flag is intentionally
# NOT used here: CI never restores node_modules/.cache, so the flag was
# a no-op that only implied caching existed.
# always run the identical set. It composes `npm run lint`, keeping one
# eslint invocation home; the --cache flag inside it is a no-op in CI
# (node_modules/.cache is never restored) but still speeds local runs.
# Each sub-lint prints its own banner, so a failure identifies itself.
- name: Lint — all (ESLint, skill deps, test-file count, command contract, PR checks, legacy name, regression-test names)
run: npm run lint:ci
@@ -203,10 +204,11 @@ jobs:
# the SAME V8 coverage data left in coverage/tmp by the run above — no
# extra suite execution. Audit 2026-06: scripts/ measured 65.95%; the
# 55% floor prevents a collapse to zero-coverage tooling while leaving
# headroom for variance. Raise deliberately, never lower.
# headroom for variance. The threshold lives in package.json next to
# the 70% lib gate — raise deliberately, never lower.
- name: Coverage floor — scripts/ tooling (≥55%)
if: matrix.scope == 'full'
run: npx c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all
run: npm run test:coverage:scripts-floor
- name: Upload coverage artifact
if: always() && matrix.scope == 'full'

View File

@@ -46,6 +46,12 @@ identity ratchet (`npm run lint:regression-names`, part of `npm run lint:ci`):
- **Deleting/consolidating** a grandfathered file requires pruning its
allowlist entry, so the baseline only ever shrinks.
The ratchet deliberately covers only `bug-*`. Files named `feat-NNNN-*` /
`enh-NNNN-*` are *feature* test files — one (or one per suite) per feature is
the sanctioned layout (see the #443 strategy below), not a one-off regression
pattern. If `issue-*`/`perf-*` one-offs start accumulating the same way
`bug-*` did, extend the ratchet's regex and regenerate the allowlist.
## Running suites locally
```bash
@@ -96,11 +102,14 @@ The `Tests` workflow runs every PR through a scoped gate generated by
tests.
The default PR gate runs the broad `unit` (under the c8 coverage gate),
`integration`, and `security` suites once on Ubuntu / Node 24, scoped smoke on
Ubuntu / Node 22, and scoped Windows-sensitive tests on Windows / Node 24.
**Every changed test file always joins the Windows scoped lane** (the #494
invariant, narrowed): a modified test is exercised on the divergent OS before
merge at per-file cost, without paying for the three full parity lanes.
`integration`, and `security` suites once on Ubuntu / Node 24, scoped tests on
Ubuntu / Node 22, and scoped tests on Windows / Node 24. "Scoped" means the
diff-selected list from the rule table — not the full suite and not a fixed
smoke set (the fixed smoke list is only the empty-selection fallback). The
Windows lane's list is the Windows-sensitive subset of the selection, plus
**every changed test file, unconditionally** (the #494 invariant, narrowed): a
modified test is exercised on the divergent OS before merge at per-file cost,
without paying for the three full parity lanes.
PRs touching workflow, package, test-runner, install, release, or
Windows-sensitive surfaces also run the full parity matrix on macOS and the

View File

@@ -91,7 +91,7 @@
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
"lint:fix": "eslint . --fix",
"lint:ci": "eslint . && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs",
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs",
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
"lint:descriptions": "node scripts/lint-descriptions.cjs",
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
@@ -111,6 +111,7 @@
"test:slow": "node scripts/run-tests.cjs --suite slow",
"test:affected": "node scripts/run-affected-tests.cjs",
"test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
"test:coverage:unit": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit",
"test:coverage:all": "npm run test:coverage",
"test:mutation": "stryker run",

View File

@@ -9,17 +9,18 @@ const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('child_process');
const fs = require('fs');
const os = require('node:os');
const path = require('path');
const { cleanup } = require('./helpers.cjs');
const { createTempDir, cleanup } = require('./helpers.cjs');
const ROOT = path.join(__dirname, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'lint-regression-test-names.cjs');
let sandbox;
let fixtureCount = 0;
function runLint({ files, allowlist }) {
const testsDir = path.join(sandbox, `tests-${Math.random().toString(36).slice(2)}`);
const testsDir = path.join(sandbox, `tests-${fixtureCount++}`);
fs.mkdirSync(testsDir, { recursive: true });
for (const f of files) fs.writeFileSync(path.join(testsDir, f), '');
const allowlistPath = path.join(testsDir, 'allowlist.json');
@@ -37,7 +38,7 @@ function runLint({ files, allowlist }) {
describe('lint-regression-test-names', () => {
before(() => {
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-lint-regression-'));
sandbox = createTempDir('gsd-lint-regression-');
});
after(() => {