Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/cli-test-typecheck-wiring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
"@objectstack/cli": patch
---

fix(cli): wire `packages/cli`'s test layer into `check:test-typecheck`, so its 115 test files are type-checked at all (#14710)

`packages/cli/tsconfig.json` declares `include: ["src"]` and no `exclude` at
all, and the package's `typecheck` script was a bare `tsc --noEmit` against that
very config — so the 115 test files in the sibling `test/` tree were read by no
tsc program anywhere. This is the shape AGENTS.md already forbids, reached by
the OTHER spelling: not an `exclude` naming the test globs, but an `include`
that never reaches them.

Measured at `5a5336b399` with the workspace closure built first, rather than
read off the config: `tsc --noEmit --listFiles -p tsconfig.json` puts 1009 files
in the program and **0** of the 115 among them, while 119 of 119 non-test
`src/**` files and all 121 `src/**` test files ARE there — so the zero is the
`include` line, not a probe that sees nothing. The directional control is
`packages/drivers/driver-memory`, whose tsconfig carries no test exclusion: the
same probe puts 40 of its 40 test files in the program. Under the new
`tsconfig.test.json` the count is **115 of 115**, plus the three package-root
harness modules (`vitest.config.ts`, `vitest-tiers.ts`,
`vitest-tiers.fixtures.ts`) and `test/helpers/serve-process.ts`.

Onboarded by *wiring* to the shared mechanism (`scripts/check-test-typecheck.mts`)
the way `objectql`, `rest`, `lint`, the fourteen `packages/plugins/**` and
`runtime` are wired, never by copying it: a sibling `tsconfig.test.json`
matching vitest's real module semantics (`module: esnext`,
`moduleResolution: bundler`), named by `typecheck` via
`check:test-typecheck --project`. Strictness is untouched and inherited; not one
`any` and not one `@ts-expect-error` was added to any test file to open the
gate. `rootDir` IS widened to `../..`, the way `packages/client`'s test config
already does it — this package's tests sit outside the build config's
`rootDir: "src"`, and three of them import fixtures from
`examples/app-showcase/src/**`.

**Seeded, not repaired, per this card's triage ruling.** The layer reports 28
errors across 3 files and they are recorded EXACT and shrink-only in the new
`test-typecheck-debt.json`. Every one is pre-existing: no test file is edited
here. The other 112 files carry no entry, so any error they gain is red on
arrival.

This is a CONVERSION rather than a new debt-opening decision. The same
population under the build config's inherited NodeNext reports 144 — exactly the
number `scripts/check-type-check-coverage.mjs` already held for this package in
its per-PACKAGE `TEST_DEBT` ledger, class for class — and that entry graduates
here, as the pairing forces. The 144 → 28 step is attributed in both directions
with no remainder: −120 config-tier diagnostics that dissolve under vitest's
module semantics (TS2835 ×56, the TS7006 ×59 cascading above them, TS2307 ×3,
TS18046 ×2) and +4 that collapsing the cascade exposed (TS18048 ×4 in
`test/i18n-extract-action-description.test.ts`, previously masked by an `any`
from two unresolved imports). The 24 TS2339 survive unchanged, file for file.
4 changes: 3 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"build": "if [ -n \"$OS_SKIP_DTS\" ]; then tsc -p tsconfig.build.json --noCheck --declaration false --declarationMap false; else tsc -p tsconfig.build.json; fi",
"dev": "tsc -p tsconfig.build.json --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/cli --project tsconfig.test.json",
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/cli --project tsconfig.test.json",
"check:app-nav-i18n": "node scripts/check-app-nav-i18n.mjs --self-test && node scripts/check-app-nav-i18n.mjs"
},
"keywords": [
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/test-typecheck-debt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"_comment": "Per-file tsc error debt of the @objectstack/cli TEST layer (#5286). `tsconfig.test.json` compiles `src/**/*.test.ts` — which `tsconfig.json` excludes and therefore no gate ever read — and every file below still carries errors from before that gate existed. THIS FIELD IS GENERATED: every regeneration rewrites it from scripts/check-test-typecheck.mts, and the EXACT ratchet below requires a regeneration on every repair — so an edit made here is gone by the next one. Anything true of THIS package goes in the sibling `_note` field, which is authored, is preserved verbatim, and is never written by the generator (#12624). This comment states NO cause for the errors, deliberately: the classes differ per package and per file, they move as the debt is paid down, and a cause written here is rewritten verbatim into every ledger by every regeneration — so it outlives its own repair and cannot be corrected in the file where it is read. Measure instead, before repairing anything: `tsc --noEmit --pretty false -p tsconfig.test.json` in the package prints the real classes with their TS codes. Each entry maps a file to its per-SIGNATURE error counts, never to a bare total (#13470): a signature is the TS code plus the diagnostic message with structural type blobs collapsed, and it carries NO line or column — so the pin survives edits that move code around, and only stops matching when the error itself becomes a different error. EXACT ratchet, judged by re-running tsc: a file that gains errors is red, a file that loses them is red until its number is re-recorded, a file that reaches zero is red until its entry is deleted, a signature that ARRIVES or VANISHES is red even when the file total is unchanged, and a file NOT listed here may have no errors at all. Regenerate with: pnpm --filter @objectstack/cli gen:test-typecheck-debt",
"_note": "SEEDED, NOT REPAIRED, and that was this card's own triage ruling rather than this author's convenience: 28 errors across 3 of this package's 115 test files, measured at 5a5336b399 with the workspace closure built first, on the PR that first put those files in front of tsc. Repairing them in the same change would have buried the gate under a cleanup, which is how an onboarding stops landing; the ratchet only has to shrink from here. THIS PACKAGE REACHED THE HIDDEN STATE BY THE OTHER SPELLING, which is the part worth carrying to the next package: `tsconfig.json` here has no `exclude` at all. It declares `include: [\"src\"]`, and these 115 files live in a sibling `test/` tree the glob simply never reaches — the identical end state AGENTS.md forbids by naming the `exclude` spelling, arriving through a door that rule's text does not cover. Measured rather than read off the config: `tsc --noEmit --listFiles -p tsconfig.json` puts 1009 files in the program and 0 of the 115 among them, while 119 of 119 non-test `src/**` files and all 121 `src/**` test files ARE there — so the zero is the `include` line, not a probe that sees nothing. The directional control is `packages/drivers/driver-memory`, whose tsconfig carries no test exclusion: the same probe puts 40 of its 40 test files in the program. Under `tsconfig.test.json` the count is 115 of 115, plus the 3 package-root harness modules (`vitest.config.ts`, `vitest-tiers.ts`, `vitest-tiers.fixtures.ts`) and `test/helpers/serve-process.ts`, none of which any tsc program read before either. WHERE THE 28 CAME FROM, both directions attributed with no unexplained remainder: the same file population under the build config's inherited NodeNext semantics reports 144, and 144 is exactly what `scripts/check-type-check-coverage.mjs` had recorded for this package in its per-PACKAGE `TEST_DEBT` ledger — class for class on re-measurement (TS7006 x59, TS2835 x56, TS2339 x24, TS2307 x3, TS18046 x2). So this file does not add debt, it holds the SAME population one level finer, per file and per signature, and that `TEST_DEBT` entry graduated in the same change. 120 of the 144 dissolve under this program's vitest-matching module semantics (TS2835 x56 extension-less relative imports, the TS7006 x59 cascading above them, TS2307 x3, TS18046 x2) and were about the CHECK rather than the code; collapsing that cascade EXPOSED 4 more (TS18048 x4 in test/i18n-extract-action-description.test.ts, previously hidden behind an `any` from its two unresolved imports of `../src/utils/i18n-extract` and `../src/utils/i18n-coverage`). The 24 TS2339 survive the move unchanged, file for file and count for count. 144 - 120 + 4 = 28. A SECOND CONFIG-TIER PILE THAT IS NOT IN THIS LEDGER, recorded so nobody re-derives it: this package's test tree sits OUTSIDE the build config's `rootDir` (`src`), so with `rootDir` inherited the same program adds 116 x TS6059 — one per file in `test/`, saying nothing about any test. `tsconfig.test.json` widens `rootDir` to `../..` the way `packages/client`'s test config already does, and for one measured reason beyond the 116: three of these tests import fixtures from `examples/app-showcase/src/**`, which is outside this package entirely, so even `rootDir: \".\"` still reports 3 x TS6059. WHAT IS ALREADY GATED, and it is nearly all of the package: 112 of the 115 test files carry no entry here at all, so any error any one of them gains is red on arrival. ⚠️ This layer holds ZERO `@ts-expect-error` directives — measured across all 115 files plus the 3 root harness modules, not assumed — so unlike packages/spec and packages/client no pin was silently dead here, and none reports TS2578 under this program. The card's sharpest line ('a `@ts-expect-error` in any of those files is a phantom check') is therefore a statement about what WOULD happen, not about an existing dead pin. What the gap really cost is the other half: 144 real diagnostics that no gate has ever reported, and no way to write a type-level pin in this tree at all. THE PILE IS CONCENTRATED, which is the useful thing for whoever pays it down: test/data-model-rules.test.ts x23 is 23 of the 28 on its own, and all 23 are TS2339 property reads off a lint-diagnostic type ('message' x17, 'fix' x4, 'severity' x2) — one narrowing helper on that type is very nearly the whole ledger. The remaining 5 are 4 TS18048 in one file and a single TS2339 in another. RECORDED EXACTLY, no bootstrap margin: this layer has never been gated, so the first new error in it should go red rather than be absorbed. ⚠️ That includes test/run-dev-unbuilt-workspace.e2e.test.ts, a known repo-wide RUNTIME flake whose fix was in the merge queue when this landed: it type-checks clean and carries no entry here, so a type error it gains is red like any other. A flake in `vitest` is not a diagnostic in `tsc` and this ledger never sees one.",
"entries": {
"test/data-model-rules.test.ts": {
"TS2339: Property 'fix' does not exist on type '…'.": 4,
"TS2339: Property 'message' does not exist on type '…'.": 17,
"TS2339: Property 'severity' does not exist on type '…'.": 2
},
"test/i18n-declared-surface-gate.test.ts": {
"TS2339: Property 'locale' does not exist on type '…'.": 1
},
"test/i18n-extract-action-description.test.ts": {
"TS18048: 'bundles.en.objects' is possibly 'undefined'.": 2,
"TS18048: '…' is possibly 'undefined'.": 2
}
}
}
132 changes: 132 additions & 0 deletions packages/cli/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// The TEST-layer type-check program (#14710), adopting the mechanism #5286 set
// for `packages/spec` and #5449 generalised. `tsconfig.json` beside this file
// stays exactly as it is: it is the BUILD config, and its `include: ["src"]` is
// correct for a build — `tsconfig.build.json` ships `src` and nothing else.
// This sibling puts the unread layer in front of tsc, and `package.json`'s
// `typecheck` script NAMES it (via `check:test-typecheck --project`), because a
// config no script invokes is exactly the phantom this whole change is about.
//
// ⚠️ THIS PACKAGE REACHED THE HIDDEN STATE BY THE OTHER SPELLING, and that is
// the part worth carrying to the next package. AGENTS.md forbids `exclude`-ing
// `*.test.ts` from a package tsconfig. This package has no `exclude` AT ALL —
// it declares `include: ["src"]`, and its 115 test files live in a sibling
// `test/` tree the glob simply never reaches. Identical end state, arriving
// through a door the rule's text does not cover. Whether that gap should be
// closed in AGENTS.md or in `check:type-check-coverage` itself is deliberately
// NOT this file's business (the card's triage said so); what IS this file's
// business is that `hiddenTests` — which walks the whole package rather than
// the include roots — is the only detector that could ever have seen it.
//
// BEFORE THIS FILE, NO tsc PROGRAM COMPILED A SINGLE `test/` FILE HERE, and
// that is measured rather than read off the config. At 5a5336b399 with the
// workspace closure built first, `tsc --noEmit --listFiles -p tsconfig.json`
// puts 1009 files in the program and **0** of the 115 `test/**/*.test.ts`
// among them — while 119 of 119 non-test `src/**` files AND all 121 of the
// package's `src/**` test files ARE there. So the zero is the `include` line
// and not a probe that sees nothing, and the contrast is inside this one
// package: tests under `src` always compiled, tests under `test/` never did.
// The directional control is `packages/drivers/driver-memory`, whose tsconfig
// carries no test exclusion: the same probe puts **40 of its 40** test files in
// the program. Under this file the count is **115 of 115** (1416 files total).
// `pnpm --filter @objectstack/cli typecheck` exiting 0 was a true sentence
// carrying no information about any file in `test/`.
//
// THE THREE PACKAGE-ROOT HARNESS MODULES COME IN HERE TOO, deliberately and not
// as a separate concern: `vitest.config.ts`, `vitest-tiers.ts` and
// `vitest-tiers.fixtures.ts` were in no tsc program either (#14554 recorded
// them as NOT MEASURED rather than claiming coverage). They fall out of this
// same `include`, as does `test/helpers/serve-process.ts` — the one non-test
// module in the test tree, which had its own `UNCHECKED_SOURCE_DEBT` row in
// `scripts/check-type-check-coverage.mjs` predicting it would "graduate with
// the TEST_DEBT one rather than before it". Both graduated in this change.
//
// What differs from the build config, and what deliberately does NOT:
// - module semantics, plus `rootDir`. The tests are written and executed as
// ESM by vitest (esbuild/vite), and this package IS `"type": "module"`, so
// the build config's NodeNext compiles them as ESM too — and then demands
// explicit `.js` extensions on relative imports, which vitest does not.
// Measured cost of that mismatch here: 120 of the 144 raw diagnostics —
// TS2835 x56 (extension-less relative import), the TS7006 x59 sitting above
// them (an import that does not resolve makes every symbol it names `any`),
// TS2307 x3 and TS18046 x2. Those 120 are about the CHECK, never about the
// code. Matching vitest is fidelity, not laxity. `lib` keeps the build
// config's `DOM`/`DOM.Iterable`, restated rather than narrowed: unlike
// `packages/runtime`, this package's own build config already declares them.
// - ⚠️ Collapsing that cascade EXPOSES errors as well as removing them, so
// 144 − 120 is 24 and the real figure is 28:
// `test/i18n-extract-action-description.test.ts` gains 4 x TS18048 that its
// two unresolved imports (`../src/utils/i18n-extract`,
// `../src/utils/i18n-coverage`) had been hiding behind `any`. The 24 TS2339
// survive the move unchanged, file for file and count for count. Both
// directions are attributed in `test-typecheck-debt.json`; there is no
// unexplained remainder.
// - `rootDir` IS widened, to `../..`, and this is the one place this file
// departs from `packages/runtime`'s. Runtime's tests live under `src`, so
// its `rootDir` could stay inherited. This package's live in a sibling
// `test/` tree that is NOT under the inherited `rootDir` of `src`, and the
// measured cost of leaving it alone is 116 x TS6059 — one per file in
// `test/`, saying nothing about any test. `"."` is not enough either:
// three of these tests import fixtures from `examples/app-showcase/src/**`,
// outside this package entirely, and still report 3 x TS6059. `../..` is
// the repo root and is exactly the shape `packages/client`'s test config
// already uses for the same reason. ⛔ It widens the ROOT, never the
// strictness.
// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `esModuleInterop`,
// `forceConsistentCasingInFileNames` and the rest are inherited from
// `tsconfig.json`, and `types: ["node"]` restates it. Nothing here may
// loosen a type rule; if a test does not compile, that is the finding.
// ⛔ Not one `any` and not one `@ts-expect-error` was added to any test file
// to open this gate — that shape is what turns a real gate into a phantom.
//
// ⚠️ ONE THING THE FILING OVERSTATED, recorded so the next reader does not go
// looking for it: the card's sharpest line — "a `@ts-expect-error` in any of
// those 112 files is a phantom check that evaluates never" — is a statement
// about what WOULD happen, not about an existing pin. Measured here: this layer
// holds **zero** `@ts-expect-error` directives across all 115 test files and
// the 3 root harness modules, so no pin was silently dead and none reports
// TS2578 under this program. What the gap really cost is the other half: 144
// real diagnostics that no gate has ever reported, and no way to write a
// type-level pin in this tree at all. The first half is now ledgered; the
// second is now possible.
//
// MEASURED at 5a5336b399, workspace closure built first (an error count taken
// against an unbuilt closure is not a reading — unresolved-import cascades
// inflate it): this program reports **28 errors across 3 files**, from a raw
// 144 under the inherited NodeNext semantics. That 144 is the same number
// `scripts/check-type-check-coverage.mjs` recorded for this package in its
// per-PACKAGE `TEST_DEBT` ledger — reproduced class for class on re-measurement
// (TS7006 x59, TS2835 x56, TS2339 x24, TS2307 x3, TS18046 x2) — which is why
// that entry GRADUATES in this change rather than being paid down: the
// identical population is now held one level finer, per file and per signature,
// in `test-typecheck-debt.json` beside this config. By code the residue is
// TS2339 x24 and TS18048 x4; `test/data-model-rules.test.ts` carries 23 of the
// 28 on its own, all of them property reads off a lint-diagnostic type, so one
// narrowing helper there is very nearly the whole ledger.
//
// Every one of the 28 is PRE-EXISTING: this change edits no test file, and each
// would have been reported on `origin/main` had this program always existed.
// They are ledgered per file and per signature in `test-typecheck-debt.json`,
// EXACT and shrink-only — a file that gains an error is red, one that loses one
// is red until re-recorded, one that reaches zero is red until its entry is
// deleted, a signature that arrives or vanishes is red even at a constant
// total, and a file NOT listed there may have no errors at all.
// ⛔ The 28 are NOT repaired here, per this card's triage ruling: seeding the
// ledger and letting the ratchet shrink is the deliverable, and turning the
// onboarding into a cleanup is how it stops landing at all.
//
// ⚠️ 112 of the 115 files carry NO ledger entry, and that is load-bearing rather
// than incidental: any error any one of them ever gains is red on arrival. That
// is the half of this gate that starts working today.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"rootDir": "../..",
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["node"]
},
"include": ["test/**/*", "vitest.config.ts", "vitest-tiers.ts", "vitest-tiers.fixtures.ts"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading