diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e217bd008..146bbf679 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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' diff --git a/docs/TESTING-SUITES.md b/docs/TESTING-SUITES.md index b5f31e053..6454bf961 100644 --- a/docs/TESTING-SUITES.md +++ b/docs/TESTING-SUITES.md @@ -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 diff --git a/package.json b/package.json index 57f6ce2ad..6838f89ae 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/lint-regression-test-names.test.cjs b/tests/lint-regression-test-names.test.cjs index 8f30cd816..430d57d4f 100644 --- a/tests/lint-regression-test-names.test.cjs +++ b/tests/lint-regression-test-names.test.cjs @@ -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(() => {