Skip to content

Commit 496825f

Browse files
os-zhuangclaude
andauthored
fix(app-shell): actually compile spec-symbol-parity.test.ts's type assertions (#3181) (#3187)
The file's `type _X = Assert<Equal<A, B>>` lines were never read by any compiler. `packages/app-shell/tsconfig.json` is the BUILD config and excludes `**/*.test.ts`; CI's only type gate drives that config (`pnpm type-check` -> turbo -> per-package `tsc --noEmit`), eslint is not type-aware, and vitest erases types before it runs. A provably-false `Assert<Equal<1, 2>>` appended to the file passed `pnpm type-check` at exit 0 — the same "declared != enforced" landmine the file's own header cites #3009 for. Adds `packages/app-shell/tsconfig.typetests.json`: an emit-free project with an EXPLICIT include list (not a glob), chained from the package's `type-check` script so it runs in the existing CI `Type Check` job. The list is explicit because app-shell's wider test tree still has a pre-existing error backlog, already declared as TEST_DEBT — a glob would sweep that in and the project would get deleted rather than fixed. That backlog stays tracked in #3181. `scripts/check-type-check-coverage.mjs` grows the matching ratchet: a `tsconfig.typetests.json` that `type-check` does not chain, that emits, or that includes no test file is now a hard failure, so this gate cannot go quiet the same way the assertions did. Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 Co-authored-by: Claude <noreply@anthropic.com>
1 parent f07d5c7 commit 496825f

4 files changed

Lines changed: 133 additions & 3 deletions

File tree

packages/app-shell/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"scripts": {
2828
"build": "tsc",
2929
"test": "vitest run",
30-
"type-check": "tsc --noEmit",
30+
"type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
31+
"type-check:typetests": "tsc -p tsconfig.typetests.json",
3132
"lint": "eslint ."
3233
},
3334
"dependencies": {

packages/app-shell/src/__tests__/spec-symbol-parity.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@
3737
* subpath's `.d.ts` through the TypeScript checker, exactly as
3838
* `scripts/check-spec-symbol-derivation.mjs` does, and gets types and values
3939
* alike.
40+
*
41+
* ## What compiles the `type _X = Assert<…>` lines below (objectui#3181)
42+
*
43+
* Nothing did, for the first stretch of this file's life. `tsconfig.json` here
44+
* is the package BUILD config and excludes `**\/*.test.ts`; CI's only type gate
45+
* drives that config (`pnpm type-check` -> turbo -> per-package `tsc --noEmit`),
46+
* and types are erased before vitest ever runs. Appending a provably-false
47+
* `Assert<Equal<1, 2>>` to this file therefore passed `pnpm type-check` at exit
48+
* 0 — the type half of this "tripwire" was, literally, commentary. Which is the
49+
* same landmine this file's own header cites objectui#3009 for.
50+
*
51+
* They are now compiled by `packages/app-shell/tsconfig.typetests.json`, chained
52+
* from this package's `type-check` script, i.e. run by the CI `Type Check` job.
53+
* That project lists its files EXPLICITLY (app-shell's wider test tree still has
54+
* a pre-existing error backlog, tracked as TEST_DEBT), so **a new
55+
* type-assertion test file is unchecked until it is added to that include
56+
* list** — and `scripts/check-type-check-coverage.mjs` fails if the chaining is
57+
* ever dropped, so the gate cannot go quiet again the way it did here.
4058
*/
4159

4260
import { describe, it, expect } from 'vitest';
@@ -193,7 +211,11 @@ describe('re-exported values are the spec binding itself', () => {
193211
/* divergence cannot silently grow and cannot silently outlive its reason. */
194212
/* -------------------------------------------------------------------------- */
195213

196-
/** Compile-time assertions. A violation is a `tsc` error, not a runtime failure. */
214+
/**
215+
* Compile-time assertions. A violation is a `tsc` error, not a runtime failure —
216+
* so it surfaces only under `tsconfig.typetests.json` (see the file header), not
217+
* under vitest.
218+
*/
197219
type Assert<T extends true> = T;
198220
type Extends<A, B> = [A] extends [B] ? true : false;
199221
type IsAny<T> = 0 extends 1 & T ? true : false;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
// Compiles the test files whose ENTIRE value is compile-time type assertions,
3+
// so that those assertions are actually checked by CI (objectui#3181).
4+
//
5+
// Why this exists as a THIRD project rather than as `tsconfig.test.json`:
6+
//
7+
// - `tsconfig.json` is the package BUILD (`tsc` -> dist, "rootDir": "src",
8+
// "composite", "declaration"). It excludes `**/*.test.ts` correctly — test
9+
// files would otherwise emit into the published dist.
10+
// - `tsconfig.test.json` is the repo's name for "this package compiles ALL of
11+
// its tests" (see packages/types). app-shell cannot claim that yet: its
12+
// test tree still has a large pre-existing error backlog, declared as
13+
// TEST_DEBT in scripts/check-type-check-coverage.mjs. Naming this file
14+
// `tsconfig.test.json` would tell that guard the debt is paid and make it
15+
// demand the entry be deleted — trading one false "checked" claim for
16+
// another.
17+
//
18+
// So: a narrow, explicitly-listed project that compiles only files which are
19+
// ALREADY clean and whose assertions are load-bearing. It is chained from the
20+
// package's `type-check` script, which is what the CI `Type Check` job runs
21+
// (`pnpm type-check` -> `turbo run type-check`), and that chaining is enforced
22+
// by scripts/check-type-check-coverage.mjs — a config nothing runs is exactly
23+
// the objectui#3009 / objectui#3181 failure this file is fixing.
24+
//
25+
// Adding a file here is a one-line change; the bar is that it compiles clean
26+
// today. Do NOT switch this to a glob: a glob would sweep in the backlog and
27+
// the first agent to hit it would "fix" that by deleting the whole project.
28+
"extends": "../../tsconfig.json",
29+
"compilerOptions": {
30+
// A checking project, never an emitting one — and explicitly not part of
31+
// the build graph, so it cannot leak test output into dist.
32+
"noEmit": true,
33+
"composite": false,
34+
"declaration": false,
35+
"lib": ["ES2020", "DOM"],
36+
// `spec-symbol-parity.test.ts` resolves `@objectstack/spec`'s own `.d.ts`
37+
// files off disk (createRequire / readFileSync / node:path) to read the
38+
// spec's export names through the TypeScript checker.
39+
"types": ["node"],
40+
// Same reason as tsconfig.json: drop the root tsconfig's source-tree `paths`
41+
// so `@objectstack/spec` and the `@object-ui/*` workspace deps resolve
42+
// through the real dependency graph rather than through sibling `src/`.
43+
"paths": {}
44+
},
45+
// Explicit list, not a glob. Every entry is a file whose type assertions are
46+
// the point of the file.
47+
"include": ["src/__tests__/spec-symbol-parity.test.ts"]
48+
}

scripts/check-type-check-coverage.mjs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ const CHECKED_BY_OWN_BUILD = {
8989
// type to silence it.
9090
const TEST_DEBT = {
9191
"@object-ui/core": { errors: 72, issue: 4118, note: "TS2741x32, TS2322x17 — mostly the input-vs-output fixture confusion" },
92+
// Partially covered already: `tsconfig.typetests.json` compiles the test files
93+
// whose whole value is compile-time type assertions (objectui#3181). The entry
94+
// stays because the REST of the test tree is still unchecked — the number below
95+
// is that remainder, not the whole package.
9296
"@object-ui/app-shell": { errors: 53, issue: 4118, note: "TS2339x24 — implementation wider than the type" },
9397
"@object-ui/components": { errors: 31, issue: 4118, note: "TS7006x12, TS7031x12 — untyped test callback params" },
9498
"@object-ui/react": { errors: 27, issue: 4118, note: "TS2769x9 — overload mismatch on render helpers" },
@@ -158,6 +162,12 @@ function collect() {
158162
} catch {
159163
/* no test config */
160164
}
165+
let typeTestsConfig = null;
166+
try {
167+
typeTestsConfig = readFileSync(resolve(root, dir, "tsconfig.typetests.json"), "utf8");
168+
} catch {
169+
/* no type-tests config */
170+
}
161171
const typeCheck = pkg.scripts?.["type-check"] ?? "";
162172
out.push({
163173
name: pkg.name,
@@ -176,6 +186,12 @@ function collect() {
176186
// The config existing is not the same as anything running it — that gap
177187
// IS objectui#3009. `type-check` has to chain it.
178188
chainsTestConfig: /-p\s+tsconfig\.test\.json/.test(typeCheck),
189+
hasTypeTestsConfig: typeTestsConfig !== null,
190+
typeTestsConfig,
191+
// Same question, same answer, for the narrow type-assertion project
192+
// (objectui#3181): `type-check` is what CI runs, so `type-check` — not
193+
// some sibling script CI never invokes — has to be the thing that runs it.
194+
chainsTypeTestsConfig: /-p\s+tsconfig\.typetests\.json/.test(typeCheck),
179195
});
180196
}
181197
}
@@ -324,6 +340,47 @@ for (const pkg of packages) {
324340
}
325341
}
326342

343+
// ── 5½. The narrow type-assertion project, if a package has one ──────────────
344+
// Some test files exist ONLY for their COMPILE-TIME assertions (`Assert<Equal<A,
345+
// B>>`). Types are erased at runtime, so vitest proves nothing about those; only
346+
// `tsc` does. That is objectui#3181: app-shell's `spec-symbol-parity.test.ts`
347+
// carried a header calling its assertions a tripwire, while a provably-false
348+
// `Assert<Equal<1, 2>>` appended to it passed `pnpm type-check` at exit 0.
349+
//
350+
// A package whose test tree is still in TEST_DEBT can rescue those specific
351+
// files with a `tsconfig.typetests.json` that lists them explicitly, instead of
352+
// waiting for the whole backlog. This section keeps that project honest — it is
353+
// worth exactly as much as the gate that runs it, and nothing at all otherwise.
354+
for (const pkg of packages) {
355+
if (!pkg.hasTypeTestsConfig) continue;
356+
357+
if (!pkg.chainsTypeTestsConfig) {
358+
errors.push(
359+
`${pkg.name} (${pkg.dir}) has a tsconfig.typetests.json that its "type-check" script never\n` +
360+
` runs, so its compile-time assertions are erased at runtime and compiled by nothing —\n` +
361+
` the exact state objectui#3181 fixed. CI runs \`pnpm type-check\` (turbo -> the package's\n` +
362+
` own "type-check"), so that is the script that has to chain it:\n` +
363+
` "type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json"`
364+
);
365+
continue;
366+
}
367+
368+
if (!/"noEmit"\s*:\s*true/.test(pkg.typeTestsConfig)) {
369+
errors.push(
370+
`${pkg.name} (${pkg.dir}): tsconfig.typetests.json must set "noEmit": true — it is a checking\n` +
371+
` project, and emitting would put test output in the published dist.`
372+
);
373+
}
374+
375+
if (!/\.test\.tsx?"/.test(pkg.typeTestsConfig)) {
376+
errors.push(
377+
`${pkg.name} (${pkg.dir}): tsconfig.typetests.json does not "include" any test file, so it\n` +
378+
` compiles nothing and passes vacuously. List the files whose type assertions it exists\n` +
379+
` to check, e.g. "include": ["src/__tests__/spec-symbol-parity.test.ts"]`
380+
);
381+
}
382+
}
383+
327384
// 6. Ratchet — a declared test gap that has been closed must leave the list.
328385
for (const [name, spec] of Object.entries(TEST_DEBT)) {
329386
const pkg = byName.get(name);
@@ -363,6 +420,7 @@ const byBuild = Object.keys(CHECKED_BY_OWN_BUILD).length;
363420
const withTests = packages.filter((p) => p.hasScript && p.testFiles > 0);
364421
const testsChecked = withTests.filter(testsCovered).length;
365422
const testDebtErrors = Object.values(TEST_DEBT).reduce((sum, d) => sum + d.errors, 0);
423+
const typeTestProjects = packages.filter((p) => p.hasTypeTestsConfig && p.chainsTypeTestsConfig).length;
366424

367425
if (errors.length === 0) {
368426
console.log(
@@ -373,7 +431,8 @@ if (errors.length === 0) {
373431
);
374432
console.log(
375433
`✅ test type-check coverage: ${testsChecked}/${withTests.length} packages compile their tests, ` +
376-
`${Object.keys(TEST_DEBT).length} declared debt (${testDebtErrors} errors outstanding).`
434+
`${Object.keys(TEST_DEBT).length} declared debt (${testDebtErrors} errors outstanding), ` +
435+
`${typeTestProjects} with a narrow type-assertion project.`
377436
);
378437
process.exit(0);
379438
}

0 commit comments

Comments
 (0)