Merge pull request #2343 from modelcontextprotocol/v2/chore/2341-gate-single-test-pass
chore: run each client's test suite once in local:gate, not twice
This commit is contained in:
commit
aa56551be2
19 changed files with 211 additions and 41 deletions
|
|
@ -30,10 +30,19 @@ review, which is the argument for not making a fourth. `npm run local:gate`
|
|||
prints each stage as it starts, so the running command is the other reliable
|
||||
answer.
|
||||
|
||||
It is a **strict superset** of GitHub CI (which additionally runs `npm install`,
|
||||
and runs `coverage` as a parallel job). So the direction that matters holds:
|
||||
**passing `local:gate` locally means CI's gates will pass.** The reverse does
|
||||
not.
|
||||
It runs **every check** GitHub CI runs (which additionally runs `npm install`,
|
||||
and runs `coverage` as a parallel job), plus two local-only steps. So the
|
||||
direction that matters holds: **passing `local:gate` locally means every check
|
||||
CI applies has already passed on your machine** — the strongest predictor of a
|
||||
green CI there is here, though not a proof (a different OS, and the bare test
|
||||
pass noted below). The reverse does not hold at all.
|
||||
|
||||
One difference in *invocations*, not checks: CI runs each client's unit suite
|
||||
twice — bare inside `validate`, instrumented inside `coverage` — on two
|
||||
parallel runners, while the gate runs it **once**, instrumented (#2341). The
|
||||
gate's first stage is `local:validate`, which is `validate` minus each client's
|
||||
`test` leg; `npm run validate` itself is unchanged. The reasoning is in
|
||||
[`AGENTS.md`](../../../AGENTS.md#mandatory-pre-push-gate).
|
||||
|
||||
⚠️ **There is no `npm run ci`.** The gate was renamed to `local:gate` (#2146)
|
||||
precisely because `npm ci` is a built-in that clean-installs from the lockfile
|
||||
|
|
|
|||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
|
|
@ -49,7 +49,9 @@ jobs:
|
|||
# (#2159), which consumes nothing this job produces — every client's
|
||||
# `test:coverage` builds whatever it needs itself. Unit tests run in
|
||||
# both jobs (fast here, instrumented there); now that the two run in
|
||||
# parallel that duplication costs no wall clock at all.
|
||||
# parallel that duplication costs no wall clock at all. (The local gate,
|
||||
# which runs serially, calls `local:validate` instead and runs them once
|
||||
# — #2341. Keep `validate` here: this job IS the fast inner loop.)
|
||||
# A future optimization could split `coverage` into per-client parallel
|
||||
# jobs, but that's a larger restructure and deliberately out of scope.
|
||||
run: npm run validate
|
||||
|
|
@ -109,9 +111,10 @@ jobs:
|
|||
|
||||
- name: Run cross-client smokes
|
||||
# NOTE: this workflow runs GitHub CI's tier only. The LOCAL pre-push
|
||||
# gate is `npm run local:gate`, and it is a strict superset — do not add
|
||||
# it, a `local:*` script, `smoke:web:firefox`/`smoke:web:webkit`,
|
||||
# `smoke:web:engine`, or a non-Chromium `SMOKE_BROWSER` to any workflow.
|
||||
# gate is `npm run local:gate`, and it runs every check here plus the
|
||||
# local-only ones — do not add it, a `local:*` script,
|
||||
# `smoke:web:firefox`/`smoke:web:webkit`, `smoke:web:engine`, or a
|
||||
# non-Chromium `SMOKE_BROWSER` to any workflow.
|
||||
# `npm run test:scripts` fails if you do (scripts/lib/workflow-gate.mjs,
|
||||
# #2146). `npm run smoke` and `smoke:web:chromium` belong here and are
|
||||
# not affected. The canonical CI-vs-local table is in the root README.
|
||||
|
|
|
|||
|
|
@ -217,8 +217,8 @@ Skills are conditional — a skill's body loads only when it is invoked — so a
|
|||
skill that stops being reachable loses behavior **silently**. Four rules keep
|
||||
that from happening:
|
||||
|
||||
1. **`npm run verify:skills` must pass.** It runs inside `validate` (and so in
|
||||
`local:gate` and in CI). It parses each `SKILL.md`'s frontmatter the way Claude
|
||||
1. **`npm run verify:skills` must pass.** It runs inside `validate:guards`,
|
||||
which `validate` (CI) and `local:validate` (the gate) both run first. It parses each `SKILL.md`'s frontmatter the way Claude
|
||||
Code does and fails on anything that would strip the metadata — most importantly
|
||||
**malformed YAML**, which loads the body with an _empty_ description, so
|
||||
`/skill-name` still works and a manual spot check passes while the skill can
|
||||
|
|
@ -473,7 +473,8 @@ free to run a full `local:gate`). Raising a budget nobody chose hides no race.
|
|||
## Mandatory pre-push gate
|
||||
|
||||
- **ALWAYS run `npm run format` before committing.** The **root** `format` auto-fixes `core/`, the root `scripts/` tooling, the root shared surface, and every client's scope in one shot. `validate` runs the non-fixing `format:check` and will fail in CI on any unformatted file, so run the auto-fixer first rather than letting `format:check` catch it.
|
||||
- **`npm run local:gate` is the mandatory pre-push command.** It is a **strict superset** of `.github/workflows/main.yml`, so passing it locally means CI's gates will pass. Expect several minutes.
|
||||
- **`npm run local:gate` is the mandatory pre-push command.** It runs **every check** `.github/workflows/main.yml` runs, plus two local-only steps, so a green run here is the strongest predictor of a green CI this repo has: every check CI applies has already passed on your machine. It is not a proof — CI runs on a different OS and, since #2341, runs each client's suite bare where the gate runs it only instrumented — so the one residual is a test that passes *only* when slowed down, which is a race (#1596) to fix, never headroom to keep. Expect several minutes.
|
||||
- **The gate runs each client's test suite once, instrumented; CI runs it twice (#2341).** CI's `build` job runs the bare `test` inside `validate` and its parallel `coverage` job runs `test:coverage`, which costs CI no wall clock. Serially in one process the bare pass was ~80s of a ~370s gate, re-running exactly the files the coverage pass runs a few minutes later — so the gate calls **`local:validate`**, which is `validate` minus each client's `test` leg (every client's `validate` is `check && test`, and `local:validate` runs the `check` half). "Every check" is therefore a claim about checks, not invocations, and it holds because `@vitest/coverage-v8` collects coverage from V8's own profiler (`Profiler.takePreciseCoverage`) and rewrites no source: a test file sees identical code either way, and the instrumented run is only *slower*, which makes it the stricter of the two for the failure class this repo actually sees (a correct test cut off under load). A test that passed only *because* it ran slower would be a race — #1596's class, a defect wherever it surfaces — and CI's bare pass still runs it. **`npm run validate` is unchanged**, because CI runs it directly and it is the inner-loop check; `verify:*` guards that ask "is this reachable from `validate`" are unaffected. `local:validate` lives in the `local:` namespace so the workflow guard keeps it out of CI by construction.
|
||||
- **`npm run validate` is the fast inner-loop check and is NOT an acceptable substitute.** It runs `test`, not `test:coverage`, so it does **zero** coverage gating, no smokes, and no Storybook tests. Skipping the gate is how a push passes every fast local check and still fails CI.
|
||||
- There is deliberately **no `npm run ci`** — that name collided with the `npm ci` built-in, which clean-installs from the lockfile and does not run this script.
|
||||
- What each stage covers, and why two of them are local-only, is [`docs/quality-gate.md`](./docs/quality-gate.md); how to diagnose a failing stage is the `pre-push-gate` skill.
|
||||
|
|
@ -508,7 +509,7 @@ The two coverage guards do **not** catch this, and adding a third is not the fix
|
|||
|
||||
**Every `lint` script runs with `--max-warnings 0`, so a warning fails `validate` exactly as an error does (#2085).** All six scopes carry the flag — each of `clients/{web,cli,tui,launcher}`'s `eslint .`, plus the root's `lint:core` and `lint:shared`.
|
||||
|
||||
This exists because the gate's promise — that passing `npm run local:gate` locally means CI's gates pass — was kept while a real bug walked through it. `react-hooks/exhaustive-deps` ships at `warn` in the recommended set, and two `useCallback`s in `App.tsx` omitted a non-stable `refresh` from their dependency arrays; ESLint printed the right message on both lines on every run, nothing consumed it, and the stale closure was caught only by a review round on #2076. It is the same argument [Build output is never a gate target](#build-output-is-never-a-gate-target) makes from the other direction: a channel nobody fails on is one people learn to skim.
|
||||
This exists because the gate's promise — that a green `npm run local:gate` means every check CI applies has already passed — was kept while a real bug walked through it. `react-hooks/exhaustive-deps` ships at `warn` in the recommended set, and two `useCallback`s in `App.tsx` omitted a non-stable `refresh` from their dependency arrays; ESLint printed the right message on both lines on every run, nothing consumed it, and the stale closure was caught only by a review round on #2076. It is the same argument [Build output is never a gate target](#build-output-is-never-a-gate-target) makes from the other direction: a channel nobody fails on is one people learn to skim.
|
||||
|
||||
Two consequences worth stating:
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Each client self-validates from its own folder; the root scripts chain them. The
|
|||
```bash
|
||||
npm run validate # fast inner loop: format:check + lint + typecheck + build + unit tests
|
||||
npm run coverage # the per-file ≥90% gate (lines/statements/functions/branches)
|
||||
npm run local:gate # MANDATORY before pushing — a strict superset of GitHub CI
|
||||
npm run local:gate # MANDATORY before pushing — every GitHub CI check, plus two local-only ones
|
||||
```
|
||||
|
||||
`npm run local:gate` chains every check below, plus the smokes and the Storybook tests. [Testing and the quality gate](./docs/quality-gate.md) owns the stage list and says what each one covers and why two are local-only; [`AGENTS.md`](./AGENTS.md) holds the testing rules themselves.
|
||||
|
|
|
|||
|
|
@ -472,16 +472,19 @@ While the Web Client provides a rich visual interface, the CLI is designed for:
|
|||
Like the other clients, the CLI self-validates from its own folder:
|
||||
|
||||
```bash
|
||||
npm run validate # format:check && lint && typecheck && test (fast; no coverage gate)
|
||||
npm run check # format:check && lint && typecheck (no tests)
|
||||
npm run validate # check && test (fast; no coverage gate)
|
||||
npm test # build test-servers + binary, then run all tests
|
||||
npm run test:coverage # build + tests under the per-file ≥90 coverage gate
|
||||
```
|
||||
|
||||
The CLI's `test` / `test:coverage` **build the binary first** (out-of-process
|
||||
`e2e.test.ts` spawns it). `validate` is `format:check && lint && typecheck && test`
|
||||
with no separate `build` step (`pretest` builds). Repo-root `validate:cli`
|
||||
delegates here; the coverage gate is `npm run coverage` / `coverage:cli` (also in
|
||||
`npm run local:gate`), matching AGENTS.md.
|
||||
`e2e.test.ts` spawns it). `check` is `format:check && lint && typecheck` and
|
||||
`validate` is `check && test`, with no separate `build` step (`pretest` builds).
|
||||
Repo-root `validate:cli` delegates to `validate`, and the root `local:validate`
|
||||
— the first stage of `npm run local:gate` — runs `check` instead, so the gate
|
||||
runs the suite once, under `coverage` / `coverage:cli` (#2341), matching
|
||||
AGENTS.md.
|
||||
|
||||
Tests run the CLI **in-process** (importing `runCli()`) so `src/` is measured
|
||||
under coverage, with a thin out-of-process spawn layer for the real binary. See
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
||||
"validate": "npm run format:check && npm run lint && npm run typecheck && npm run test",
|
||||
"check": "npm run format:check && npm run lint && npm run typecheck",
|
||||
"validate": "npm run check && npm run test",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "npm run test-servers:build && npm run build && vitest run --coverage",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ stale fixture reports a product failure rather than a staleness one.
|
|||
Like the web client, the launcher self-validates from its own folder:
|
||||
|
||||
```bash
|
||||
npm run validate # format:check && lint && build && test:coverage
|
||||
npm run check # format:check && lint && typecheck && build (no tests)
|
||||
npm run validate # check && test
|
||||
```
|
||||
|
||||
This has **no** dependency on the other clients being built — it only checks the
|
||||
|
|
@ -118,7 +119,9 @@ launcher's own source. `eslint.config.js` is a Node-only flat config (the web
|
|||
client's React/Storybook plugins stripped out), and the per-file coverage gate
|
||||
covers `parse-launcher-argv.ts` (the pure arg-parsing logic); `src/index.ts` is
|
||||
excluded as binary bootstrap and is instead exercised by the smokes above. The
|
||||
repo-root `validate:launcher` simply delegates here (`cd clients/launcher && npm run validate`).
|
||||
repo-root `validate:launcher` simply delegates here (`cd clients/launcher && npm run validate`),
|
||||
and the root `local:validate` — the first stage of `npm run local:gate` — runs
|
||||
`check` instead, so the gate runs the suite once, under `coverage:launcher` (#2341).
|
||||
|
||||
## Publishing
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"format:check": "prettier --check src __tests__ scripts \"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}\"",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"validate": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test"
|
||||
"check": "npm run format:check && npm run lint && npm run typecheck && npm run build",
|
||||
"validate": "npm run check && npm run test"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,15 @@ The TUI provides terminal-native tabs and panes for interacting with your MCP se
|
|||
Like the other clients, the TUI self-validates from its own folder:
|
||||
|
||||
```bash
|
||||
npm run validate # format:check && lint && build && test:coverage
|
||||
npm run check # format:check && lint && typecheck && build (no tests)
|
||||
npm run validate # check && test
|
||||
npm test # run all tests
|
||||
npm run test:coverage # run tests under the per-file coverage gate
|
||||
```
|
||||
|
||||
The repo-root `validate:tui` just delegates here. `eslint.config.js` registers
|
||||
The repo-root `validate:tui` just delegates here; the root `local:validate`
|
||||
(the first stage of `npm run local:gate`) runs `check` instead, so the gate
|
||||
runs the suite once, under `coverage:tui` (#2341). `eslint.config.js` registers
|
||||
`react-hooks` for the classic rules only (rules-of-hooks + exhaustive-deps); the
|
||||
stricter react-hooks@7 rules are not enforced on the interim component surface
|
||||
(#1501).
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
"dev": "vite-node --config vitest.config.ts dev.ts",
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
||||
"validate": "npm run format:check && npm run lint && npm run typecheck && npm run build && npm run test",
|
||||
"check": "npm run format:check && npm run lint && npm run typecheck && npm run build",
|
||||
"validate": "npm run check && npm run test",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
"build:client": "vite build",
|
||||
"build:runner": "tsup --config tsup.runner.config.ts",
|
||||
"build:storybook": "storybook build",
|
||||
"validate": "npm run format:check && npm run lint && npm run build && npm run test",
|
||||
"check": "npm run format:check && npm run lint && npm run build",
|
||||
"validate": "npm run check && npm run test",
|
||||
"pretest": "npm run test-servers:build",
|
||||
"test": "vitest run --project=unit",
|
||||
"test:watch": "vitest --project=unit",
|
||||
|
|
|
|||
|
|
@ -12,16 +12,18 @@ Each client self-validates from its own folder; the root scripts chain them. The
|
|||
| Tier | How it runs | What it covers |
|
||||
| --- | --- | --- |
|
||||
| **GitHub CI** (`.github/workflows/main.yml`) | Automatically, on every push | `npm install`, then `validate`, `verify:skills:cli`, `verify:build-gate`, `verify:bundle-externals`, `smoke` (which includes `smoke:web:chromium`), `test:storybook` — plus `coverage` in a parallel job ([#2159](https://github.com/modelcontextprotocol/inspector/issues/2159)) |
|
||||
| **The local gate** (`npm run local:gate`) | By hand, before you push | Every check above (the install is yours to run), **plus** the Firefox engine pass (`smoke:web:firefox`), and `smoke:tui` for real rather than self-skipped |
|
||||
| **The local gate** (`npm run local:gate`) | By hand, before you push | Every check above (the install is yours to run; `local:validate` stands in for `validate`, see below), **plus** the Firefox engine pass (`smoke:web:firefox`), and `smoke:tui` for real rather than self-skipped |
|
||||
|
||||
The local gate is a **strict superset**, not a mirror. Two of its steps have no GitHub CI counterpart, each for its own reason:
|
||||
The local gate runs **every check** CI runs, and is not a mirror. Two of its steps have no GitHub CI counterpart, each for its own reason:
|
||||
|
||||
| Local-only step | Why it is local-only |
|
||||
| --- | --- |
|
||||
| `smoke:web:firefox` | Trialled as a CI job and removed ([#2086](https://github.com/modelcontextprotocol/inspector/issues/2086)): across a dozen runs it never once disagreed with Chromium, and `playwright install --with-deps` carries a real flake surface. Kept in front of a human about to push instead. `smoke:web:webkit` is worse still — it fails two of the three smokes for reasons nobody has identified — so it is in neither tier. See [Supported browsers](#supported-browsers). |
|
||||
| `smoke:tui` | The Ink TUI needs a real TTY. It _is_ invoked in CI via `npm run smoke` and self-skips there on `process.env.CI`, so it needs no guarding — it handles itself. |
|
||||
|
||||
So the direction that matters holds: **passing `npm run local:gate` means CI's gates will pass.** The reverse does not.
|
||||
And one CI *invocation* the gate deliberately does not repeat: CI runs each client's unit suite **twice** — bare inside `validate` in the `build` job, instrumented inside `coverage` in the parallel job — at no wall-clock cost, because the two jobs run on separate runners. Serially in one process the **bare** pass was ~80s of a ~370s gate, running exactly the test files the coverage pass runs again a few minutes later ([#2341](https://github.com/modelcontextprotocol/inspector/issues/2341)). So the gate's first stage is **`local:validate`**: the same guards and `validate:core`, then each client's `check` — `format:check` + `lint` + `typecheck`, plus `build` for web, tui and launcher, which name one explicitly; cli's `validate` built only through `test`'s `pretest` hook, so dropping the `test` leg drops that build too, and `coverage:cli` builds the binary once, later — instead of its `validate` (`check` + `test`). The suites still run once, under `coverage`. That subsumes the bare pass because `@vitest/coverage-v8` reads V8's own precise-coverage profiler and rewrites nothing — the code under test is byte-identical, and the instrumented run is only slower, which is the stricter direction for the failures this repo sees. `npm run validate` and both CI jobs are unchanged.
|
||||
|
||||
So the direction that matters holds: **passing `npm run local:gate` means every check CI applies has already passed on your machine** — the strongest predictor of a green CI this repo has, though not a proof: CI runs on a different OS, and the bare pass above is the one invocation it has that the gate does not, so a test that passes *only* when instrumentation slows it down would surface there first. That test is a race ([#1596](https://github.com/modelcontextprotocol/inspector/issues/1596)) to fix, not a reason to put the second pass back. The reverse direction does not hold at all: CI green says nothing about the Firefox pass or the real `smoke:tui`.
|
||||
|
||||
**The gate is deliberately not named `ci`** ([#2146](https://github.com/modelcontextprotocol/inspector/issues/2146)). That name collided with `npm ci` — a built-in that clean-installs from the lockfile, does **not** run this script, and is one keystroke away — and it invited the cross-engine passes into GitHub CI by looking like the thing a workflow ought to call. There is **no back-compat alias**: `npm run ci` now fails with npm's missing-script error, which prints the available scripts.
|
||||
|
||||
|
|
@ -35,13 +37,14 @@ That is the readable half, and prose rots. The enforced half is `scripts/lib/wor
|
|||
| `npm run verify:build-gate` | Runs a real `vite build` with a Node built-in forced into the browser graph and asserts the build **fails** via the #1769 gate (which turns Vite's browser-externalization warning into a hard error). Guards against the warning phrasing drifting in a Vite bump and silently disabling the gate. Part of `npm run local:gate` and of GitHub CI. |
|
||||
| `npm run verify:bundle-externals` | Guards the must-not-bundle invariant (#2067): for each tsup-bundled client it reads the **built** `build/` output and fails if any package that must stay external was inlined anyway. Candidates are the union of the client's own `external` array **and the root manifest's `dependencies`** — the latter because #2067 was a *missing* `external` entry, which a self-referential check would have passed. Detection is via esbuild's `// <path>` module banners, so it covers both shapes — a separate `<pkg>-HASH.js` chunk (what a dynamically `import()`ed CommonJS package produces) and a statically-imported package folded straight into `index.js`, which emits no chunk at all. A build with no banners fails as such rather than passing clean, so enabling `minify` cannot silently retire the check. `undici` was declared only in the root and `clients/cli` manifests, and tsup auto-externalizes only what the _nearest_ manifest declares, so the web and TUI bundles inlined 1.05MB of it — and CommonJS inlined into an ESM bundle throws `Dynamic require of "assert" is not supported` on first use, from a specifier no user-side install can satisfy. Reads the output rather than the config because those two disagreed for four releases. Part of `npm run local:gate` and of GitHub CI. |
|
||||
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
|
||||
| `npm run verify:skills` | Guards `.claude/skills` ([#2163](https://github.com/modelcontextprotocol/inspector/issues/2163)). A skill's body loads only when it is invoked, so one that stops being reachable loses behavior **silently** — and the way that happens is frontmatter Claude Code cannot parse, which loads the body with an *empty* description: `/skill-name` still works, so a manual spot check passes, while nothing is left for the model to match against. This parses each `SKILL.md` the same way, requires an explicit invocation mode on every skill and committed eval cases (positives **and** negatives) on every model-invoked one, and fails when the skill listing exceeds its recorded character budget. Also runs `claude plugin validate` — the authoritative schema — when the installed CLI is **exactly** the pinned version, and otherwise says so and moves on. It runs inside `validate`, which `local:gate` executes *before* the pinned step, so accepting any other local CLI would let it reject skills that CI accepts and exit the gate before the reproducible check ever ran. Runs in `validate`; because that hand-off is best-effort, the authoritative validator also gets a guaranteed step of its own — see `verify:skills:cli` below. |
|
||||
| `npm run verify:skills` | Guards `.claude/skills` ([#2163](https://github.com/modelcontextprotocol/inspector/issues/2163)). A skill's body loads only when it is invoked, so one that stops being reachable loses behavior **silently** — and the way that happens is frontmatter Claude Code cannot parse, which loads the body with an *empty* description: `/skill-name` still works, so a manual spot check passes, while nothing is left for the model to match against. This parses each `SKILL.md` the same way, requires an explicit invocation mode on every skill and committed eval cases (positives **and** negatives) on every model-invoked one, and fails when the skill listing exceeds its recorded character budget. Also runs `claude plugin validate` — the authoritative schema — when the installed CLI is **exactly** the pinned version, and otherwise says so and moves on. It runs inside `validate:guards`, which both `validate` (CI) and `local:validate` (the gate's first stage) execute *before* the pinned step, so accepting any other local CLI would let it reject skills that CI accepts and exit the gate before the reproducible check ever ran. Runs in `validate`; because that hand-off is best-effort, the authoritative validator also gets a guaranteed step of its own — see `verify:skills:cli` below. |
|
||||
| `npm run verify:skills:cli` | Runs `claude plugin validate` — the **authoritative** skill schema — as a guaranteed step, in `local:gate` and in CI. `verify:skills` reimplements the parse and *skips* when the CLI is absent, which is right for `validate` (fast, offline, no Claude Code required) but adds up to "never runs". This one resolves the CLI instead: an installed one **only when it matches the pin exactly**, otherwise the pinned package via `npx -y @anthropic-ai/claude-code@…`. Exact, not a floor: accepting a newer local CLI would validate against a different schema than CI's, so the same `local:gate` could disagree across machines — which is the failure a pin exists to prevent. Needs no authentication. Not in `validate` — it can reach the network. |
|
||||
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs`, `scripts/lib/tsc-program.mjs` + the exported helpers of `verify-typecheck-coverage.mjs` and `verify-dep-lockstep.mjs`), one case per rule they encode, plus two suites over shared `scripts/lib` helpers that no smoke can check itself: `resolve-node-bin.test.mjs` — the cross-platform bin resolver (#1939), pinned against the real `bin`/`exports` shapes of the packages the scripts actually spawn — and `announced-child.test.mjs` — the spawn/readiness ownership helper (#2000), which drives real `node -e` children to prove a child that never announces is still published to the caller before the timeout throws, and so is reachable by teardown rather than orphaned. Four more do the same: `mcp-app-flow.test.mjs` covers the shared MCP Apps flow (#2003) — the deep link's two CSRF gates and `appArgs` encoding, plus `driveAppFlow`'s failure branches against a stand-in page, all of which are dead code from the happy-path smokes' point of view and would otherwise surface only as opaque timeouts; `render-smoke.test.mjs` and `pty.test.mjs` cover the TUI boot harness ([#2147](https://github.com/modelcontextprotocol/inspector/issues/2147)) — the former driving real `node -e` stubs to prove that a child which paints the marker and *then* dies is a **failure**, which the old harness reported as OK and which no fixed TUI can reproduce; the latter pinning the three mutually-incompatible `script(1)` invocations, where a wrong guess stops the pseudoterminal from being allocated at all; and `ensure-test-servers.test.mjs` pins the [#2111](https://github.com/modelcontextprotocol/inspector/issues/2111) invariant — that `test-servers/build` is rebuilt **even when it already exists** — which no smoke can assert about itself, since one driving a stale fixture reports a product failure rather than a staleness one. `workflow-gate.test.mjs` is a different shape again ([#2146](https://github.com/modelcontextprotocol/inspector/issues/2146)): besides the table of parser cases it runs the parser over the repo's real `.github/workflows/**`, so a workflow that invokes the local-only gate or a non-Chromium engine pass fails here rather than in a CI run nobody expected to be red. Five more arrive with the skills (#2163). `skill-manifest.test.mjs` pins the frontmatter contract one case per rule — most importantly that a fence which is not a whole line is not a terminator, and that malformed YAML is an error rather than an empty description. `verify-skills.main.test.mjs` drives that guard's `main()` over fixture directories, the sibling guards' `*.main.test.mjs` pattern, so it cannot stop enforcing while the parser tests stay green. `skill-eval.test.mjs` covers the trigger eval's stream parsing and its run classification, which an eval run's happy path never reaches and which would otherwise report a plausible hit rate for a CLI that failed to run. `verify-skills-cli.test.mjs` covers which validator gets run — a local CLI only at the exact pin — and the orchestration around it, neither of which an ordinary run on a working machine exercises. `claude-cli.test.mjs` covers the one place that decides how the CLI is spawned — the Windows `.cmd` shim needs a shell, and its arguments then need quoting — with the platform **injected**, since a win32-only bug that no runner here executes is exactly the kind that ships. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
|
||||
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
|
||||
| `npm run verify:dep-lockstep` | Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles the shared first-party TypeScript — `core/`, `test-servers/src`, and the root-owned `vitest.shared.mts`, all of which resolve their dependencies from the **root** install — alongside the client's own sources, putting the same package in one `tsc` program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod `4.3.6` vs `4.4.3` exhausted the 4GB tsc heap in `clients/web`). Derives its candidate set from **what actually enters each program** (#1965) — every client tsconfig project listed with `tsc --listFilesOnly` via the shared `scripts/lib/tsc-program.mjs`, each resolved `node_modules` file mapped to its owning install, keeping the packages that reach one program from two installs (a package whose declarations arrive only through another package's `.d.ts`, as `@modelcontextprotocol/sdk`'s do, is invisible to a scan of first-party imports). Prices each copy from the lockfile entry for the exact install path the program resolved, compares only the installs that met in one program, and **fails deny-by-default** on any disagreement not in the annotated `TOLERATED_SKEW` allowlist — empty today — with an allowlisted package tolerated only *within a major version*. A **second tier** (#2226) runs alongside it, asking the weaker but broader question the `AGENTS.md` rule actually states: does a package this repo *declares* anywhere resolve to two versions across our installs at all? Its candidate set is every name in any install's `dependencies`/`devDependencies`/`optionalDependencies` — unioned across the root and all four clients, so a copy declared by only one of them still counts — that **more than one install holds a top-level copy of** (17 packages today). Nested copies are excluded: one exists because some dependency asked for a different version, so it is that dependency's range to govern, not ours. Neither tier subsumes the other — the program tier sees a copy no manifest names (`@modelcontextprotocol/sdk`, arriving through another package's `.d.ts`), while the declared tier sees a **transitive** copy no program loads (cli's `@types/node`, hoisted via `@types/express` — the case that motivated it), two **clients** disagreeing with no root copy involved (`@types/react`, web against tui), and the peer shadows `eslint`/`typescript`/`vitest` that never enter a program. Same deny-by-default and same within-a-major rule, against its own `TOLERATED_DECLARED_SKEW` — also empty. Two limits: it reads **lockfiles**, so an uncommitted hand-installed copy is invisible, and it compares only declared names, so a purely transitive package no manifest names stays the first tier's business. Runs in `validate`.
|
||||
| `npm run verify:test-timeouts` | Guards the wall-clock budgets the test gates run under (#2323). The class it encodes against is a budget **nobody chose**: three of the six Vitest projects ran on Vitest's own `testTimeout: 5000` and five on its `hookTimeout`/`teardownTimeout: 10000`, sized for an idle machine rather than the one this team works on — three or four concurrent agent sessions in separate worktrees, each free to run a full `local:gate`, on eight logical cores. A correct, deterministic test cut off by such a budget fails a gate its diff did not break, which is the same "channel nobody trusts" failure [Lint has no warning tier](../AGENTS.md#lint-has-no-warning-tier) describes from the other direction; #2292, #1942 and #1742 were each an instance, found one site at a time. Deliberately **not** cited: #2278 (a missing condition wait around a geometry read) and #2250 (a real race in a test's own timing) were fixed by making the test wait for the right thing, and presenting a race fix as evidence for a larger ceiling would argue against #1596. It asks **Vitest itself** to resolve each of the four configs and reads the number a test actually gets, rather than checking that a key is absent from a config block — which would pass just as happily on a config that had stopped being loaded. A **seventh project** with no row, or a config file it does not discover (it knows all twelve filenames Vitest accepts, not just the two this repo uses), is an error rather than a silent skip. It also checks that every project loads `vitest.setup.shared.mts`. ⚠️ **Everything it reads comes from a resolved project, never from source text** — the two rules no config can report are asserted at runtime instead: `retry` by `vitest.setup.shared.mts` (which reads the value Vitest resolved, so a per-test option, a `describe` option, a project setting and a `--retry` flag are one check) and Testing Library's `asyncUtilTimeout` by `clients/web/src/test/asyncUtilTimeout.test.ts` (which reads what the project's own `waitFor`s use). Both began as source scanning in #2334 and the review found a new valid spelling missed in five consecutive rounds, so the boundary is deliberate: if a rule cannot be answered by asking the tool, assert it at runtime rather than reading the source for it. Observed **red** against the pre-#2323 config before it was trusted. Runs in `validate`; its decision logic has a sibling `verify-test-timeouts.test.mjs` under `test:scripts`.
|
||||
| `npm run local:gate` | **Mandatory pre-push command.** `validate` → `verify:skills:cli` → `coverage` → `verify:build-gate` → `verify:bundle-externals` → `smoke` → `smoke:web:firefox` → `local:storybook`. A strict superset of GitHub CI — see [Two tiers](#two-tiers-github-ci-and-the-local-gate). Named `local:` rather than `ci` on purpose (#2146); there is no `npm run ci` alias. |
|
||||
| `npm run local:gate` | **Mandatory pre-push command.** `local:validate` → `verify:skills:cli` → `coverage` → `verify:build-gate` → `verify:bundle-externals` → `smoke` → `smoke:web:firefox` → `local:storybook`. Every GitHub CI check plus two local-only ones — see [Two tiers](#two-tiers-github-ci-and-the-local-gate). Named `local:` rather than `ci` on purpose (#2146); there is no `npm run ci` alias. |
|
||||
| `npm run local:validate` | The gate's first stage: `validate` with each client's `test` leg removed (#2341) — the same `validate:guards` and `validate:core`, then every client's `check` — `format:check` + `lint` + `typecheck`, plus `build` for web, tui and launcher, which name one explicitly (cli's only validate-time build was `test`'s `pretest` hook, so it goes with the `test` leg and `coverage:cli` builds the binary once, later) — each client's `validate` being `check && test`. The suites run once, under `coverage`, instead of bare here and instrumented there. In the `local:` namespace so the workflow guard keeps it out of CI, where the bare pass is free. |
|
||||
| `npm run pack:verify` | Publish smoke — see [Publishing](./publishing.md). |
|
||||
|
||||
Per-client scripts exist too (`validate:web`, `coverage:cli`, `smoke:tui`, …), plus root `validate:core` / `format:core` for the shared `core/` package, `format:scripts` for the root `scripts/` tooling, and `format:shared` / `lint:shared` for the root "shared" surface (`test-servers/src/**`, `vitest.shared.mts`, the root `eslint.config.js`). Run `npm run format` before committing — the root `format` fixes `core/`, the root `scripts/`, the shared surface, and every client; `validate` runs the non-fixing `format:check` and fails CI on any unformatted file.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@
|
|||
"build:tui": "cd clients/tui && npm run build",
|
||||
"build:web": "cd clients/web && npm run build",
|
||||
"build:launcher": "cd clients/launcher && npm run build",
|
||||
"local:gate": "npm run validate && npm run verify:skills:cli && npm run coverage && npm run verify:build-gate && npm run verify:bundle-externals && npm run smoke && npm run smoke:web:firefox && npm run local:storybook",
|
||||
"local:gate": "npm run local:validate && npm run verify:skills:cli && npm run coverage && npm run verify:build-gate && npm run verify:bundle-externals && npm run smoke && npm run smoke:web:firefox && npm run local:storybook",
|
||||
"local:validate": "npm run validate:guards && npm run validate:core && cd clients/web && npm run check && cd ../cli && npm run check && cd ../tui && npm run check && cd ../launcher && npm run check",
|
||||
"local:storybook": "cd clients/web && npx playwright install chromium && npm run test:storybook",
|
||||
"verify:build-gate": "node scripts/verify-build-gate.mjs",
|
||||
"verify:bundle-externals": "node scripts/verify-bundle-externals.mjs",
|
||||
|
|
@ -46,7 +47,8 @@
|
|||
"verify:skills": "node scripts/verify-skills.mjs",
|
||||
"verify:skills:cli": "node scripts/verify-skills-cli.mjs",
|
||||
"test:scripts": "node --test \"scripts/**/*.test.mjs\"",
|
||||
"validate": "npm run verify:format-coverage && npm run verify:skills && npm run verify:typecheck-coverage && npm run verify:dep-lockstep && npm run verify:test-timeouts && npm run test:scripts && npm run validate:core && npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher",
|
||||
"validate": "npm run validate:guards && npm run validate:core && npm run validate:web && npm run validate:cli && npm run validate:tui && npm run validate:launcher",
|
||||
"validate:guards": "npm run verify:format-coverage && npm run verify:skills && npm run verify:typecheck-coverage && npm run verify:dep-lockstep && npm run verify:test-timeouts && npm run test:scripts",
|
||||
"verify:format-coverage": "node scripts/verify-format-coverage.mjs",
|
||||
"verify:dep-lockstep": "node scripts/verify-dep-lockstep.mjs",
|
||||
"verify:test-timeouts": "node scripts/verify-test-timeouts.mjs",
|
||||
|
|
|
|||
|
|
@ -25,14 +25,17 @@ export function tokenize(command) {
|
|||
* <name>` references within a single manifest's `scripts`, plus npm's implicit
|
||||
* `pre<name>`/`post<name>` lifecycle hooks (npm runs those around `<name>`
|
||||
* without an explicit `npm run`, so a gate moved into e.g. `prevalidate` is
|
||||
* still reached). A gate harvested from a script that nothing reachable from
|
||||
* still reached). Every spelling npm accepts for `run` is followed —
|
||||
* `run-script` is the canonical command and `run`, `rum` and `urn` are its
|
||||
* aliases — so a chain written as `npm run-script validate:web` reaches the
|
||||
* same scripts as `npm run validate:web` (Copilot, #2341). A gate harvested from a script that nothing reachable from
|
||||
* `entry` invokes gates nothing, so callers restrict to this set to assert "CI
|
||||
* actually runs this", not merely "the script exists".
|
||||
*/
|
||||
export function reachableScripts(scripts, entry = "validate") {
|
||||
const reached = new Set();
|
||||
const queue = [entry];
|
||||
const runRef = /npm run ([\w:-]+)/g;
|
||||
const runRef = /npm (?:run-script|run|rum|urn) ([\w:-]+)/g;
|
||||
while (queue.length > 0) {
|
||||
const name = queue.shift();
|
||||
if (reached.has(name)) continue;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,25 @@ test("reachableScripts: follows `npm run` refs and pre/post hooks (r10)", () =>
|
|||
assert.ok(!reached.has("unrelated"));
|
||||
});
|
||||
|
||||
test("reachableScripts: follows every npm spelling of `run` (#2341)", () => {
|
||||
// `run-script` is the canonical command; `run`, `rum` and `urn` are its
|
||||
// aliases. A chain written in any of them reaches the same scripts, so a
|
||||
// guard that only followed `npm run` would let `npm run-script validate:web`
|
||||
// reintroduce what it exists to forbid (Copilot).
|
||||
const scripts = {
|
||||
gate: "npm run-script a && npm rum b && npm urn c && npm run d",
|
||||
a: "noop",
|
||||
b: "noop",
|
||||
c: "noop",
|
||||
d: "noop",
|
||||
e: "noop",
|
||||
};
|
||||
const reached = reachableScripts(scripts, "gate");
|
||||
for (const name of ["a", "b", "c", "d"])
|
||||
assert.ok(reached.has(name), `npm spelling reaching ${name}`);
|
||||
assert.ok(!reached.has("e"));
|
||||
});
|
||||
|
||||
test("reachableScripts: a `prevalidate`-hosted typecheck is reachable (r10)", () => {
|
||||
const scripts = {
|
||||
validate: "npm run build && npm run test",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@
|
|||
* two verify gates, `smoke` and the Storybook tests. `npm run smoke` covers
|
||||
* `smoke:launcher`, `smoke:cli`, `smoke:tui`, `smoke:web` and
|
||||
* `smoke:web:chromium` — all of which BELONG there.
|
||||
* - **`npm run local:gate`**, the pre-push gate, is a strict superset: it adds
|
||||
* the **Firefox** engine pass, and `smoke:tui` really runs there rather than
|
||||
* self-skipping. WebKit is on demand and belongs to neither tier.
|
||||
* - **`npm run local:gate`**, the pre-push gate, runs every check CI runs and
|
||||
* adds the **Firefox** engine pass, and `smoke:tui` really runs there rather
|
||||
* than self-skipping. WebKit is on demand and belongs to neither tier. It
|
||||
* runs each client's unit suite once (instrumented, under `coverage`) via
|
||||
* `local:validate`, where CI's two parallel jobs run it twice (#2341).
|
||||
*
|
||||
* The narrow thing that must never reach CI is a **non-Chromium engine pass**.
|
||||
* `smoke:web:firefox` was placed in the local gate on purpose (#2086,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
findWorkflowViolations,
|
||||
formatWorkflowViolations,
|
||||
} from "./workflow-gate.mjs";
|
||||
import { reachableScripts } from "./npm-scripts.mjs";
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "..", "..");
|
||||
const workflowDir = join(repoRoot, ".github", "workflows");
|
||||
|
|
@ -609,6 +610,119 @@ describe("the gate's name", () => {
|
|||
assert.match(scripts["local:gate"], /local:storybook/);
|
||||
});
|
||||
|
||||
describe("runs each client's test suite once, not twice (#2341)", () => {
|
||||
// CI runs each client's unit suite bare (inside `validate`) and again
|
||||
// instrumented (inside `coverage`) on two PARALLEL runners, so the second
|
||||
// pass is free there. The gate runs serially, where the bare pass was ~80s
|
||||
// of a ~370s run re-executing exactly the files `coverage` runs a few
|
||||
// minutes later. So the gate's first stage is `local:validate` — `validate`
|
||||
// minus every client's `test` leg — and these pin the three properties
|
||||
// that keep it honest: the gate no longer reaches a client's bare `test`,
|
||||
// it still reaches every non-test check `validate` reaches, and `validate`
|
||||
// itself (CI's inner loop) is untouched.
|
||||
const clients = ["web", "cli", "tui", "launcher"];
|
||||
const clientScripts = Object.fromEntries(
|
||||
clients.map((c) => [
|
||||
c,
|
||||
JSON.parse(
|
||||
readFileSync(join(repoRoot, "clients", c, "package.json"), "utf8"),
|
||||
).scripts,
|
||||
]),
|
||||
);
|
||||
|
||||
it("starts from `local:validate`, and never from `validate`", () => {
|
||||
assert.match(scripts["local:gate"], /^npm run local:validate && /);
|
||||
assert.doesNotMatch(scripts["local:gate"], /\brun validate(?=$|[\s&;])/);
|
||||
// Nothing reachable from the WHOLE gate — not just its first stage —
|
||||
// may run `validate` or a client's `validate:*`: those are the doors
|
||||
// through which the bare `test` leg would come back, and a later stage
|
||||
// could open one just as easily as the first (Copilot).
|
||||
const reached = reachableScripts(scripts, "local:gate");
|
||||
assert.ok(!reached.has("validate"), "local:gate must not reach validate");
|
||||
for (const c of clients)
|
||||
assert.ok(
|
||||
!reached.has(`validate:${c}`),
|
||||
`local:gate must not reach validate:${c}`,
|
||||
);
|
||||
// Each client is visited AND its `check` is the very next command —
|
||||
// matched together, so dropping one client's `npm run check` while the
|
||||
// `cd` and another client's `check` remain cannot stay green (Copilot).
|
||||
for (const c of clients)
|
||||
assert.match(
|
||||
scripts["local:validate"],
|
||||
new RegExp(
|
||||
`(?:cd (?:\\.\\./|clients/)${c} && npm run check|--prefix clients/${c} run check)(?=$|[\\s&;])`,
|
||||
),
|
||||
`local:validate must run \`check\` in clients/${c}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("shares the guards and `validate:core` with `validate`", () => {
|
||||
// One list of durable guards, reached by both entry points — so a guard
|
||||
// added to `validate:guards` lands in CI and in the gate together.
|
||||
const gate = reachableScripts(scripts, "local:validate");
|
||||
const inner = reachableScripts(scripts, "validate");
|
||||
for (const name of ["validate:guards", "validate:core"]) {
|
||||
assert.ok(gate.has(name), `local:validate must reach ${name}`);
|
||||
assert.ok(inner.has(name), `validate must reach ${name}`);
|
||||
}
|
||||
// Everything `validate` reaches at the root that is not a client
|
||||
// delegation is reached by the gate too.
|
||||
for (const name of inner)
|
||||
if (
|
||||
name !== "validate" &&
|
||||
!/^validate:(web|cli|tui|launcher)$/.test(name)
|
||||
)
|
||||
assert.ok(gate.has(name), `local:validate must reach ${name}`);
|
||||
});
|
||||
|
||||
it("each client's `validate` is `check` plus `test`, and `check` runs no tests", () => {
|
||||
for (const c of clients) {
|
||||
const s = clientScripts[c];
|
||||
assert.equal(
|
||||
s.validate,
|
||||
"npm run check && npm run test",
|
||||
`clients/${c} validate`,
|
||||
);
|
||||
const fromCheck = reachableScripts(s, "check");
|
||||
for (const name of fromCheck)
|
||||
assert.doesNotMatch(
|
||||
name,
|
||||
/^(pre|post)?test(:|$)/,
|
||||
`clients/${c} check must not reach ${name}`,
|
||||
);
|
||||
// `reachableScripts` follows `npm run <name>` only, so also reject the
|
||||
// spellings that reach the suite without one: a direct `vitest`, and
|
||||
// npm's built-in `npm test` / `npm t` / `npm tst` aliases (Copilot).
|
||||
const checkCommands = [...fromCheck].map((n) => s[n]).join(" ");
|
||||
assert.doesNotMatch(
|
||||
checkCommands,
|
||||
/\bvitest\b/,
|
||||
`clients/${c} check must not invoke vitest`,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
checkCommands,
|
||||
/\bnpm (?:test|t|tst)(?=$|[\s&;])/,
|
||||
`clients/${c} check must not invoke \`npm test\``,
|
||||
);
|
||||
// And `check` is the whole of `validate` minus the test leg: what
|
||||
// `validate` reaches is exactly itself, plus what `check` reaches, plus
|
||||
// what `test` reaches (its `pretest` build preambles included).
|
||||
assert.deepEqual(
|
||||
[...reachableScripts(s, "validate")].sort(),
|
||||
[
|
||||
...new Set([
|
||||
"validate",
|
||||
...fromCheck,
|
||||
...reachableScripts(s, "test"),
|
||||
]),
|
||||
].sort(),
|
||||
`clients/${c} validate must reach exactly check + test`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("has no `ci` alias, not even a deprecated one", () => {
|
||||
// The rename is only worth anything if the old name is GONE (#2146): an
|
||||
// alias preserves exactly the association being scrubbed, and leaves
|
||||
|
|
|
|||
|
|
@ -350,8 +350,9 @@ if (!output.includes(ERROR_PREFIX)) {
|
|||
// so PROBE_MODULE must appear. Without this, a repo that already leaks a built-in
|
||||
// would report OK even if the probe was tree-shaken or entryPath went stale —
|
||||
// branch (c)'s failure, silently inverted into a pass. (Unreachable inside
|
||||
// `npm run local:gate`: `validate`'s `build:web` fails first on a pre-existing leak — but
|
||||
// this script is a documented standalone command, run exactly when debugging one.)
|
||||
// `npm run local:gate`: web's `check` — its `build`, run by `local:validate` —
|
||||
// fails first on a pre-existing leak; but this script is a documented
|
||||
// standalone command, run exactly when debugging one.)
|
||||
if (!output.includes(PROBE_MODULE)) {
|
||||
fail(
|
||||
`the #1769 gate fired, but ${PROBE_MODULE} isn't among the offenders — it ` +
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ function main(argv = process.argv.slice(2)) {
|
|||
// The authoritative parse, when it is available — strictly a bonus, and only
|
||||
// from the EXACT pinned CLI.
|
||||
//
|
||||
// `local:gate` runs `validate` (and therefore this) before the pinned
|
||||
// `local:gate` runs `local:validate` (and therefore this) before the pinned
|
||||
// `verify:skills:cli` step, so a local CLI on any other version could reject
|
||||
// skills that CI's pinned validator accepts — and the gate would exit here,
|
||||
// never reaching the reproducible step (Copilot). Accepting merely "new enough"
|
||||
|
|
|
|||
Loading…
Reference in a new issue