chore(#4591): add platform-conformance-tier classifier + gate CI on it (#4598)

This commit is contained in:
Tom Boucher
2026-09-10 08:36:13 -04:00
committed by GitHub
parent 2cefa5a5ac
commit bcd99696d3
31 changed files with 1448 additions and 30 deletions

View File

@@ -782,6 +782,133 @@ jobs:
CI_JOB_TIMEOUT_MINUTES: '45'
run: node scripts/ci-check-job-near-cap.cjs
# #4591 (epic #4589 Phase 2): runs ONLY the platform-conformance-tier file
# list (scripts/lib/platform-conformance-tier.generated.cjs) on real
# Windows/macOS — the OS-agnostic bulk of the suite already ran once on
# ubuntu-latest in the `test` job above. Gated the same way `test-full` is
# (product code changed AND full_matrix). Windows is sharded 3 ways
# (matching test-full's own precedent) after the first real CI run measured
# it: unsharded, windows-latest hit its 45-minute timeout and was CANCELLED
# (started 03:41:19Z, cancelled 04:26:25Z, run 34434252144) while
# macos-latest finished the identical file set in 26m58s — this conformance
# tier is dominated by subprocess-spawning tests (343/565 files), and
# Windows process-spawn overhead is the documented reason test-full's own
# windows lane needed the same fix (#3057). macos-latest stays unsharded; it
# has real headroom (27m against the 45m cap).
# `test-full` (above) keeps running the WHOLE suite on the
# same OSes as a non-gating safety net for one release cycle (see
# `required-tests` below) — this job is the new GATING signal for
# windows/macos coverage.
test-conformance:
name: conformance test (${{ matrix.os }}, ${{ matrix.node-version }}${{ matrix.shard && format(', shard {0}', matrix.shard) || '' }})
needs: [changes, preflight]
if: needs.changes.outputs.code_changed == 'true' && needs.changes.outputs.full_matrix == 'true'
runs-on: ${{ matrix.os }}
defaults:
run:
shell: ${{ matrix.shell }}
# No LANE_COSTS entry exists yet for this brand-new job — see
# tests/ci-test-job-timeout-budget.test.cjs's own header: "no unit test
# can prove a lane fits its budget — only a real CI run measures that."
# Generous and unchanged from test-full's own budget until a real
# measurement exists; the smaller file count should comfortably undercut
# this ceiling.
timeout-minutes: 45
env:
GSD_PLUGIN_ROOT: .ci-gsd-plugin-root-disabled
GSD_STRICT_LIVE_CONFIG_GUARD: ${{ matrix.os != 'windows-latest' && '1' || '' }}
GSD_EMITTED_BASE: ${{ github.event.pull_request.base.sha }}
AUDIT_BASELINE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || (github.event_name == 'push' && github.event.before) || (github.event_name == 'merge_group' && github.event.merge_group.base_sha) || '' }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
node-version: 24
shell: pwsh
shard: 1/3
- os: windows-latest
node-version: 24
shell: pwsh
shard: 2/3
- os: windows-latest
node-version: 24
shell: pwsh
shard: 3/3
- os: macos-latest
node-version: 24
shell: 'zsh {0}'
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 (Windows)
if: runner.os == 'Windows'
with:
fetch-depth: 0
persist-credentials: true
token: ${{ github.token }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (Linux/macOS)
if: runner.os != 'Windows'
with:
fetch-depth: 0
persist-credentials: true
token: ${{ github.token }}
- name: Record job start time (near-cap check)
run: node -e 'require("fs").appendFileSync(process.env.GITHUB_ENV, "CI_JOB_START_EPOCH_MS=" + Date.now() + "\n")'
- name: Guard — require GitHub-hosted runner
run: node scripts/ci-guard-runner.cjs
- name: Rebase check — merge PR base branch into PR head
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ github.token }}
CI_REBASE_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node scripts/ci-rebase-check.cjs
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Environment check
run: npm run check:env
- name: Install dependencies
run: npm ci
- name: Dependency integrity gate
run: node scripts/check-npm-integrity.cjs
- name: Restore emitted-baseline cache
if: github.event_name == 'pull_request'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .gsd-cache/emitted-baseline.json
key: emitted-baseline-${{ github.event.pull_request.base.sha }}
# #4591: the generated conformance-tier list is a .cjs module (so
# scripts/gen-platform-conformance-tier.cjs's own tests and other
# scripts can `require()` it directly) — run-tests.cjs --files-from
# expects a plain newline-delimited text file, so this step bridges
# the two, one path per line, matching the existing scoped-lane
# convention (.ci-selected-tests.txt).
- name: Prepare conformance-tier test list
run: node -e "require('./scripts/lib/platform-conformance-tier.generated.cjs').CONFORMANCE_TIER_FILES.forEach(f => console.log(f))" > .ci-conformance-tests.txt
- name: Run platform-conformance-tier tests
run: node scripts/run-tests.cjs --files-from .ci-conformance-tests.txt${{ matrix.shard && format(' --shard {0}', matrix.shard) || '' }}
- name: Check job budget (near-cap advisory)
if: always()
continue-on-error: true
env:
CI_JOB_LABEL: "conformance test (${{ matrix.os }}, ${{ matrix.node-version }})"
CI_JOB_TIMEOUT_MINUTES: '45'
run: node scripts/ci-check-job-near-cap.cjs
# #2952: the coverage gate that the `test` lane used to run inline. Sharding
# the unit suite means no single runner sees the whole picture, so the gate
# moves here, downloads every shard's raw V8 dumps into one coverage/tmp, and
@@ -929,6 +1056,7 @@ jobs:
- test
- test-inert
- test-full
- test-conformance
- coverage-gate
- qa-loop-walk
if: always()
@@ -944,6 +1072,7 @@ jobs:
CHANGES_RESULT: ${{ needs.changes.result }}
LINT_RESULT: ${{ needs.lint-tests.result }}
TEST_RESULT: ${{ needs.test.result }}
TEST_CONFORMANCE_RESULT: ${{ needs.test-conformance.result }}
INERT_RESULT: ${{ needs.test-inert.result }}
FULL_TEST_RESULT: ${{ needs.test-full.result }}
COVERAGE_GATE_RESULT: ${{ needs.coverage-gate.result }}
@@ -957,6 +1086,7 @@ jobs:
echo "changes=$CHANGES_RESULT"
echo "lint-tests=$LINT_RESULT"
echo "test=$TEST_RESULT"
echo "test-conformance=$TEST_CONFORMANCE_RESULT"
echo "test-inert=$INERT_RESULT"
echo "test-full=$FULL_TEST_RESULT"
echo "coverage-gate=$COVERAGE_GATE_RESULT"
@@ -1002,13 +1132,27 @@ jobs:
echo "::error::test matrix did not pass"
exit 1
fi
# #4591 (epic #4589 Phase 2): test-conformance is the new gating
# signal for real Windows/macOS coverage — the conformance-tier
# file list, not the whole suite.
if [ "$TEST_CONFORMANCE_RESULT" != "success" ] && [ "$TEST_CONFORMANCE_RESULT" != "skipped" ]; then
echo "::error::platform-conformance-tier matrix did not pass"
exit 1
fi
# #2952: the coverage gates no longer run inside the `test` matrix —
# sharding moved them to the `coverage-gate` job, which merges every
# shard's dumps. TEST_RESULT therefore does NOT cover them any more;
# COVERAGE_GATE_RESULT below is what gates coverage.
# #4591 (epic #4589 Phase 2): downgraded from a hard gate to a
# non-blocking safety net for one release cycle, per the epic's
# phased-rollout design — test-conformance (above) is now the real
# gate for windows/macos coverage. A red full-suite run here is
# still surfaced (it means the conformance-tier classifier missed
# something) but does not block merge; retire this job entirely
# in a follow-up once the classifier has proven itself over one
# release cycle with no such miss.
if [ "$FULL_TEST_RESULT" != "success" ] && [ "$FULL_TEST_RESULT" != "skipped" ]; then
echo "::error::full parity matrix did not pass"
exit 1
echo "::warning::full legacy parity matrix did not pass (non-gating safety net — see #4591)"
fi
# #2952: the coverage gate is skipped when product code did not change

View File

@@ -436,7 +436,7 @@ const GSD_CHANGESET_FILES = [
'github-release-notes.cjs', 'lint.cjs', 'new.cjs',
'README.md', // documentation only — not user-authored
];
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs'];
const GSD_SCRIPTS_LIB_FILES = ['cli-exit.cjs', 'allowlist-ratchet.cjs', 'drift-scan.cjs', 'alias-drift-families.cjs', 'exit-code-registry.cjs', 'ndjson-reporter.cjs', 'ci-job-timing.cjs', 'shellcheck-fetch.cjs', 'npm-version-check-diagnosis.cjs', 'platform-conformance-tier.generated.cjs', 'suite-detection.cjs'];
/**
* Resolve a runtime's shared-hooks directory name from its descriptor.

View File

@@ -404,7 +404,7 @@ Two properties are load-bearing and easy to break:
failure message only.
Gated: `test.yml` (`lint-tests`, `test`, `test-inert`, `test-full`,
`coverage-gate`, `qa-loop-walk`, `required-tests`), `install-smoke.yml`,
`test-conformance`, `coverage-gate`, `qa-loop-walk`, `required-tests`), `install-smoke.yml`,
`mutation.yml`, `security-scan.yml`, `docs-required.yml`,
`changeset-required.yml`, `default-flip-documentation.yml`, `branch-naming.yml`.
@@ -547,6 +547,17 @@ cap stays at least the **headroom factor** (1.5x) above a documented,
hand-measured cost for that job — now all four of the jobs above, not just
`test`/`test-full`/`coverage-gate`/`test-inert` as before.
`test-conformance` in `test.yml` (#4591, epic #4589 Phase 2) runs the
`scripts/lib/platform-conformance-tier.generated.cjs` file list on
`windows-latest` (sharded three ways) and `macos-latest` (unsharded) — the
gating signal for real-OS coverage, replacing `test-full`'s old gating role
for one release cycle while `test-full` runs as a non-gating safety net. It
also declares a `timeout-minutes` cap and runs the same in-job near-cap check
described below, but it has no `LANE_COSTS` entry in
`tests/ci-test-job-timeout-budget.test.cjs` yet — no real, completed
(non-cancelled) per-shard measurement exists — so the headroom-factor gate
does not cover it until one lands.
Two runtime mechanisms sit on top of that static gate, both new in #4036:
- **In-job near-cap check** (`scripts/ci-check-job-near-cap.cjs`) — the last
@@ -561,7 +572,7 @@ Two runtime mechanisms sit on top of that static gate, both new in #4036:
`scripts/ci-timeout-report.cjs`) — runs daily and on `workflow_dispatch`. It
polls GitHub's Actions REST API for recently completed jobs across
`test.yml`, `mutation.yml`, and `install-smoke.yml`, resolves each job's
declared cap (a literal `timeout-minutes` for `test`/`test-full`/`smoke`, or
declared cap (a literal `timeout-minutes` for `test`/`test-full`/`test-conformance`/`smoke`, or
`scripts/mutation-matrix.cjs`'s `COVERED[<module>].timeoutMinutes` for
`mutate`'s per-module shards), and appends any new `(runId, jobName)`
records to `tests/ci-timeout-budget-history.jsonl`. Unlike the in-job check,

View File

@@ -1,9 +1,12 @@
# How to read CI timeout budget signals
Every matrixed CI job (`test`, `test-full` in `.github/workflows/test.yml`; `mutate` in
`mutation.yml`; `smoke` in `install-smoke.yml`) now reports how close it ran to its
Every matrixed CI job (`test`, `test-full`, `test-conformance` in `.github/workflows/test.yml`;
`mutate` in `mutation.yml`; `smoke` in `install-smoke.yml`) now reports how close it ran to its
`timeout-minutes` cap. This page is for a maintainer trying to answer: *is a lane drifting
toward its cap, and where do I look?*
toward its cap, and where do I look?* (`test-conformance` runs the platform-conformance-tier file
list — `scripts/lib/platform-conformance-tier.generated.cjs` — on `windows-latest`, sharded three
ways, and `macos-latest`, unsharded; it is the gating signal for real-OS coverage, replacing
`test-full`'s old gating role for one release cycle.)
## 1. A single run crossed 90% of its budget

View File

@@ -110,7 +110,7 @@
"gen:registry": "node scripts/gen-registry.cjs --write",
"gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
"gen:section-manifest": "node scripts/gen-section-manifest.cjs --write",
"regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write",
"regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write && node scripts/gen-platform-conformance-tier.cjs --write",
"validate:registry": "node scripts/validate-registry.cjs",
"prepack": "npm run build:lib",
"prepare": "npm run build:lib",
@@ -132,7 +132,7 @@
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
"lint:changeset": "node scripts/changeset/lint.cjs",
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check",
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check",
"lint:docs": "node scripts/lint-docs-required.cjs",
"lint:qa-smells": "node scripts/qa-smell-ratchet.cjs",
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",

View File

@@ -35,6 +35,7 @@ const JOB_RULES = [
{ workflowFile: 'test.yml', jobKey: 'test', test: (name) => name.startsWith('test (') && name !== 'test (inert CI)' },
{ workflowFile: 'test.yml', jobKey: 'test-full', test: (name) => name.startsWith('full test (') },
{ workflowFile: 'test.yml', jobKey: 'coverage-gate', test: (name) => name === 'Coverage gate (merged shards)' },
{ workflowFile: 'test.yml', jobKey: 'test-conformance', test: (name) => name.startsWith('conformance test (') },
{ workflowFile: 'install-smoke.yml', jobKey: 'smoke', test: (name) => name.startsWith('smoke (') },
];

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env node
'use strict';
/**
* Generates scripts/lib/platform-conformance-tier.generated.cjs — the list of
* test files under tests/**\/*.test.cjs whose CONTENT signals they exercise
* platform-specific behavior (Windows/macOS path quirks, raw child_process
* usage, chmod mode bits, etc.) and therefore need REAL-OS coverage rather
* than a Linux-only conformance lane (#4591).
*
* Only `unit`-suite test files (no suite suffix, per `scripts/run-tests.cjs`'s
* `suiteOf()`) are considered for the conformance tier. `install`-,
* `security`-, `slow`-, `integration`-, and `qa`-suffixed files are excluded
* entirely — never merely deprioritized — for three independent reasons: (1)
* `install`/`slow` are explicitly PR-excluded suites
* (`scripts/affected-tests-lib.cjs`'s `PR_EXCLUDED_SUITES`; "PRs must never
* select or run these"), and this generator's output feeds a `pull_request`-
* triggered job; (2) `integration`/`security` already run via their own
* separate, dedicated, unsharded, shard-1-only steps in the `test`/
* `test-full` jobs (.github/workflows/test.yml) — folding any of them into
* this job's generic `--files-from` + `--shard` invocation is unproven and,
* per the incident below, unsafe. (3) `qa` (loop-walk-suite files) already
* runs via its own separate, dedicated `qa-loop-walk` job
* (.github/workflows/test.yml) — the same rationale as (2): a purpose-built
* home already exists, so folding it into this job's generic invocation
* duplicates coverage without benefit. Real incident that surfaced this: a live
* CI run's `conformance test (windows-latest, shard 2/3)` job was killed with
* 11 tests in flight — including `tests/release-tarball-smoke.install.test.cjs`
* — because this generator had (wrongly) placed an `install`-suite file into
* the Linux-conformance candidate pool with no suite filtering at all.
*
* `classifyContent(content)` is the pure, exported classifier: it favors
* simple, auditable substring/regex matching over AST parsing, mirroring
* eslint-rules/lib/portability-vocab.cjs's own design stance (over-inclusion
* is the safe direction — a false positive costs one extra test running on a
* real OS; a false negative silently drops real-OS coverage).
*
* KNOWN LIMIT, disclosed deliberately: this is a STATIC content classifier,
* not a real per-file, per-OS behavioral diff. Epic #4589's issue #4591 asked
* for the cutover to be validated by "running the existing full matrix one
* more time as a parity baseline, diffing pass/fail per file between the
* real-OS runs and the Linux run" before moving any file into the Linux-only
* bulk. That literal per-file diff was NOT performed — no historical
* per-file, per-OS pass/fail dataset exists to diff against (GitHub Actions
* publishes coverage/QA artifacts from CI runs, not per-file JUnit results).
* What stands in for it: (1) the most recent push-triggered run on `next`
* (unconditionally full-matrix) is green on every OS for every file in this
* classification, confirmed before this classifier was built; (2) the
* existing `test-full` job keeps running the WHOLE suite on real Windows/
* macOS as a non-gating safety net for one release cycle (.github/workflows/
* test.yml) — a classifier miss surfaces as a visible warning there, not a
* silent gap, before the safety net is retired. This is the same
* static-analysis-substitutes-for-real-OS-execution stance ADR-1703's whole
* rule catalog already takes; it is a real, disclosed limit, not a silent
* substitution.
*
* Usage:
* node scripts/gen-platform-conformance-tier.cjs # print summary to stdout
* node scripts/gen-platform-conformance-tier.cjs --write # write the generated file
* node scripts/gen-platform-conformance-tier.cjs --check # exit 1 if the committed file is stale
* node scripts/gen-platform-conformance-tier.cjs --tests-dir <path> # override the tests/ root (tests only)
* node scripts/gen-platform-conformance-tier.cjs --out <path> # override the generated-file path (tests only)
*
* `--tests-dir`/`--out` (or the TESTS_DIR/OUT_PATH env vars, flag takes
* precedence) exist solely so tests/platform-conformance-tier.test.cjs can
* point the CLI at a small temp fixture tree instead of this repo's real,
* 900+-file tests/ tree. Production usage (package.json's lint:generated-sync
* / regen:derived chains) passes no flags and gets the real repo paths.
*/
const fs = require('node:fs');
const path = require('node:path');
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
const { suiteOf } = require('./lib/suite-detection.cjs');
const ROOT = path.resolve(__dirname, '..');
const DEFAULT_TESTS_DIR = path.join(ROOT, 'tests');
const DEFAULT_OUT_PATH = path.join(ROOT, 'scripts', 'lib', 'platform-conformance-tier.generated.cjs');
const GENERATED_HEADER =
'// GENERATED FILE — do not hand-edit. Run `node scripts/gen-platform-conformance-tier.cjs --write` to regenerate.\n';
/**
* Detection categories (#4591 design doc). Each entry's `test` receives the
* raw file content string and returns true when that category's signal is
* present. Order matches the design doc's enumeration; `signals` in
* `classifyContent`'s return value preserves this order.
*/
const CATEGORIES = [
{
name: 'process-platform',
test: (content) => /process\.platform/.test(content),
},
{
name: 'os-platform',
test: (content) => /os\.platform\(\)/.test(content),
},
{
name: 'win32-darwin-literal',
test: (content) => /\bwin32\b/.test(content) || /\bdarwin\b/.test(content),
},
{
name: 'chmod-mode-bit',
test: (content) => /chmodSync|chmod\(/.test(content) || /0o[0-7]{3,4}\b/.test(content),
},
{
name: 'windows-shell-token',
test: (content) => /cmd\.exe|powershell|pwsh|ComSpec/i.test(content),
},
{
name: 'windows-env-var',
test: (content) => /\bPATHEXT\b|\bUSERPROFILE\b|\bHOMEDRIVE\b|\bHOMEPATH\b/.test(content),
},
{
name: 'process-seam-subprocess',
test: (content) => /\brunNode\(|\brunGit\(|\brunHook\(|\brunGsdTools\(|\bgitOrThrow\(/.test(content),
},
{
name: 'raw-child-process',
// Requires BOTH the child_process import/reference token AND one of the
// three call names in the same file — this is what keeps a same-named
// local identifier (e.g. a variable called `spawnResult`) from false-
// positiving: `spawnResult` never forms the substring `spawnSync(`.
test: (content) => {
if (/require\((['"])(?:node:)?child_process\1\)/.test(content)) return true;
if (!content.includes('child_process')) return false;
return /\bspawnSync\(|\bexecSync\(|\bexecFileSync\(/.test(content);
},
},
{
name: 'symlink-keyword',
// A leading `\b` with no trailing one, case-insensitive: this is
// deliberately NOT `/\bsymlink\b|\bSymlink\b/` (that literal pair would
// never match the dominant real-world call shape `symlinkSync(` /
// `readlinkSync(` — no word boundary exists between "symlink" and the
// immediately-following "Sync", both \w characters). The leading `\b`
// alone still excludes a mid-word embedding like "presymlink".
test: (content) => /\bsymlink/i.test(content),
},
{
name: 'hardcoded-path-vs-path-call',
// Intentionally coarse (design doc: over-inclusion is the safe
// direction): a path.* call ANYWHERE in the file plus a quoted
// forward-slash-leading string literal ANYWHERE in the file, with no
// attempt at proximity/scoping.
test: (content) =>
/path\.(join|resolve|dirname|basename|normalize|relative)\(/.test(content) &&
/['"`]\/[\w.\-/]*['"`]/.test(content),
},
];
/**
* Pure classifier: given a test file's raw string content, returns which
* platform-conformance categories matched and whether the file needs real-OS
* coverage (true iff at least one category matched).
*
* @param {string} content
* @returns {{needsRealOs: boolean, signals: string[]}}
*/
function classifyContent(content) {
const text = typeof content === 'string' ? content : '';
const signals = [];
for (const category of CATEGORIES) {
if (category.test(text)) signals.push(category.name);
}
return { needsRealOs: signals.length > 0, signals };
}
/**
* Recursively collect every `*.test.cjs` file beneath `dir`.
* @param {string} dir
* @returns {string[]} absolute paths
*/
function walkTestFiles(dir) {
const out = [];
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return out;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...walkTestFiles(full));
} else if (entry.isFile() && entry.name.endsWith('.test.cjs')) {
out.push(full);
}
}
return out;
}
/**
* Classify every test file under `testsDir`, returning `{ total, files }`
* where `files` is the SORTED array of `tests/<...>.test.cjs`-relative paths
* (POSIX-normalized, per RULESET.CONTENT-PATH-NORMALIZATION) whose content
* needs real-OS coverage.
*
* @param {string} testsDir
* @returns {{ total: number, files: string[] }}
*/
function classifyTree(testsDir) {
const absoluteFiles = walkTestFiles(testsDir);
// Only unit-suite files (no suite suffix) are eligible for the conformance
// tier — see the header doc-comment for why suite-tagged files are excluded
// entirely rather than merely deprioritized.
const unitFiles = absoluteFiles.filter((absPath) => suiteOf(absPath) === null);
const flagged = [];
for (const absPath of unitFiles) {
const content = fs.readFileSync(absPath, 'utf8');
const { needsRealOs } = classifyContent(content);
if (needsRealOs) {
const rel = path.relative(testsDir, absPath).replace(/\\/g, '/');
flagged.push('tests/' + rel);
}
}
flagged.sort();
return { total: absoluteFiles.length, files: flagged };
}
/**
* Render the generated `.cjs` module body — one array entry per line for a
* readable diff, matching scripts/lib/portability-vocab.cjs's array-literal
* style.
*
* @param {string[]} files - already sorted.
* @returns {string}
*/
function renderGeneratedFile(files) {
const lines = files.map((f) => ` ${JSON.stringify(f)},`).join('\n');
return (
GENERATED_HEADER +
"'use strict';\n\n" +
'module.exports = {\n' +
' CONFORMANCE_TIER_FILES: [\n' +
(lines.length > 0 ? lines + '\n' : '') +
' ],\n' +
'};\n'
);
}
/** Resolve the effective tests-dir / out-path from argv/env, flag beats env. */
function resolveOverrides(argv) {
let testsDir = process.env.TESTS_DIR || DEFAULT_TESTS_DIR;
let outPath = process.env.OUT_PATH || DEFAULT_OUT_PATH;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--tests-dir') {
const value = argv[i + 1];
if (!value || value.startsWith('--')) {
throw new ExitError(1, 'gen-platform-conformance-tier: --tests-dir requires a path value');
}
testsDir = path.resolve(value);
i++;
} else if (argv[i] === '--out') {
const value = argv[i + 1];
if (!value || value.startsWith('--')) {
throw new ExitError(1, 'gen-platform-conformance-tier: --out requires a path value');
}
outPath = path.resolve(value);
i++;
}
}
return { testsDir: path.resolve(testsDir), outPath: path.resolve(outPath) };
}
function main() {
const argv = process.argv.slice(2);
const { testsDir, outPath } = resolveOverrides(argv);
const mode = argv.includes('--check') ? 'check' : argv.includes('--write') ? 'write' : 'print';
const { total, files } = classifyTree(testsDir);
if (mode === 'write') {
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, renderGeneratedFile(files));
process.stdout.write(`Wrote ${outPath} (${files.length} conformance-tier file(s))\n`);
return;
}
if (mode === 'check') {
// Never trust a stale require cache — the committed file may have been
// rewritten (by --write, or by hand) since this process started.
let committed;
try {
const resolved = require.resolve(outPath);
delete require.cache[resolved];
committed = require(resolved);
} catch (err) {
throw new ExitError(
1,
`gen-platform-conformance-tier: could not load ${outPath} — run ` +
'`node scripts/gen-platform-conformance-tier.cjs --write` first ' +
`(${err && err.message ? err.message : err})`,
);
}
const committedFiles = Array.isArray(committed.CONFORMANCE_TIER_FILES)
? committed.CONFORMANCE_TIER_FILES
: [];
const committedSet = new Set(committedFiles);
const liveSet = new Set(files);
const added = files.filter((f) => !committedSet.has(f));
const removed = committedFiles.filter((f) => !liveSet.has(f));
if (added.length > 0 || removed.length > 0) {
process.stderr.write(
`${path.relative(ROOT, outPath).replace(/\\/g, '/')} is stale. Run:\n` +
' node scripts/gen-platform-conformance-tier.cjs --write\n\n',
);
for (const f of added) process.stderr.write(' + ' + f + '\n');
for (const f of removed) process.stderr.write(' - ' + f + '\n');
throw new ExitError(1);
}
process.stdout.write(
`ok gen-platform-conformance-tier: ${files.length} conformance-tier files, list matches\n`,
);
return;
}
// No flag: print a classification summary, write nothing.
process.stdout.write(
`gen-platform-conformance-tier: ${total} file(s) scanned, ` +
`${files.length} need real OS, ${total - files.length} excluded (Linux-only conformance tier eligible)\n`,
);
}
/* c8 ignore next 3 -- CLI entry guard; this repo measures coverage with c8, which does not honor istanbul pragmas */
if (require.main === module) {
runMain(main);
}
module.exports = { classifyContent, CATEGORIES, walkTestFiles, classifyTree, renderGeneratedFile };

View File

@@ -0,0 +1,553 @@
// GENERATED FILE — do not hand-edit. Run `node scripts/gen-platform-conformance-tier.cjs --write` to regenerate.
'use strict';
module.exports = {
CONFORMANCE_TIER_FILES: [
"tests/active-workstream-store.test.cjs",
"tests/active-workstream-store.unit.test.cjs",
"tests/adr-15-progress-converge.test.cjs",
"tests/adr-22-plan-drift-guard.test.cjs",
"tests/adr-612-bracket-coherence.test.cjs",
"tests/adr-612-bracket-phase-counting.test.cjs",
"tests/adr-612-bracket-read-tolerance.test.cjs",
"tests/adr-index-gate.test.cjs",
"tests/adr857-core-without-capabilities.test.cjs",
"tests/advance-plan-ambiguous-phase.test.cjs",
"tests/agent-frontmatter.test.cjs",
"tests/agent-hint-routing-1689.test.cjs",
"tests/agent-install-check.test.cjs",
"tests/agent-install-validation.test.cjs",
"tests/agent-skills.test.cjs",
"tests/ai-evals.test.cjs",
"tests/antigravity-upgrades.test.cjs",
"tests/api-coverage-gate-e2e.test.cjs",
"tests/api-coverage.test.cjs",
"tests/assumption-delta-checkpoint-e2e.test.cjs",
"tests/assumption-delta.test.cjs",
"tests/audit-command-cutover.test.cjs",
"tests/audit-open-remainder-count.test.cjs",
"tests/audit-uat-acknowledged.test.cjs",
"tests/audit-uat-summary-segmentation.test.cjs",
"tests/audit-workstream-layouts.test.cjs",
"tests/augment-upgrades.test.cjs",
"tests/autonomous-converge.test.cjs",
"tests/autonomous-interactive.test.cjs",
"tests/benchmark-compact-content.test.cjs",
"tests/branch-no-track-guard.test.cjs",
"tests/broken-windows-description.test.cjs",
"tests/broken-windows.test.cjs",
"tests/bugs-1656-1657.test.cjs",
"tests/canary-version-leak-lint.test.cjs",
"tests/capability-cli.test.cjs",
"tests/capability-command-dispatch.test.cjs",
"tests/capability-consent.test.cjs",
"tests/capability-ledger.test.cjs",
"tests/capability-lifecycle.test.cjs",
"tests/capability-loader.test.cjs",
"tests/capability-lock-mkdir-failure-3987.test.cjs",
"tests/capability-precedence-parity.test.cjs",
"tests/capability-probe-fallback.test.cjs",
"tests/capability-registry.test.cjs",
"tests/capability-source.test.cjs",
"tests/capability-state.test.cjs",
"tests/capability-trust.test.cjs",
"tests/capability-writer.test.cjs",
"tests/changeset-cli.test.cjs",
"tests/changeset-github-release-notes.test.cjs",
"tests/changeset-lint.test.cjs",
"tests/changeset-new.test.cjs",
"tests/check-contract-drift.test.cjs",
"tests/check-env.test.cjs",
"tests/check-gap-analysis-plan-post-e2e.test.cjs",
"tests/check-glossary-refs.test.cjs",
"tests/check-tdd-review-checkpoint-e2e.test.cjs",
"tests/check-ui-plan-gate.test.cjs",
"tests/check-ui-safety-gate.test.cjs",
"tests/check-update-config-dir.test.cjs",
"tests/chunked-planning-parallel.test.cjs",
"tests/ci-docs-guard-registry.test.cjs",
"tests/ci-next-health.test.cjs",
"tests/ci-pr-mergeability.test.cjs",
"tests/ci-rebase-check.test.cjs",
"tests/ci-test-scope.test.cjs",
"tests/cjs-command-router-adapter.test.cjs",
"tests/claude-md-path.test.cjs",
"tests/claude-md.test.cjs",
"tests/claude-orchestration-command-router.test.cjs",
"tests/claude-orchestration.test.cjs",
"tests/claude-skills-migration.test.cjs",
"tests/cli-exit.test.cjs",
"tests/cline-install.test.cjs",
"tests/clock-seam.test.cjs",
"tests/clock.test.cjs",
"tests/close-phase-todos-padded-resolves.test.cjs",
"tests/code-review-command.test.cjs",
"tests/code-review-depth.test.cjs",
"tests/code-review-fix-pipeline-regression.test.cjs",
"tests/code-review-pipeline-regression.test.cjs",
"tests/code-review-tier3-files-override-scoping.test.cjs",
"tests/code-review.test.cjs",
"tests/codebuddy-install.test.cjs",
"tests/codebuddy-upgrades.test.cjs",
"tests/codex-config-agents.test.cjs",
"tests/codex-config-hooks.test.cjs",
"tests/codex-config-install.test.cjs",
"tests/codex-config.test.cjs",
"tests/codex-declarative-reference.test.cjs",
"tests/codex-inherit-smoke.test.cjs",
"tests/command-contract.test.cjs",
"tests/command-routing-hub.test.cjs",
"tests/commands.test.cjs",
"tests/commit-docs-bypass.test.cjs",
"tests/commit-files-deletion.test.cjs",
"tests/commit-files-pathspec.test.cjs",
"tests/commonjs-marker.test.cjs",
"tests/compact-content-4139.test.cjs",
"tests/compact-content-partition-guard.test.cjs",
"tests/completion-predicate-drift-guard.test.cjs",
"tests/completion-ratio-scope-withholding.test.cjs",
"tests/completion-ratio-single-owner.test.cjs",
"tests/concurrency-safety.test.cjs",
"tests/config-defaults-runtime-exclusion.test.cjs",
"tests/config-field-docs.test.cjs",
"tests/config-get-default.test.cjs",
"tests/config-loader.test.cjs",
"tests/config-schema.property.test.cjs",
"tests/config.test.cjs",
"tests/configuration-migrate-config.test.cjs",
"tests/context-drift.test.cjs",
"tests/context-predicates-query.test.cjs",
"tests/copilot-install.test.cjs",
"tests/copilot-upgrades.test.cjs",
"tests/core-utils.test.cjs",
"tests/coverage-metadata-parser.test.cjs",
"tests/coverage-uat-routing.test.cjs",
"tests/cursor-hook-workspace-roots.test.cjs",
"tests/cursor-hooks.test.cjs",
"tests/cursor-reviewer.test.cjs",
"tests/cursor-subagent-isolation.test.cjs",
"tests/debug-session-manager-commit.test.cjs",
"tests/decisions.test.cjs",
"tests/declarative-reference-antigravity.test.cjs",
"tests/declarative-reference-augment.test.cjs",
"tests/declarative-reference-codebuddy.test.cjs",
"tests/declarative-reference-copilot.test.cjs",
"tests/declarative-reference-windsurf.test.cjs",
"tests/declarative-reference-zcode.test.cjs",
"tests/default-flip-documentation-lint.test.cjs",
"tests/dispatcher.test.cjs",
"tests/docs-hooks-table-parity.test.cjs",
"tests/docs-parity-live-registry.test.cjs",
"tests/docs-update.test.cjs",
"tests/drift-detection.test.cjs",
"tests/edge-probe-spec-phase-contract.test.cjs",
"tests/edge-probe.test.cjs",
"tests/edit-phase-milestone-scope-guard.test.cjs",
"tests/edit-phase.test.cjs",
"tests/effort-surface-axis.test.cjs",
"tests/effort-sync-installed-runtime.test.cjs",
"tests/emitted-ack-trailer.test.cjs",
"tests/emitted-attribution.test.cjs",
"tests/emitted-caps-gate.test.cjs",
"tests/emitted-provenance.test.cjs",
"tests/emitted-sizes.test.cjs",
"tests/ensure-runtime-build.test.cjs",
"tests/enumeration-single-owner.test.cjs",
"tests/eslint-glob-coverage.test.cjs",
"tests/eslint-rules.test.cjs",
"tests/estimate-calibrate.test.cjs",
"tests/estimate-loop-convergence.test.cjs",
"tests/execute-mvp-tdd-gate.test.cjs",
"tests/execute-phase-wave.test.cjs",
"tests/execute-phase-worktree-guard.test.cjs",
"tests/execute-plan-update-codebase-map-diff-base.test.cjs",
"tests/execute-wave-post-gate-pipeline-e2e.test.cjs",
"tests/executed-plan.test.cjs",
"tests/executor-mvp-tdd-section.test.cjs",
"tests/exit-code-registry.test.cjs",
"tests/explore-command.test.cjs",
"tests/external-descriptor-confinement.test.cjs",
"tests/failing-direction.test.cjs",
"tests/fallow-runner.test.cjs",
"tests/faulty-deps.test.cjs",
"tests/feat-2296-provider-escalation.test.cjs",
"tests/feat-2483-review-claude-mds-guard.test.cjs",
"tests/feat-2646-deferred-items-audit-scanner.test.cjs",
"tests/feat-3881-yaml-parser-consequences.test.cjs",
"tests/features-index-gate.test.cjs",
"tests/fixture-builder.test.cjs",
"tests/frontmatter-cli.test.cjs",
"tests/frontmatter.test.cjs",
"tests/gap-checker.property.test.cjs",
"tests/gate-predicate-evaluator-missing.test.cjs",
"tests/gemini-runtime-removed.test.cjs",
"tests/gen-context-index.test.cjs",
"tests/gen-health-docs.test.cjs",
"tests/gen-registry.test.cjs",
"tests/gen-section-manifest.test.cjs",
"tests/gen-state-md-docs.test.cjs",
"tests/git-base-branch.test.cjs",
"tests/git-fixture.test.cjs",
"tests/golden-install-tree.test.cjs",
"tests/graphify-command-cutover.test.cjs",
"tests/graphify-graph-path.test.cjs",
"tests/graphify-visualization.test.cjs",
"tests/graphify.test.cjs",
"tests/gsd-agent-isolation-guard.test.cjs",
"tests/gsd-check-update-worker-atomic-cache.test.cjs",
"tests/gsd-check-update-worker-platform-gate.test.cjs",
"tests/gsd-mcp-server-bin.test.cjs",
"tests/gsd-quick-batch-merge-integration.test.cjs",
"tests/gsd-quick-batch-workflow.test.cjs",
"tests/gsd-secret-read-guard.test.cjs",
"tests/gsd-settings-advanced.test.cjs",
"tests/gsd-statusline.test.cjs",
"tests/gsd-tools-path-refs.test.cjs",
"tests/gsd-validate-commit-crash-policy.test.cjs",
"tests/gsd-write-guard.property.test.cjs",
"tests/gsd-write-guard.test.cjs",
"tests/gsd2-import.test.cjs",
"tests/hardcoded-paths.test.cjs",
"tests/health-diagnostic-rules/agent-install.test.cjs",
"tests/health-diagnostic-rules/config-validation.test.cjs",
"tests/health-diagnostic-rules/consistency.test.cjs",
"tests/health-diagnostic-rules/phase-structure.test.cjs",
"tests/health-diagnostic-rules/root-existence.test.cjs",
"tests/health-diagnostic-rules/state-consistency.test.cjs",
"tests/health-diagnostic-rules/worktree-health.test.cjs",
"tests/health-diagnostic.test.cjs",
"tests/health-validation.test.cjs",
"tests/helpers-cleanup.test.cjs",
"tests/helpers-process-isolation.test.cjs",
"tests/hermes-skills-migration.test.cjs",
"tests/hooks-commonjs-marker.test.cjs",
"tests/hooks-crash-policy.test.cjs",
"tests/hooks-opt-in.test.cjs",
"tests/host-integration.test.cjs",
"tests/host-runtime-detection.test.cjs",
"tests/ingest-docs.test.cjs",
"tests/init-debug.test.cjs",
"tests/init-manager.test.cjs",
"tests/init.test.cjs",
"tests/injection-blocking-config.test.cjs",
"tests/inline-plan-threshold.test.cjs",
"tests/install-fs-adapter-seam.test.cjs",
"tests/install-minimal-hooks.test.cjs",
"tests/install-nested-layout.test.cjs",
"tests/install-path-detection.test.cjs",
"tests/install-regressions.test.cjs",
"tests/install-runtime-artifacts.test.cjs",
"tests/install-scope.test.cjs",
"tests/install-write-confinement.test.cjs",
"tests/install.test.cjs",
"tests/installed-surface-resolver.test.cjs",
"tests/installer-migration-antigravity-retire-confighome-artifacts.test.cjs",
"tests/installer-migration-authoring.test.cjs",
"tests/installer-migration-config-root-marker.test.cjs",
"tests/installer-migration-pi-retire-hooks-dir.test.cjs",
"tests/installer-migration-prune-stale-pristine.test.cjs",
"tests/installer-migration-rename-gsd-core.test.cjs",
"tests/installer-migration-report.test.cjs",
"tests/installer-migrations-manifest-schema.test.cjs",
"tests/installer-migrations.test.cjs",
"tests/intel-command-cutover.test.cjs",
"tests/intel.test.cjs",
"tests/inventory-manifest-sync.test.cjs",
"tests/inventory-nested-families.test.cjs",
"tests/io.test.cjs",
"tests/isolation-sentinel.test.cjs",
"tests/kilo-upgrades.test.cjs",
"tests/kimi-agent-converter.test.cjs",
"tests/kimi-normalize-payload.property.test.cjs",
"tests/kimi-upgrades.test.cjs",
"tests/kimi-variant-disambiguation.test.cjs",
"tests/learnings.test.cjs",
"tests/legacy-cleanup-config-dir.test.cjs",
"tests/lint-allow-test-rule-refs.test.cjs",
"tests/lint-compiled-artifact-sync.test.cjs",
"tests/lint-docs-command-form.test.cjs",
"tests/lint-frontmatter-scalar-broad-grep.test.cjs",
"tests/lint-hooks-runtime-build-seam.test.cjs",
"tests/lint-legacy-dir-name.test.cjs",
"tests/lint-planning-artifact-writer-drift.test.cjs",
"tests/lint-pr-check-project-dir.test.cjs",
"tests/lint-regression-test-names.test.cjs",
"tests/lint-seam-enforcement.test.cjs",
"tests/lint-skill-deps.test.cjs",
"tests/lint-test-file-count.test.cjs",
"tests/lint-workflow-shellcheck-fetch.test.cjs",
"tests/list-seeds.test.cjs",
"tests/live-config-guard.test.cjs",
"tests/live-dom-uat.test.cjs",
"tests/lockfile-cve-audit.test.cjs",
"tests/locking-bugs-1909-1916-1925-1927.test.cjs",
"tests/loop-hooks-empty-points-e2e.test.cjs",
"tests/loop-hooks-ship-pre-e2e.test.cjs",
"tests/loop-hooks-verify-post-e2e.test.cjs",
"tests/loop-render-hooks.test.cjs",
"tests/managed-hooks.test.cjs",
"tests/manifest-version-sync.test.cjs",
"tests/markdown-table.test.cjs",
"tests/mcp-catalog.property.test.cjs",
"tests/mcp-catalog.test.cjs",
"tests/milestone-archive.test.cjs",
"tests/milestone-lock.test.cjs",
"tests/milestone-prefixed-convention.test.cjs",
"tests/milestone-window-drift-guard.test.cjs",
"tests/milestone-window-single-owner.test.cjs",
"tests/milestone.test.cjs",
"tests/model-omit-when-inherit-guard.test.cjs",
"tests/model-profiles.test.cjs",
"tests/model-resolver.test.cjs",
"tests/mutation-matrix-ratchet.test.cjs",
"tests/mutation-score-ratchet.test.cjs",
"tests/mutation-workflow-base-ref.test.cjs",
"tests/mvp-phase-integration.test.cjs",
"tests/new-milestone-clear-phases.test.cjs",
"tests/next-decimal-roadmap-scan.test.cjs",
"tests/next-up-clear-order.test.cjs",
"tests/no-bare-gsd-tools-command-position.test.cjs",
"tests/no-bare-npm-exec.rule.test.cjs",
"tests/no-dead-sdk-refs.test.cjs",
"tests/no-exact-case-env-access.rule.test.cjs",
"tests/no-hardcoded-home-gsd-tools.test.cjs",
"tests/no-hardcoded-tmp.rule.test.cjs",
"tests/no-path-literal-in-assert.rule.test.cjs",
"tests/no-pending-3212-markers.test.cjs",
"tests/no-phantom-issue-refs.test.cjs",
"tests/no-posix-mode-bit-assert.rule.test.cjs",
"tests/no-private-binary-resolution.rule.test.cjs",
"tests/no-rendered-text-length-assert.rule.test.cjs",
"tests/no-swallowed-precondition.rule.test.cjs",
"tests/no-unbounded-dirname-walk.rule.test.cjs",
"tests/no-unbounded-spawn-allowlist.test.cjs",
"tests/no-unbounded-spawn.test.cjs",
"tests/no-unguarded-nonportable-exec.rule.test.cjs",
"tests/normalize-path-in-content.rule.test.cjs",
"tests/normalize-test-command.test.cjs",
"tests/npm-audit-baseline.test.cjs",
"tests/npm-integrity-gate.test.cjs",
"tests/onboard-command.test.cjs",
"tests/opencode-command-dir-plural.test.cjs",
"tests/opencode-permissions.test.cjs",
"tests/opencode-plugin-adapter.test.cjs",
"tests/orphan-worktree-detection.test.cjs",
"tests/orphaned-hooks.test.cjs",
"tests/overlay-repo-helpers.test.cjs",
"tests/package-name-single-source.test.cjs",
"tests/packaging-shipped-scripts-require-only-shipped.test.cjs",
"tests/parallel-dependent-plans.test.cjs",
"tests/path-replacement.test.cjs",
"tests/pattern-mapper.test.cjs",
"tests/pattern.test.cjs",
"tests/pause-work-context-detection.test.cjs",
"tests/perf-317-context-monitor-fs.test.cjs",
"tests/phase-command-router.test.cjs",
"tests/phase-completion-single-owner.test.cjs",
"tests/phase-estimation.test.cjs",
"tests/phase-id-drift-guard.test.cjs",
"tests/phase-locator.test.cjs",
"tests/phase-resolution-parity.test.cjs",
"tests/phase-tdd-applicable.test.cjs",
"tests/phase.test.cjs",
"tests/phase6-capstone-conformance.test.cjs",
"tests/phases-command-router.test.cjs",
"tests/pi-config-dir-env-override.test.cjs",
"tests/pi-extension-reachability.test.cjs",
"tests/pick-flag.test.cjs",
"tests/plan-bounce.test.cjs",
"tests/plan-count-single-owner.test.cjs",
"tests/plan-phase-drift-guard.test.cjs",
"tests/plan-phase-mvp-flag.test.cjs",
"tests/plan-phase-stall-detection.test.cjs",
"tests/plan-pre-hook-e2e.test.cjs",
"tests/plan-review-convergence.test.cjs",
"tests/planning-inspect.test.cjs",
"tests/planning-inspect.unit.test.cjs",
"tests/planning-lock-mkdir-failure-1884.test.cjs",
"tests/planning-prompt-drift.test.cjs",
"tests/planning-snapshot-bypass-drift.test.cjs",
"tests/planning-snapshot.test.cjs",
"tests/planning-workspace.test.cjs",
"tests/platform-conformance-tier.test.cjs",
"tests/platform-guard.unit.test.cjs",
"tests/playwright-ui-verify.test.cjs",
"tests/plugin-manifest.test.cjs",
"tests/policy-160-route0-resume.test.cjs",
"tests/policy-shell-pinning.test.cjs",
"tests/portability-rule-disable-ban.test.cjs",
"tests/portability-vocab-drift.test.cjs",
"tests/post-planning-gaps-2493.test.cjs",
"tests/pr-branch-planning-filter.test.cjs",
"tests/precommit-alias-drift-hook.test.cjs",
"tests/precondition-element.test.cjs",
"tests/prepush-enterprise-email-hook.test.cjs",
"tests/probe-core.test.cjs",
"tests/process-seam.test.cjs",
"tests/profile-output.test.cjs",
"tests/profile-pipeline.test.cjs",
"tests/prohibition-enforcement.test.cjs",
"tests/prohibition-probe.verify-tier.test.cjs",
"tests/project-instruction-file-parity.test.cjs",
"tests/project-root.test.cjs",
"tests/prompt-budget-cli.test.cjs",
"tests/prune-orphaned-worktrees.test.cjs",
"tests/quick-batch-command-router.test.cjs",
"tests/quick-batch.test.cjs",
"tests/quick-branching.test.cjs",
"tests/quick-research.test.cjs",
"tests/quick-review-scope-tip-bound.test.cjs",
"tests/qwen-upgrades.test.cjs",
"tests/read-guard.test.cjs",
"tests/read-injection-scanner.property.test.cjs",
"tests/reapply-patches.test.cjs",
"tests/reapply-verify-hunks.test.cjs",
"tests/refactor-trigger-cli.test.cjs",
"tests/registry-reviewer-parity.test.cjs",
"tests/release-hotfix-empty-cherry-pick.test.cjs",
"tests/removed-but-needed-lint.test.cjs",
"tests/repo-invariants.test.cjs",
"tests/repo-layout.test.cjs",
"tests/representative-corpus.test.cjs",
"tests/require-fs-op-fallback.rule.test.cjs",
"tests/require-full-tmpdir-triad.rule.test.cjs",
"tests/require-issue-link-policy.test.cjs",
"tests/require-userprofile-with-home.rule.test.cjs",
"tests/research-cli.test.cjs",
"tests/research-store.test.cjs",
"tests/resolve-execution-dynamic-routing.test.cjs",
"tests/resolver-hoist-guard.test.cjs",
"tests/response-language-coverage.test.cjs",
"tests/retired-artifact-cleanup.test.cjs",
"tests/reversibility-tagging.test.cjs",
"tests/review-build-prompt-optional-sections.test.cjs",
"tests/review-default-reviewers-config.test.cjs",
"tests/review-default-reviewers-workflow.test.cjs",
"tests/review-lane-descriptor.test.cjs",
"tests/review-lane-invocation.test.cjs",
"tests/review-lane-runner.test.cjs",
"tests/review-lane-windows-spawn-resolution.test.cjs",
"tests/review-model-config.test.cjs",
"tests/review-parallel-lanes.test.cjs",
"tests/review-plan-coverage-manifest.test.cjs",
"tests/review-reviewer-instances-config.test.cjs",
"tests/reviewer-config-federation.test.cjs",
"tests/reviewer-docs-parity.test.cjs",
"tests/reviewer-lane-declarations.test.cjs",
"tests/reviewer-manifest-body.test.cjs",
"tests/reviewer-step-dispatch.test.cjs",
"tests/reviewer-trust-disclosure.test.cjs",
"tests/revision-remediation-binding.test.cjs",
"tests/roadmap-mode-field.test.cjs",
"tests/roadmap-parser.test.cjs",
"tests/roadmap-phase-fallback.test.cjs",
"tests/roadmap-upgrade.test.cjs",
"tests/roadmap.test.cjs",
"tests/roadmapper-granularity.test.cjs",
"tests/run-tests-harness.test.cjs",
"tests/run-tests-temp-root.test.cjs",
"tests/run-with-timeout.test.cjs",
"tests/runtime-artifact-install-plan.test.cjs",
"tests/runtime-artifact-layout-descriptor-drive.test.cjs",
"tests/runtime-artifact-layout-install-profiles.test.cjs",
"tests/runtime-artifact-layout-surface.test.cjs",
"tests/runtime-artifact-layout.test.cjs",
"tests/runtime-converters.test.cjs",
"tests/runtime-homes-descriptor-drive.test.cjs",
"tests/runtime-homes.property.test.cjs",
"tests/runtime-identity.test.cjs",
"tests/runtime-launcher-parity.test.cjs",
"tests/runtime-name-policy.test.cjs",
"tests/safe-resume-gate-anchoring.test.cjs",
"tests/schema-drift.test.cjs",
"tests/sdk-removal-query-family-dispatch.test.cjs",
"tests/section-manifest-init-facts.test.cjs",
"tests/section-manifest.test.cjs",
"tests/security.test.cjs",
"tests/settings-integrations.test.cjs",
"tests/settings-jsonc.test.cjs",
"tests/sh-hook-paths.test.cjs",
"tests/shadow-report.test.cjs",
"tests/shared-hooks-dir-resolution.test.cjs",
"tests/shell-command-projection-dispatch.test.cjs",
"tests/shell-command-projection-path-sep.test.cjs",
"tests/ship-notes-wedged-pr.test.cjs",
"tests/shipped-reference-cites.test.cjs",
"tests/skill-frontmatter-contract.test.cjs",
"tests/skill-manifest.test.cjs",
"tests/slash-command-namespace.test.cjs",
"tests/slug-derivation-drift-guard.test.cjs",
"tests/smart-entry.unit.test.cjs",
"tests/spec-section.test.cjs",
"tests/stale-bake-guard.test.cjs",
"tests/state-command-cutover.test.cjs",
"tests/state-contract.test.cjs",
"tests/state-document.test.cjs",
"tests/state-field-drift.test.cjs",
"tests/state-io.test.cjs",
"tests/state-prune.test.cjs",
"tests/state-rebuild-cli.test.cjs",
"tests/state-todos-render.test.cjs",
"tests/state-transition.test.cjs",
"tests/state-write-path-drift-guard.test.cjs",
"tests/state.test.cjs",
"tests/stats-mvp-display.test.cjs",
"tests/stats-phase-id-shape.test.cjs",
"tests/subagent-timeout.test.cjs",
"tests/summary-status-blocked-3345.test.cjs",
"tests/surface-md-paths.regression.test.cjs",
"tests/sync-skills-cross-runtime-refuse.test.cjs",
"tests/table-schema-drift-lint.test.cjs",
"tests/tdd-backend-wiring.test.cjs",
"tests/tdd-mode.test.cjs",
"tests/tdd-red-evidence.test.cjs",
"tests/tdd-single-statement.test.cjs",
"tests/teams-status.test.cjs",
"tests/template.test.cjs",
"tests/thinking-partner.test.cjs",
"tests/todos-done-rename-guard.test.cjs",
"tests/todos-workstream-scope.test.cjs",
"tests/tracer-bullet.test.cjs",
"tests/trae-imperative-reference.test.cjs",
"tests/tsconfig-noemit.test.cjs",
"tests/uat-predicate.test.cjs",
"tests/uat.test.cjs",
"tests/ui-spec-inventory-provenance.test.cjs",
"tests/ultraplan-phase.test.cjs",
"tests/unreachable-guard-drift.test.cjs",
"tests/unreachable-shell-guard.test.cjs",
"tests/unusable-input.test.cjs",
"tests/update-context.test.cjs",
"tests/update-custom-backup.test.cjs",
"tests/update-workflow.test.cjs",
"tests/user-artifact-staging.test.cjs",
"tests/validate-context.test.cjs",
"tests/validate-registry.test.cjs",
"tests/verification-status.test.cjs",
"tests/verify-archive-dirs-live-path.test.cjs",
"tests/verify-command-grounding.test.cjs",
"tests/verify-health.test.cjs",
"tests/verify.test.cjs",
"tests/vscode-browser-no-node-api.test.cjs",
"tests/vscode-extension-reachability.test.cjs",
"tests/windows-robustness.test.cjs",
"tests/windsurf-conversion.test.cjs",
"tests/windsurf-hooks-bridge.test.cjs",
"tests/windsurf-install.test.cjs",
"tests/workflow-compat.test.cjs",
"tests/workflow-fragments.test.cjs",
"tests/workflow-guard.test.cjs",
"tests/workflow-maintainer-skip.test.cjs",
"tests/workflow-shell-pinning.test.cjs",
"tests/workspace.test.cjs",
"tests/workstream-inventory.test.cjs",
"tests/workstream-scoped-paths.test.cjs",
"tests/workstream.test.cjs",
"tests/worktree-base-ref.test.cjs",
"tests/worktree-baseref-install.test.cjs",
"tests/worktree-cleanup.test.cjs",
"tests/worktree-safety-reap.test.cjs",
"tests/worktree-safety.test.cjs",
"tests/worktree.test.cjs",
],
};

View File

@@ -0,0 +1,32 @@
'use strict';
/**
* Suite classification by filename suffix (#4591 packaging fix — extracted
* from scripts/run-tests.cjs so a SHIPPED script, e.g.
* scripts/gen-platform-conformance-tier.cjs, can depend on it: run-tests.cjs
* itself does not ship (it is a repo-development-only tool), so a shipped
* file requiring it directly is MODULE_NOT_FOUND in a published install
* (tests/packaging-shipped-scripts-require-only-shipped.test.cjs, #2858).
* run-tests.cjs re-exports suiteOf from here unchanged for full backward
* compatibility — this is a pure relocation, not a behavior change.
*
* A file with no suite suffix (plain `foo.test.cjs`) belongs to the `unit`
* suite and returns `null` here; a suite-tagged file (`foo.install.test.cjs`)
* returns its marker string.
*/
const { basename } = require('node:path');
const MARKED_SUITES = ['integration', 'install', 'security', 'slow', 'qa'];
function suiteOf(filename) {
const name = basename(filename);
if (!name.endsWith('.test.cjs')) return null;
const base = name.slice(0, -'.test.cjs'.length);
const lastDot = base.lastIndexOf('.');
if (lastDot === -1) return null;
const marker = base.slice(lastDot + 1);
return MARKED_SUITES.includes(marker) ? marker : null;
}
module.exports = { MARKED_SUITES, suiteOf };

View File

@@ -41,6 +41,7 @@ const { tmpdir } = require('os');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
const { suiteOf } = require('./lib/suite-detection.cjs');
const {
resolveLiveConfigRoots,
resolveExtraWatchTargets,
@@ -179,8 +180,6 @@ function ensureBuiltHooks(overrides = {}) {
runBuild();
}
}
const MARKED_SUITES = ['integration', 'install', 'security', 'slow', 'qa'];
// Recursively collect *.test.cjs files under dir, returning paths relative to dir.
// Skips node_modules to avoid accidentally picking up decoy files.
function walkTestFiles(dir, relBase) {
@@ -794,20 +793,8 @@ function parseArgs(argv) {
return { suite, files, filesFrom, shard };
}
// Return the marked suite name embedded in a filename, or null if it's unmarked.
// foo.security.test.cjs -> "security"
// foo.test.cjs -> null (unit)
// Accepts either a bare filename or a relative subdir path; classification is
// based on the basename only so subdir paths classify identically to root files.
function suiteOf(filename) {
const name = basename(filename);
if (!name.endsWith('.test.cjs')) return null;
const base = name.slice(0, -'.test.cjs'.length);
const lastDot = base.lastIndexOf('.');
if (lastDot === -1) return null;
const marker = base.slice(lastDot + 1);
return MARKED_SUITES.includes(marker) ? marker : null;
}
// suiteOf (and its backing MARKED_SUITES) is imported from
// ./lib/suite-detection.cjs — see that module's header comment for why.
function selectFiles(allFiles, suite) {
if (suite === null || suite === 'all') {

View File

@@ -253,6 +253,7 @@ test('near-cap check CI_JOB_TIMEOUT_MINUTES literals match each job\'s own timeo
const staticLanes = [
{ workflowFile: 'test.yml', jobKey: 'test', envLiteral: '32' },
{ workflowFile: 'test.yml', jobKey: 'test-full', envLiteral: '45' },
{ workflowFile: 'test.yml', jobKey: 'test-conformance', envLiteral: '45' },
{ workflowFile: 'install-smoke.yml', jobKey: 'smoke', envLiteral: '12' },
];
@@ -294,5 +295,10 @@ test('near-cap check CI_JOB_TIMEOUT_MINUTES literals match each job\'s own timeo
assert.equal(testWorkflow.jobs['coverage-gate'].name, 'Coverage gate (merged shards)',
'test.yml jobs.coverage-gate.name changed — update JOB_RULES to match');
assert.equal(coverageGateRule.test('Coverage gate (merged shards)'), true);
const testConformanceRule = JOB_RULES.find((r) => r.workflowFile === 'test.yml' && r.jobKey === 'test-conformance');
assert.ok(testWorkflow.jobs['test-conformance'].name.startsWith('conformance test ('),
'test.yml jobs.test-conformance.name no longer starts with "conformance test (" — update JOB_RULES to match');
assert.equal(testConformanceRule.test('conformance test (windows-latest, 24, shard 1/3)'), true);
});
});

View File

@@ -624,7 +624,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -695,7 +695,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-ns-context/SKILL.md",
"skills/gsd-ns-context/skills/docs-update/SKILL.md",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md",

View File

@@ -559,5 +559,7 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/shellcheck-fetch.cjs"
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs"
]

View File

@@ -623,7 +623,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -585,7 +585,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-ns-context/SKILL.md",
"skills/gsd-ns-context/skills/docs-update/SKILL.md",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md",

View File

@@ -695,7 +695,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -623,5 +623,7 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/shellcheck-fetch.cjs"
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs"
]

View File

@@ -585,7 +585,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -596,7 +596,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -623,7 +623,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd/DESCRIPTION.md",
"skills/gsd/gsd-ns-context/SKILL.md",
"skills/gsd/gsd-ns-context/skills/docs-update/SKILL.md",

View File

@@ -698,7 +698,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -624,7 +624,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -591,7 +591,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -698,7 +698,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-add-tests/SKILL.md",
"skills/gsd-ai-integration-phase/SKILL.md",
"skills/gsd-audit-fix/SKILL.md",

View File

@@ -425,5 +425,7 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/shellcheck-fetch.cjs"
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs"
]

View File

@@ -623,7 +623,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-ns-context/SKILL.md",
"skills/gsd-ns-context/skills/docs-update/SKILL.md",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md",

View File

@@ -583,7 +583,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-ns-context/SKILL.md",
"skills/gsd-ns-context/skills/docs-update/SKILL.md",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md",

View File

@@ -518,5 +518,7 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/shellcheck-fetch.cjs"
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs"
]

View File

@@ -655,7 +655,9 @@
"scripts/lib/exit-code-registry.cjs",
"scripts/lib/ndjson-reporter.cjs",
"scripts/lib/npm-version-check-diagnosis.cjs",
"scripts/lib/platform-conformance-tier.generated.cjs",
"scripts/lib/shellcheck-fetch.cjs",
"scripts/lib/suite-detection.cjs",
"skills/gsd-ns-context/SKILL.md",
"skills/gsd-ns-context/skills/docs-update/SKILL.md",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md",

View File

@@ -0,0 +1,307 @@
'use strict';
/**
* Test matrix: .gsd/phase/chore-4591-conformance-tier-linux-primary/50-test-matrix.md
*
* Rows 1-15 exercise the pure, exported `classifyContent(content)` classifier
* directly on short string fixtures. Rows 16-18 exercise the
* `--check`/`--write` CLI behavior against a small temp fixture tree, driven
* through the process seam (tests/helpers/process-seam.cjs's `runNode`) per
* CONTRIBUTING.md's "spawning a subprocess: use the process seam" rule. Row
* 19 is a real-tree regression proving the committed generated file matches a
* fresh sweep of this repo's actual tests/ tree. Rows 20-21 prove the
* suite-exclusion fix (#4591 CI incident): a suite-tagged file must never
* enter the conformance-tier pool even when its content would otherwise
* qualify, and the committed generated file must contain zero such files.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { runNode } = require('./helpers/process-seam.cjs');
const { classifyContent, classifyTree } = require('../scripts/gen-platform-conformance-tier.cjs');
const ROOT = path.resolve(__dirname, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'gen-platform-conformance-tier.cjs');
const GENERATED_PATH = path.join(ROOT, 'scripts', 'lib', 'platform-conformance-tier.generated.cjs');
// ─── Rows 1-15: classifyContent, pure fixtures ────────────────────────────────
describe('classifyContent — happy-path signals', () => {
test('flags process.platform', () => {
const { needsRealOs, signals } = classifyContent("if (process.platform === 'win32') { doThing(); }");
assert.equal(needsRealOs, true);
assert.ok(signals.includes('process-platform'));
});
test('flags os.platform()', () => {
const { needsRealOs, signals } = classifyContent("const os = require('node:os');\nconst p = os.platform();");
assert.equal(needsRealOs, true);
assert.ok(signals.includes('os-platform'));
});
test('flags win32 literal', () => {
const { needsRealOs, signals } = classifyContent("const platforms = ['win32', 'linux'];");
assert.equal(needsRealOs, true);
assert.ok(signals.includes('win32-darwin-literal'));
});
test('flags darwin literal', () => {
const { needsRealOs, signals } = classifyContent("const platforms = ['darwin', 'linux'];");
assert.equal(needsRealOs, true);
assert.ok(signals.includes('win32-darwin-literal'));
});
test('flags chmod mode-bit octal', () => {
const { needsRealOs, signals } = classifyContent('fs.chmodSync(target, 0o755);');
assert.equal(needsRealOs, true);
assert.ok(signals.includes('chmod-mode-bit'));
});
test('flags Windows-shell tokens', () => {
for (const fixture of ["spawn('cmd.exe', args)", "spawn('powershell', args)", 'const e = process.env.ComSpec;']) {
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, true, fixture);
assert.ok(signals.includes('windows-shell-token'), fixture);
}
});
test('flags Windows env-var names', () => {
for (const fixture of [
'const home = process.env.USERPROFILE;',
'const drive = process.env.HOMEDRIVE;',
'const p = process.env.HOMEPATH;',
]) {
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, true, fixture);
assert.ok(signals.includes('windows-env-var'), fixture);
}
});
test('flags process-seam subprocess helpers', () => {
for (const fixture of [
"runNode(['--check'])",
"runGit(['status'])",
"runHook(HOOK_PATH, [])",
"runGsdTools(['state', 'show'])",
]) {
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, true, fixture);
assert.ok(signals.includes('process-seam-subprocess'), fixture);
}
});
test('flags raw child_process usage', () => {
const fixture = "const { execFileSync } = require('child_process');\nexecFileSync('ls', []);";
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, true);
assert.ok(signals.includes('raw-child-process'));
});
test('flags symlink keyword', () => {
const { needsRealOs, signals } = classifyContent('fs.symlinkSync(target, link);');
assert.equal(needsRealOs, true);
assert.ok(signals.includes('symlink-keyword'));
});
test('flags hardcoded path literal vs path.* call', () => {
const fixture = "const p = path.join(root, 'x');\nassert.equal(rendered, '/etc/passwd');";
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, true);
assert.ok(signals.includes('hardcoded-path-vs-path-call'));
});
});
describe('classifyContent — negative / hostile inputs', () => {
test('does not flag a clean in-process unit test', () => {
const fixture = "const assert = require('node:assert/strict');\ntest('adds numbers', () => { assert.equal(1 + 1, 2); });";
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, false);
assert.deepEqual(signals, []);
});
test('does not flag incidental use of the word "path"', () => {
const fixture = '// This test verifies the correct path through the state machine.\nassert.ok(true);';
const { needsRealOs } = classifyContent(fixture);
assert.equal(needsRealOs, false);
});
test('does not crash on empty content', () => {
assert.doesNotThrow(() => classifyContent(''));
const { needsRealOs, signals } = classifyContent('');
assert.equal(needsRealOs, false);
assert.deepEqual(signals, []);
});
test('does not flag an unrelated identifier containing "spawn"', () => {
const fixture = 'const spawnResult = computeSomething();\nassert.ok(spawnResult);';
const { needsRealOs, signals } = classifyContent(fixture);
assert.equal(needsRealOs, false);
assert.deepEqual(signals, []);
});
});
// ─── Rows 16-18: CLI --check/--write against a temp fixture tree ──────────────
/** Spawn the real generator CLI via the process seam. */
function runGen(args) {
return runNode([SCRIPT, ...args]);
}
describe('gen-platform-conformance-tier.cjs CLI (temp fixture tree)', () => {
test('--check passes when the generated file is fresh', () => {
const tmpDir = createTempDir('gen-platform-conformance-tier-');
try {
const testsDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testsDir, { recursive: true });
fs.writeFileSync(path.join(testsDir, 'flagged.test.cjs'), "if (process.platform === 'win32') {}\n");
fs.writeFileSync(path.join(testsDir, 'clean.test.cjs'), "assert.equal(1 + 1, 2);\n");
const outPath = path.join(tmpDir, 'platform-conformance-tier.generated.cjs');
const write = runGen(['--write', '--tests-dir', testsDir, '--out', outPath]);
assert.equal(write.exitCode, 0, write.stderr);
assert.ok(fs.existsSync(outPath));
const check = runGen(['--check', '--tests-dir', testsDir, '--out', outPath]);
assert.equal(check.exitCode, 0, check.stderr);
assert.match(check.stdout, /ok gen-platform-conformance-tier/);
} finally {
cleanup(tmpDir);
}
});
test('--check fails and names the drift when the list is stale', () => {
const tmpDir = createTempDir('gen-platform-conformance-tier-');
try {
const testsDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testsDir, { recursive: true });
fs.writeFileSync(path.join(testsDir, 'flagged.test.cjs'), "if (process.platform === 'win32') {}\n");
fs.writeFileSync(path.join(testsDir, 'clean.test.cjs'), "assert.equal(1 + 1, 2);\n");
const outPath = path.join(tmpDir, 'platform-conformance-tier.generated.cjs');
const write = runGen(['--write', '--tests-dir', testsDir, '--out', outPath]);
assert.equal(write.exitCode, 0, write.stderr);
// Introduce drift: a brand-new signal-bearing file the committed list
// has never seen.
fs.writeFileSync(path.join(testsDir, 'new-signal.test.cjs'), 'fs.symlinkSync(target, link);\n');
const check = runGen(['--check', '--tests-dir', testsDir, '--out', outPath]);
assert.equal(check.exitCode, 1);
const combined = check.stdout + check.stderr;
assert.match(combined, /tests\/new-signal\.test\.cjs/, 'the drift report must name the new file');
assert.match(combined, /\+/, 'a newly-added file is reported with a + marker');
} finally {
cleanup(tmpDir);
}
});
test('--write is deterministic across repeated runs', () => {
const tmpDir = createTempDir('gen-platform-conformance-tier-');
try {
const testsDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testsDir, { recursive: true });
fs.writeFileSync(path.join(testsDir, 'a.test.cjs'), "process.env.PATHEXT;\n");
fs.writeFileSync(path.join(testsDir, 'b.test.cjs'), 'fs.symlinkSync(target, link);\n');
const out1 = path.join(tmpDir, 'out1.generated.cjs');
const out2 = path.join(tmpDir, 'out2.generated.cjs');
assert.equal(runGen(['--write', '--tests-dir', testsDir, '--out', out1]).exitCode, 0);
assert.equal(runGen(['--write', '--tests-dir', testsDir, '--out', out2]).exitCode, 0);
const content1 = fs.readFileSync(out1, 'utf8');
const content2 = fs.readFileSync(out2, 'utf8');
assert.equal(content1, content2, '--write must be byte-identical across independent runs over the same input');
} finally {
cleanup(tmpDir);
}
});
});
// ─── Row 20: suite-tagged files are excluded even when their content qualifies
describe('gen-platform-conformance-tier.cjs CLI (temp fixture tree) — suite exclusion', () => {
test('a suite-suffixed file is excluded even with a qualifying content signal', () => {
const tmpDir = createTempDir('gen-platform-conformance-tier-suite-');
try {
const testsDir = path.join(tmpDir, 'tests');
fs.mkdirSync(testsDir, { recursive: true });
const signal = "if (process.platform === 'win32') {}\n";
fs.writeFileSync(path.join(testsDir, 'foo.test.cjs'), signal);
fs.writeFileSync(path.join(testsDir, 'foo.install.test.cjs'), signal);
const outPath = path.join(tmpDir, 'platform-conformance-tier.generated.cjs');
const write = runGen(['--write', '--tests-dir', testsDir, '--out', outPath]);
assert.equal(write.exitCode, 0, write.stderr);
delete require.cache[require.resolve(outPath)];
const generated = require(outPath);
assert.deepEqual(
generated.CONFORMANCE_TIER_FILES,
['tests/foo.test.cjs'],
'the install-suite file must be excluded even though its content would otherwise qualify',
);
} finally {
cleanup(tmpDir);
}
});
});
// ─── Row 19: real tests/ tree, regression against the committed artifact ─────
describe('gen-platform-conformance-tier.cjs — real repo tree (regression)', () => {
test('real tests/ tree classification matches the committed list', () => {
const realTestsDir = path.join(ROOT, 'tests');
let fresh;
assert.doesNotThrow(() => {
fresh = classifyTree(realTestsDir);
}, 'a full sweep of the real tests/ tree must complete without throwing');
// Measured 546/952 at authoring time (#4591, post suite-exclusion fix) —
// the range below is a sanity ballpark with headroom for organic
// test-suite growth in either direction, not a brittle exact-match on
// that literal.
assert.ok(
fresh.files.length >= 450 && fresh.files.length <= 700,
`expected a real, current, sanity-checked count in [450, 700], got ${fresh.files.length}`,
);
delete require.cache[require.resolve(GENERATED_PATH)];
const committed = require(GENERATED_PATH);
assert.equal(
committed.CONFORMANCE_TIER_FILES.length,
fresh.files.length,
'the committed generated file must be fresh — run `node scripts/gen-platform-conformance-tier.cjs --write`',
);
assert.deepEqual(
committed.CONFORMANCE_TIER_FILES.slice().sort(),
fresh.files.slice().sort(),
'the committed list must match a fresh sweep exactly, not just in length',
);
});
// ─── Row 21: no suite-tagged file ever reaches the committed conformance
// tier — the exact assertion that would have caught the CI incident before
// it ever shipped.
test('the committed conformance-tier list contains zero suite-tagged files', () => {
delete require.cache[require.resolve(GENERATED_PATH)];
const { CONFORMANCE_TIER_FILES } = require(GENERATED_PATH);
const suiteTaggedPattern = /\.(install|security|slow|integration|qa)\.test\.cjs$/;
const offenders = CONFORMANCE_TIER_FILES.filter((f) => suiteTaggedPattern.test(f));
assert.deepEqual(
offenders,
[],
'suite-tagged files (install/security/slow/integration/qa) must never appear in the ' +
'conformance-tier list — install/slow are PR-excluded suites and integration/security ' +
'already run via their own dedicated steps',
);
});
});