Skip to content

Commit c667d8c

Browse files
os-warrenclaude
andauthored
feat(qa): one shared vitest filter preflight for all eight project-declaring packages (#18121)
Fixes #17978 One shared vitest filter preflight, serving all eight packages that declare vitest `projects`. Direction A was ruled by the `domain:cli` execution seat under 验证策略 (`5656963807`) and confirmed by triage (`5657101795`), which also ruled the ownership question: > **引用或点名一个包不会把卡拉进它的车道,只有编辑它才会。** ## What changed - **New private workspace package `packages/qa/vitest-filter-preflight`** — a `domain:cli`-owned home, ⛔ **not** root `scripts/` and ⛔ not inside `packages/spec`. It holds the ONE transcription of vitest's `TestProject.filterFiles`; `packages/qa/refd-timer-testkit` is the shape precedent (private, no build, `exports` straight at `src`). - **`packages/cli` migrates onto it** and its local copy is deleted. Triage calls the move "a migration inside the owning lane". Measured behaviour-equivalent (below). - **Call sites in all eight configs.** `packages/spec`, `packages/objectql` and `packages/core` are **call sites only** — one file each, `vitest.config.ts`, 32 insertions and 0 deletions, no new export and nothing under `src/`. - **Two findings this card carried are folded in**: the notice's package name is now a parameter (it was hardcoded `@objectstack/cli`), and the no-`--project` typo case is covered and pinned. ## Three measurements that shaped the design **1. The precedent's shape does not transfer, re-confirmed at `a26a114d7`.** `Populations` takes concrete paths, so a glob cannot be a member. 82 package roots under `packages/`, **8** declaring `projects` — the same eight, no drift. `packages/cli` is 1 of 8 with two exact-path projects, and only as a by-product of a tier walk it already had (#13504 / #14554). The other 7 each pair one explicit list `L` with a glob complement of `L`, so the missing component is identical across all seven. `exactAndGlobPopulations` is that component. **2. The glob project's population is a deliberate SUPERSET** (the ruled shape, and it holds): a plain recursive walk of the `*.{test,spec}.?(c|m)[jt]s?(x)` family minus `node_modules`/`dist`, minus `L`. Matching nothing in a superset implies matching nothing in the real set, so a false accusation is structurally impossible and drift can only under-report. ⛔ Not vitest's own glob engine. Cost of the walk, measured: 0.3 ms (`core`) to 5.3 ms (`spec`, 507 test files) per config load. **3. ⛔ Consumers import the module by RELATIVE PATH, never by its bare package name — and that is a measurement, not a preference.** Vite bundles a config's relative imports through esbuild, which transpiles TypeScript; it externalises bare specifiers and leaves Node to load the resolved path, which here is a `.ts` file. The bare form works on this box and prints no warning — because Node 22.22 strips types by default. Re-run with `NODE_OPTIONS=--no-experimental-strip-types`: ```text failed to load config from .../packages/core/vitest.probe-bare.config.ts TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for .../src/index.ts ``` This repo declares `engines.node: ">=22.0.0"`, so on a supported Node the bare form turns a silent-drop defect into a total harness outage for that package. The two escapes are worse: building to `dist` would make eight harnesses' **config load** depend on build state, and `pnpm --filter PKG exec vitest run FILE` — the invocation this card is about — runs no build; authoring it as `.mjs` would drop the types, and `CliParseResultOptions` is typed structurally precisely so a vitest upgrade renaming an option is a type error here instead of a silent decline. ## Per-package before/after — the preflight now speaks where it was silent `before` = `origin/main`'s config, run verbatim via `--config vitest.before.config.ts` beside the real one so `__dirname` still resolves to the package root. Every run exits **0** in both legs; the defect is silence, not failure. | package | glob side: dropped path named | exact side: dropped path named | no-`--project` typo named | healthy control | |:--|:--|:--|:--|:--| | `packages/types` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/rest` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical¹ | | `packages/runtime` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/spec` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/objectql` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/core` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical | | `packages/qa/dogfood` | 0 → 4 | 0 → 4 | 0 → 2 | byte-identical¹ | ¹ the only diff line is the `Duration` decimal. "`FILTER SELECTED NOTHING` present" reads **0 before, 2 after** in all three shapes in all seven (twice: once at config load, once from the `exit` listener). ⭐ This extends the card's reproduction from the 4 packages the first dispatch probed to **7 of 7**, including `spec`, `objectql` and `core`, which had never been probed — still **zero negative probes**, vitest **4.1.11**. `packages/qa/dogfood` also confirms the shape outside the `REPO_TESTS` idiom: its exact list is the inline `SHARED_SHOWCASE` and its glob project is `isolated`. **`packages/cli` — migration equivalence, not before/after.** Its `before` already carried a preflight, so the reading asked whether the move changed anything a reader sees. Same notice count (2), same dropped-path count (4), same `pnpm --filter @objectstack/cli` in the notice (4), healthy control byte-identical. The lost-run output differs in exactly **two words, twice**: `tier` → `project`. Deliberate — "tier" is this package's private vocabulary while "project" is vitest's own and is correct for all eight. A healthy narrowed run still contributes **zero bytes**: `renderLostFilterNotice` returns the empty string and no writer is called and no `exit` listener is registered. ## Tests, and the ablation that proves they can fail `pnpm --filter @objectstack/vitest-filter-preflight test` — **2 files, 73 tests, pass**. `typecheck` green, and `tsc --listFiles` puts **4 of 4** of the package's files in the program (`src/index.ts`, both tests, `vitest.config.ts`) — nothing hidden. `test/config-wiring-sweep.test.ts` is the anti-phantom sweep, and its population is **derived, not listed**: it walks `packages/` for package-root vitest configs, masks their comments, and requires every one that declares `projects` to invoke the preflight — so a ninth package is caught on the PR that adds it. The count is asserted as a floor, so a newcomer fails on its own wiring rather than on a number. **Ablation** (fix committed first; mutation proven on disk by occurrence count; `trap`-restored with absolute paths; restore proven by blob hash, not by an exit code): | leg | on-disk proof | sweep | |:--|:--|:--| | delete the `runFilterPreflight` call from `packages/types/vitest.config.ts` | `runFilterPreflight({` count 1 → 0; blob `a5d708a1` → `ef829c5c` | **exit 1**, `1 failed \| 40 passed`, fails on that package's row | | point `packageName` at another package | own-name count 1 → 0, wrong-name 1; blob → `aba7fc56` | **exit 1**, same row | | restore | — | blob back to `a5d708a1` (= `HEAD`), `git diff HEAD` empty | Predicted direction was "turns red" and that is what both legs did. The second leg is the pin for the carried finding: a shared notice bound to one package sends every other package's reader to a command that runs the wrong suite. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` against the actual change set, every command run, exit codes recorded to disk, then reconciled: ```text Run reconciliation — 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED (a DERIVED zero — all 91 recorded an exit code and none of them is 3). ``` Plus this lane's standing full union, which the derivation does not name: **`pnpm lint` exit 0** at `cd75179f35` (`eslint . --no-inline-config`, the whole repo — not a narrowing). Two gates needed work rather than just passing: - **`check:cross-package-test-inputs` went red and is satisfied, not routed around.** The sweep reads outside its package by construction. It now declares its radius in `scripts/cross-package-test-inputs.mjs` — three globs under `packages/` plus three `scripts/` files, with `heldBy` witnesses for the two globs built from a loop variable that the detector can see but cannot name — and `turbo.json` carries the matching `@objectstack/vitest-filter-preflight#test` inputs with `!**/node_modules/**`. ⭐ Side effect worth having: the suite's turbo hash now really covers every package's config, so a config change re-runs the sweep. - **`check:type-check-debt` first exited 3 (OOM at `--max-old-space-size=4096` on a shared box) — recorded as NOT MEASURED, not as a pass**, then re-run at 8192 and green: 77/81 packages type-checked, 55 raw errors, none above its recorded number, `surplus: none`. `check:type-check-coverage` green with the new package counted (81 packages, +1 vs the record). `check-engine-split-ratio --days 90` also exited 2 first — the gate refusing to measure on a shallow clone — and is green after `git fetch --shallow-since`, at 97.8%. Neither of those two numbers is about this diff; both are recorded so the zero above is real. No repo-level sweeper was added, so `check:pm-dispatch-gates` does not fire: the anti-phantom sweep is a vitest test inside the cli-owned home, which is also what keeps a gate-class file out of root `scripts/`. ## `skip-changeset` — measured, not assumed Nothing published moves. The new package is `private: true`. For each of the seven touched published packages, every `files[]` path was grepped for the subject symbols (`runFilterPreflight`, `exactAndGlobPopulations`, `matchesVitestFilter`, `testFilesUnder`): **zero hits in all seven**, with a positive control proving the grep works (`ObjectLogger` in `packages/core/dist`, `defineStack` in `packages/spec/dist`). `packages/cli`'s `files[]` is `["dist","README.md","CHANGELOG.md"]` and the moved module lived at the package root, outside all three. `packages/spec`'s `files[]` includes `src/**/*.zod.ts`; this diff touches no `.zod.ts`. `pnpm-lock.yaml` moved by **12 insertions and 0 deletions** — exactly one new `importers:` block for the new package, no version change to anything installed — and `pnpm install --frozen-lockfile` exits 0, which is the proof it is tool-written rather than hand-edited. ## Acceptance notes - **The seven new call sites are in no tsc program.** Only `packages/cli` type-checks its own `vitest.config.ts` (via `tsconfig.test.json`); the other seven declare `include: ["src/**/*"]` and their `tsconfig.test.json` siblings declare `["src*"]`, so no package-root config there is compiled by anything. Pre-existing — `packages/cli/tsconfig.test.json`'s header records the same state for cli before #14554 graduated it — and not widened by this change, but it does mean a `@ts-expect-error` in any of those seven files would be a phantom check. Covered here by measurement instead: the wiring sweep asserts the call shape textually, and every one of the seven configs was loaded and run in both legs above. Noted, not filed — no declared contract requires a config to be type-checked, and the carrier is the `check:type-check-coverage` ratchet's own graduation work. - The wiring sweep is scoped to `packages/`. A package-root config outside that tree growing `projects` would not be swept — an under-report, the direction this card resolves uncertainty in. Scoping is what keeps the declared radius to one already-open root instead of opening `examples/` and `apps/` roots in `ci.yml`'s `crosspkg` filter. ## Fences Module home is `domain:cli`-owned, so the stop-clause did not trigger: nothing landed in root `scripts/` or inside `packages/spec`. No gate weakened. `packages/rest`'s `OS_REST_LOG: 'silent'` from `a26a114d7` (#18090) is untouched in all three blocks. No file on #17630's surface was touched — this diff edits no test file in `packages/qa/dogfood`. No governed surface is in the PR diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01TbSMtGzMrtPwh925wDEZd5)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3b41e8 commit c667d8c

19 files changed

Lines changed: 1166 additions & 341 deletions

‎packages/cli/test/vitest-project-filter-preflight.test.ts‎

Lines changed: 0 additions & 277 deletions
This file was deleted.

‎packages/cli/tsconfig.test.json‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,15 @@
174174
// ⛔ Every package-root harness module is named here ONE BY ONE, because the
175175
// `include` above reaches `test/` and nothing else: a root module that is only
176176
// reachable through a test's import is in the program by accident and leaves
177-
// it the moment that import goes. `vitest-filter-preflight.ts` (#17853) is the
178-
// fourth for that reason, not because a test happens to import it.
177+
// it the moment that import goes. `vitest-filter-preflight.ts` was the fourth
178+
// for that reason; #17978 moved that module to
179+
// `packages/qa/vitest-filter-preflight`, which type-checks it in its own
180+
// program, so the entry leaves with the file rather than dangling here.
179181
"include": [
180182
"test/**/*",
181183
"vitest.config.ts",
182184
"vitest-tiers.ts",
183-
"vitest-tiers.fixtures.ts",
184-
"vitest-filter-preflight.ts"
185+
"vitest-tiers.fixtures.ts"
185186
],
186187
"exclude": ["node_modules", "dist"]
187188
}

‎packages/cli/vitest.config.ts‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@
644644
import { defineConfig } from 'vitest/config';
645645
import path from 'path';
646646
import { parseCLI } from 'vitest/node';
647-
import { runFilterPreflight } from './vitest-filter-preflight.js';
647+
import { runFilterPreflight } from '../qa/vitest-filter-preflight/src/index.js';
648648
import { integrationTestFiles, unitTestFiles } from './vitest-tiers.js';
649649

650650
// The two tiers, DERIVED from what the files DO — never written down — over
@@ -656,18 +656,29 @@ import { integrationTestFiles, unitTestFiles } from './vitest-tiers.js';
656656
export const INTEGRATION_FILES = integrationTestFiles(__dirname);
657657
export const UNIT_FILES = unitTestFiles(__dirname, INTEGRATION_FILES);
658658

659-
// #17853 — say so when a path named on the command line will run no tests. It
660-
// is invoked HERE, at config load, and ⛔ deliberately NOT as a `test.reporters`
661-
// entry: naming that option replaces vitest's own reporter defaulting instead
662-
// of extending it, which measurably changes a healthy run's output and would
663-
// drop the `github-actions` reporter in CI. `vitest-filter-preflight.ts` carries
664-
// both measurements. It reads the argv through vitest's own exported parser and
665-
// the SAME two derived arrays the projects below take as their `include` — ⛔
666-
// never a second derivation — and writes nothing whatever unless a named path
667-
// selects nothing.
659+
// #17853 / #17978 — say so when a path named on the command line will run no
660+
// tests. It is invoked HERE, at config load, and ⛔ deliberately NOT as a
661+
// `test.reporters` entry: naming that option replaces vitest's own reporter
662+
// defaulting instead of extending it, which measurably changes a healthy run's
663+
// output and would drop the `github-actions` reporter in CI. The ONE shared
664+
// transcription of vitest's `TestProject.filterFiles` —
665+
// `packages/qa/vitest-filter-preflight` — carries both measurements, and it is
666+
// imported by RELATIVE PATH rather than by its package name for a third measured
667+
// reason recorded in its header. It reads the argv through vitest's own exported
668+
// parser and the SAME two derived arrays the projects below take as their
669+
// `include` — ⛔ never a second derivation — and writes nothing whatever unless a
670+
// named path selects nothing.
671+
//
672+
// ⭐ This package is the ONE of the eight that needs no walked population: both
673+
// of its projects take an exact-path `include`, as a by-product of the tier walk
674+
// it already performs for unrelated reasons (#13504 / #14554). So it hands the
675+
// two arrays over directly and never calls `exactAndGlobPopulations`. That
676+
// asymmetry is exactly why a port of this package's former local copy could not
677+
// serve the other seven — #17978 carries the measurement.
668678
runFilterPreflight({
669679
argv: process.argv,
670680
root: __dirname,
681+
packageName: '@objectstack/cli',
671682
populations: { unit: UNIT_FILES, integration: INTEGRATION_FILES },
672683
parse: parseCLI,
673684
});

‎packages/core/vitest.config.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import { configDefaults, defineConfig } from 'vitest/config';
44
import { readFileSync } from 'node:fs';
55
import path from 'path';
6+
import { parseCLI } from 'vitest/node';
7+
import {
8+
exactAndGlobPopulations,
9+
runFilterPreflight,
10+
} from '../qa/vitest-filter-preflight/src/index.js';
611

712
// #16466 -- two vitest projects, two turbo tasks. `repo` owns the tests that read
813
// outside this package (the list is vitest.repo-tests.json, which
@@ -12,6 +17,33 @@ import path from 'path';
1217
// repo. `extends: true` keeps the root options (aliases included) on both.
1318
const REPO_TESTS: string[] = JSON.parse(readFileSync(path.join(__dirname, 'vitest.repo-tests.json'), 'utf8'));
1419

20+
// #17853 / #17978 — say so when a path named on the command line will run no
21+
// tests. Invoked HERE, at config load, and ⛔ deliberately NOT as a
22+
// `test.reporters` entry: naming that option replaces vitest's own reporter
23+
// defaulting instead of extending it, which measurably changes a healthy run's
24+
// output and would drop the `github-actions` reporter in CI. The ONE shared
25+
// transcription of vitest's `TestProject.filterFiles` carries both measurements,
26+
// and it is imported by RELATIVE PATH rather than by its package name for a
27+
// third measured reason recorded in its header. It reads the argv through
28+
// vitest's own exported parser and writes nothing whatever unless a named path
29+
// selects nothing.
30+
//
31+
// `local` takes a GLOB `include`, so its population is derived as a deliberate
32+
// SUPERSET — every test file under this package, minus `REPO_TESTS` — which
33+
// makes a false accusation structurally impossible and leaves drift able only
34+
// to under-report. ⛔ Not a second run of vitest's own glob engine.
35+
runFilterPreflight({
36+
argv: process.argv,
37+
root: __dirname,
38+
packageName: '@objectstack/core',
39+
populations: exactAndGlobPopulations({
40+
root: __dirname,
41+
exact: { repo: REPO_TESTS },
42+
globProject: 'local',
43+
}),
44+
parse: parseCLI,
45+
});
46+
1547
export default defineConfig({
1648
test: {
1749
// Each project re-declares the root block's test options: a ROOT-level

0 commit comments

Comments
 (0)