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
3 changes: 2 additions & 1 deletion packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"scripts": {
"build": "tsc",
"test": "vitest run",
"type-check": "tsc --noEmit",
"type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
"type-check:typetests": "tsc -p tsconfig.typetests.json",
"lint": "eslint ."
},
"dependencies": {
Expand Down
24 changes: 23 additions & 1 deletion packages/app-shell/src/__tests__/spec-symbol-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@
* subpath's `.d.ts` through the TypeScript checker, exactly as
* `scripts/check-spec-symbol-derivation.mjs` does, and gets types and values
* alike.
*
* ## What compiles the `type _X = Assert<…>` lines below (objectui#3181)
*
* Nothing did, for the first stretch of this file's life. `tsconfig.json` here
* is the package BUILD config and excludes `**\/*.test.ts`; CI's only type gate
* drives that config (`pnpm type-check` -> turbo -> per-package `tsc --noEmit`),
* and types are erased before vitest ever runs. Appending a provably-false
* `Assert<Equal<1, 2>>` to this file therefore passed `pnpm type-check` at exit
* 0 — the type half of this "tripwire" was, literally, commentary. Which is the
* same landmine this file's own header cites objectui#3009 for.
*
* They are now compiled by `packages/app-shell/tsconfig.typetests.json`, chained
* from this package's `type-check` script, i.e. run by the CI `Type Check` job.
* That project lists its files EXPLICITLY (app-shell's wider test tree still has
* a pre-existing error backlog, tracked as TEST_DEBT), so **a new
* type-assertion test file is unchecked until it is added to that include
* list** — and `scripts/check-type-check-coverage.mjs` fails if the chaining is
* ever dropped, so the gate cannot go quiet again the way it did here.
*/

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

/** Compile-time assertions. A violation is a `tsc` error, not a runtime failure. */
/**
* Compile-time assertions. A violation is a `tsc` error, not a runtime failure —
* so it surfaces only under `tsconfig.typetests.json` (see the file header), not
* under vitest.
*/
type Assert<T extends true> = T;
type Extends<A, B> = [A] extends [B] ? true : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
Expand Down
48 changes: 48 additions & 0 deletions packages/app-shell/tsconfig.typetests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
// Compiles the test files whose ENTIRE value is compile-time type assertions,
// so that those assertions are actually checked by CI (objectui#3181).
//
// Why this exists as a THIRD project rather than as `tsconfig.test.json`:
//
// - `tsconfig.json` is the package BUILD (`tsc` -> dist, "rootDir": "src",
// "composite", "declaration"). It excludes `**/*.test.ts` correctly — test
// files would otherwise emit into the published dist.
// - `tsconfig.test.json` is the repo's name for "this package compiles ALL of
// its tests" (see packages/types). app-shell cannot claim that yet: its
// test tree still has a large pre-existing error backlog, declared as
// TEST_DEBT in scripts/check-type-check-coverage.mjs. Naming this file
// `tsconfig.test.json` would tell that guard the debt is paid and make it
// demand the entry be deleted — trading one false "checked" claim for
// another.
//
// So: a narrow, explicitly-listed project that compiles only files which are
// ALREADY clean and whose assertions are load-bearing. It is chained from the
// package's `type-check` script, which is what the CI `Type Check` job runs
// (`pnpm type-check` -> `turbo run type-check`), and that chaining is enforced
// by scripts/check-type-check-coverage.mjs — a config nothing runs is exactly
// the objectui#3009 / objectui#3181 failure this file is fixing.
//
// Adding a file here is a one-line change; the bar is that it compiles clean
// today. Do NOT switch this to a glob: a glob would sweep in the backlog and
// the first agent to hit it would "fix" that by deleting the whole project.
"extends": "../../tsconfig.json",
"compilerOptions": {
// A checking project, never an emitting one — and explicitly not part of
// the build graph, so it cannot leak test output into dist.
"noEmit": true,
"composite": false,
"declaration": false,
"lib": ["ES2020", "DOM"],
// `spec-symbol-parity.test.ts` resolves `@objectstack/spec`'s own `.d.ts`
// files off disk (createRequire / readFileSync / node:path) to read the
// spec's export names through the TypeScript checker.
"types": ["node"],
// Same reason as tsconfig.json: drop the root tsconfig's source-tree `paths`
// so `@objectstack/spec` and the `@object-ui/*` workspace deps resolve
// through the real dependency graph rather than through sibling `src/`.
"paths": {}
},
// Explicit list, not a glob. Every entry is a file whose type assertions are
// the point of the file.
"include": ["src/__tests__/spec-symbol-parity.test.ts"]
}
61 changes: 60 additions & 1 deletion scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ const CHECKED_BY_OWN_BUILD = {
// type to silence it.
const TEST_DEBT = {
"@object-ui/core": { errors: 72, issue: 4118, note: "TS2741x32, TS2322x17 — mostly the input-vs-output fixture confusion" },
// Partially covered already: `tsconfig.typetests.json` compiles the test files
// whose whole value is compile-time type assertions (objectui#3181). The entry
// stays because the REST of the test tree is still unchecked — the number below
// is that remainder, not the whole package.
"@object-ui/app-shell": { errors: 53, issue: 4118, note: "TS2339x24 — implementation wider than the type" },
"@object-ui/components": { errors: 31, issue: 4118, note: "TS7006x12, TS7031x12 — untyped test callback params" },
"@object-ui/react": { errors: 27, issue: 4118, note: "TS2769x9 — overload mismatch on render helpers" },
Expand Down Expand Up @@ -158,6 +162,12 @@ function collect() {
} catch {
/* no test config */
}
let typeTestsConfig = null;
try {
typeTestsConfig = readFileSync(resolve(root, dir, "tsconfig.typetests.json"), "utf8");
} catch {
/* no type-tests config */
}
const typeCheck = pkg.scripts?.["type-check"] ?? "";
out.push({
name: pkg.name,
Expand All @@ -176,6 +186,12 @@ function collect() {
// The config existing is not the same as anything running it — that gap
// IS objectui#3009. `type-check` has to chain it.
chainsTestConfig: /-p\s+tsconfig\.test\.json/.test(typeCheck),
hasTypeTestsConfig: typeTestsConfig !== null,
typeTestsConfig,
// Same question, same answer, for the narrow type-assertion project
// (objectui#3181): `type-check` is what CI runs, so `type-check` — not
// some sibling script CI never invokes — has to be the thing that runs it.
chainsTypeTestsConfig: /-p\s+tsconfig\.typetests\.json/.test(typeCheck),
});
}
}
Expand Down Expand Up @@ -324,6 +340,47 @@ for (const pkg of packages) {
}
}

// ── 5½. The narrow type-assertion project, if a package has one ──────────────
// Some test files exist ONLY for their COMPILE-TIME assertions (`Assert<Equal<A,
// B>>`). Types are erased at runtime, so vitest proves nothing about those; only
// `tsc` does. That is objectui#3181: app-shell's `spec-symbol-parity.test.ts`
// carried a header calling its assertions a tripwire, while a provably-false
// `Assert<Equal<1, 2>>` appended to it passed `pnpm type-check` at exit 0.
//
// A package whose test tree is still in TEST_DEBT can rescue those specific
// files with a `tsconfig.typetests.json` that lists them explicitly, instead of
// waiting for the whole backlog. This section keeps that project honest — it is
// worth exactly as much as the gate that runs it, and nothing at all otherwise.
for (const pkg of packages) {
if (!pkg.hasTypeTestsConfig) continue;

if (!pkg.chainsTypeTestsConfig) {
errors.push(
`${pkg.name} (${pkg.dir}) has a tsconfig.typetests.json that its "type-check" script never\n` +
` runs, so its compile-time assertions are erased at runtime and compiled by nothing —\n` +
` the exact state objectui#3181 fixed. CI runs \`pnpm type-check\` (turbo -> the package's\n` +
` own "type-check"), so that is the script that has to chain it:\n` +
` "type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json"`
);
continue;
}

if (!/"noEmit"\s*:\s*true/.test(pkg.typeTestsConfig)) {
errors.push(
`${pkg.name} (${pkg.dir}): tsconfig.typetests.json must set "noEmit": true — it is a checking\n` +
` project, and emitting would put test output in the published dist.`
);
}

if (!/\.test\.tsx?"/.test(pkg.typeTestsConfig)) {
errors.push(
`${pkg.name} (${pkg.dir}): tsconfig.typetests.json does not "include" any test file, so it\n` +
` compiles nothing and passes vacuously. List the files whose type assertions it exists\n` +
` to check, e.g. "include": ["src/__tests__/spec-symbol-parity.test.ts"]`
);
}
}

// 6. Ratchet — a declared test gap that has been closed must leave the list.
for (const [name, spec] of Object.entries(TEST_DEBT)) {
const pkg = byName.get(name);
Expand Down Expand Up @@ -363,6 +420,7 @@ const byBuild = Object.keys(CHECKED_BY_OWN_BUILD).length;
const withTests = packages.filter((p) => p.hasScript && p.testFiles > 0);
const testsChecked = withTests.filter(testsCovered).length;
const testDebtErrors = Object.values(TEST_DEBT).reduce((sum, d) => sum + d.errors, 0);
const typeTestProjects = packages.filter((p) => p.hasTypeTestsConfig && p.chainsTypeTestsConfig).length;

if (errors.length === 0) {
console.log(
Expand All @@ -373,7 +431,8 @@ if (errors.length === 0) {
);
console.log(
`✅ test type-check coverage: ${testsChecked}/${withTests.length} packages compile their tests, ` +
`${Object.keys(TEST_DEBT).length} declared debt (${testDebtErrors} errors outstanding).`
`${Object.keys(TEST_DEBT).length} declared debt (${testDebtErrors} errors outstanding), ` +
`${typeTestProjects} with a narrow type-assertion project.`
);
process.exit(0);
}
Expand Down
Loading