Skip to content

test(build): type-check test/build as a 5th tsconfig project - #378

Merged
mtskf merged 17 commits into
mainfrom
chore/test-build-tsconfig
Aug 23, 2026
Merged

test(build): type-check test/build as a 5th tsconfig project#378
mtskf merged 17 commits into
mainfrom
chore/test-build-tsconfig

Conversation

@mtskf

@mtskf mtskf commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Summary

test/build/ was in no tsconfig — the root config excludes "test" wholesale and test/extension/tsconfig.unit.json includes only the protocol drift guard — so the whole directory was transpile-only under vitest and every type-level assertion in it was permanently vacuous. This adds test/build/tsconfig.json as a 5th project in the pnpm compile chain and clears the four errors that surfaces.

Changes

  • test/build/tsconfig.json (new) — Node + ESM compiler view (no DOM lib, no vscode-webview types), includes test/build/**/*.ts plus src/shared for the cross-layer protocol import. noEmit inherited: this is a CI gate, not a build step.
  • package.json — appended tsc -p ./test/build/tsconfig.json to compile.
  • theme-palettes.test.ts — its @ts-expect-error sat above a Biome-wrapped multi-line import. TS7016 is reported at the module specifier, not the import keyword, so the directive covered the wrong line and produced both TS2578: Unused '@ts-expect-error' directive and the live TS7016 it meant to suppress. Swapped to a namespace import, which keeps the specifier on the directive's line however many bindings the file destructures below.
  • notice-covers-bundled-deps.test.ts — added the missing directive on the untyped esbuild.config.mjs import, and annotated configs as esbuild.BuildOptions[]: Object.values() over an any widens each entry to unknown, not any.

Related

  • TODO: "test/build/ is in NO tsconfig — its type-level assertions are permanently vacuous" (type-analyzer finding on PR ci: rehearse the release SBOM pipeline on every PR #354).
  • Follow-up filed: five suites (doc-sync, preview-server-theme, stale-todo-markers, todo-hygiene, verify-sbom-scope) still carry a file-level @ts-nocheck for their untyped .mjs import, which switches the whole file off — so the new project buys them nothing yet. Measured cost of swapping them to the line-scoped directive is 31 annotations across three files, kept out of this PR to hold it to one purpose. Documented in the tsconfig comment and in CLAUDE.md so the config is not misread as proof that every file under it is checked.

Test Plan

  • pnpm compile green with the new project as the 5th tsc -p invocation.
  • Non-vacuity proven: deleting the @ts-expect-error in theme-palettes.test.tsTS7016; dropping the esbuild.BuildOptions[] annotation → TS2345. Both go red only under the new project.
  • pnpm test:unit — 265 files, 5070 tests pass (the namespace-import rewrite is a runtime change to theme-palettes.test.ts).
  • pnpm build, pnpm package (incl. the vsix audit), pnpm lint all exit 0.

mtskf added 17 commits August 22, 2026 18:34
test/build/ sat in no tsconfig: the root config excludes "test" wholesale
and test/extension/tsconfig.unit.json includes only the protocol drift
guard, so the whole directory was transpile-only under vitest. Every
type-level assertion there was permanently vacuous — proof already on
disk in the shape of a @ts-expect-error that could never fire.

Add test/build/tsconfig.json (Node + ESM compiler view, no DOM lib) and
append it to the compile chain. Clearing the four pre-existing errors it
surfaces:

- theme-palettes.test.ts had its @ts-expect-error above a Biome-wrapped
  multi-line import. TS7016 is reported at the module specifier, so the
  directive covered the wrong line and produced both an unused-directive
  error and the live error it meant to suppress. A namespace import keeps
  the specifier on the directive's line no matter how many bindings the
  file destructures.
- notice-covers-bundled-deps.test.ts was missing the directive entirely
  on its untyped esbuild.config.mjs import, and Object.values() over the
  resulting any widens each entry to unknown — annotated at the boundary
  with the esbuild.BuildOptions[] shape the suite actually requires.

Non-vacuity measured by deleting each new pin and watching the new
project go red.

Five suites in the directory still carry a file-level @ts-nocheck for
their untyped .mjs import, which switches the whole file off; swapping
those to the line-scoped directive costs 31 annotations and is tracked
separately to keep this change to one purpose.
Adding test/build/ as a tsc project invalidated several nearby comments
that asserted the opposite, and the review surfaced one annotation that
reads as a checked pin but is not.

- notice-covers-bundled-deps.test.ts: the `esbuild.BuildOptions[]`
  annotation looked like a compile-time pin on createBuildConfigs, but
  contextual typing feeds the target type back into Object.values<T>'s
  inference, so `any` satisfies it trivially. Spelled as an `as` cast
  instead, which is honest about being an unverified assertion. It is
  still load-bearing: without it the call is unknown[] (TS2345).
- quoll-perf-flag.d.ts: "All four tsc programs" is now five.
- publish-workflow-sbom-config.test.ts: the comment said its
  Record<SharedStep, RegExp> exhaustiveness check "never executes"
  because test/build/ is in no tsconfig. It executes now — this file
  carries no @ts-nocheck. The runtime assertion stays as a backstop,
  with the reason stated.
- doc-sync / stale-todo-markers / todo-hygiene / preview-server-theme:
  all four justified their @ts-nocheck with "tsc does not include
  test/build/", which is no longer true of the directory.
- tsconfig.json: replaced the hand-maintained five-filename list (also
  duplicated in CLAUDE.md) with a git grep the reader can run, and
  fixed the rootDir rationale — rootDir plays no part in module
  resolution, only emit layout and the TS6059 containment check.

Measured: dropping the "Generate SBOM (SPDX)" entry yields TS2741 at the
LOAD_BEARING literal; removing the cast yields TS2345.
… rationale

The perf-flag comment named a literal program count that this PR itself had
to bump, and it was under-counting anyway (six programs include src/shared,
not the five in the compile chain). State the property instead of the number.

test/webview/tsconfig.json claimed rootDir is why its cross-layer imports
resolve — the same false mechanism this PR corrected in the new config, which
now sits next to it contradicting it. rootDir governs emit layout and the
TS6059 containment check only.
Cycle 2 of the review found that cycle 1's corrections had introduced two
false statements of their own.

The perf-flag comment had been broadened to "every tsc program in the repo
includes src/shared/**". There are seven programs, not the five the compile
script names: test/extension/tsconfig.json is the E2E emit config, rootDir
".", and pulls in no src/ file at all. Scope the claim to the programs that
compile a QUOLL_PERF reference, name the exception, and point at
`git ls-files '*tsconfig*.json'` as the way to check it -- reading the compile
script misses the configs no script chains, which is how three separate passes
over this PR reached the same wrong answer.

test/build/tsconfig.json still justified omitting the @ts-nocheck roster by
citing a duplicate list in CLAUDE.md that cycle 1 had itself deleted.

Comment-only: there is no behaviour here to pin, which is why the tests that
guard these facts land in a separate commit.
Every type-level guarantee in this repo holds only because `compile` runs tsc
over the project that owns it. Drop a project from that chain -- a reformat, a
bad merge, a "tidy the scripts" pass -- and build, test and CI all stay green
while every assertion that project gated goes permanently vacuous. That silent
revert is the failure mode test/build/tsconfig.json was added to close, and
nothing pinned the wiring itself until now.

Extracts the -p arguments rather than substring matching: `toContain("tsc -p
./")` is satisfied by any of the four nested project paths, so the root pin
could never have gone red.

Also pins the exception named by src/shared/quoll-perf-flag.d.ts -- the E2E
program compiles no src/ file. Widening that include silently falsifies the
comment; now it fails here instead.

Verified red:
  compile chain:  expected [ './', ...(3) ] to deeply equal [ './', ...(4) ]
  E2E exception:  expected [ '../../src/shared/**/*.ts' ] to deeply equal []
The bundle-step assertion had the exact hole this file warns about for
`tsc -p ./`: "pnpm compile" is a prefix of "pnpm compile:webview", which
`build` also runs, so toContain() stayed green after the compile step was
deleted outright. Split on && and compare whole steps, and pin the ordering
against the esbuild step rather than implying it in the test name.

Verified red by removing `pnpm compile &&` from `build`:
  AssertionError: expected [ 'pnpm compile:webview', ...(1) ] to include 'pnpm compile'
…nfigs as JSONC

Cycle 3 found the tripwire too loose to catch the failures it was written for.

Extracting `-p` arguments never saw what sat between the steps, so three
mutations left it green while the gate was dead: `;` instead of `&&` (in sh the
exit status is the last command's, demoting the first four projects to
non-gates), `|| true` on a step, and an `echo` in front of one. Pin the whole
script by exact equality instead -- strictly stronger and simpler than the
regex it replaces.

The ordering test pinned `pnpm compile` but not `pnpm compile:webview`, which
is the only tsc pass over src/webview and gates the same dist/webview/ output;
moving it behind esbuild stayed green. Pin both gates, and guard the bundle
step's index so a missing step fails closed.

tsconfig reads now tolerate JSONC. Only whole-line `//` comments are dropped,
so a `//` inside a string literal survives; test/extension/tsconfig.json is the
only comment-free config of the eight, making a future doc comment likely --
and JSON.parse would have turned that into a SyntaxError, breaking `pnpm test`
on a documentation edit.

Also corrects two false claims in test/build/tsconfig.json: the roster is named
in LEARNING.md as well as TODO.md, and the unanchored grep returns eight files,
not five -- three are prose about the directive, including two suites that say
they carry none. `git grep -l '^// @ts-nocheck' test/build` returns exactly the
five that do.

Verified red: dropping a project, `&&`->`;`, `|| true`, moving compile:webview
behind the bundle step, and leaking src/ into the E2E include. Verified green:
adding a `//` comment to that config.
…ot redundant

Cycle-3 simplify pass. Test 1's assertion was widened from a project roster to
a whole-script pin, but its name still described only half of what fails it.

The comment on `toContain` guards a live trap: a missing gate makes indexOf
return -1, and -1 satisfies toBeLessThan, so the ordering assertion alone would
pass silently. That line reads as redundant and is the one a future simplify
pass would delete.

Also reflows two comment blocks in test/build/tsconfig.json that the previous
edit left broken mid-phrase.
Codex and Fable were consulted independently on whether to fix these or stop;
both returned FIX_NOW on all three, and both judged text-pinning the right
approach rather than a treadmill -- only three scripts sit upstream of the
guarantee, so pinning all three is a fixed point with no surface left to
expand into.

compile:webview's body was never pinned. `build` calling it means nothing if
the script stops type-checking: "echo skip" or a trailing `|| true` left every
other assertion green while the only tsc pass over src/webview disappeared.
Pin the body exactly, as `compile` already is.

The gate-order check split on `&&` and compared indices, which cannot see shell
short-circuiting: `pnpm compile && pnpm compile:webview && false || node
esbuild…` put both gates before the bundle segment while `||` ran the bundler
on failure. Assert the required prefix instead -- it models the real property
(the bundler is reachable only through two successful gates) and replaces the
split/indexOf logic rather than adding to it.

The E2E include filter matched "src/" as a substring, missing a bare-directory
include spelled "../../src". Match `src` as a path segment.

Verified by 12 mutations: all four previously-green holes now red, all seven
earlier holes still red, and two negative controls still green -- an added
bundler flag and a JSONC comment, confirming the pins did not widen into false
tripwires.
… fails

`expect(script.startsWith(prefix)).toBe(true)` reports only "expected false to
be true", which tells whoever trips this tripwire nothing. Compare the sliced
prefix instead, so the failure prints the injected text -- under the
short-circuit mutation the `&& false ||` is visible in the diff itself.

Rename the test from "runs both type-check gates ahead of the bundle step" to
"reaches the bundle step only through both type-check gates": the old name
described the ordering check this commit's predecessor deleted, and ordering is
the weaker property that let `&& false || node esbuild…` through in the first
place.
…tail

Codex and Fable independently found the prefix pin leaves the script's tail
free: `… --production || node esbuild…` bundles ungated when a gate fails, and
`… --production || true` makes `pnpm build` exit 0 on a failed type-check. The
rename shipped in the previous commit therefore claimed more than the assertion
delivered.

Both proposed constraining the tail -- a grammar, or rejecting `[&|;\n]`. Taken
instead: pin `build` exactly, as `compile` already is.

Two reasons. The stated rationale for exempting it -- "the bundle step's
trailing flags are expected to move for ordinary reasons" -- was never measured
and is false: `build` has not changed since the initial commit. And a
forbidden-character list is a denylist, while this file's entire history is
denylists missing a case; that one would still admit `>`, `$(…)`, backticks and
`#`. An exact match has no such gap.

The cost is deliberate and now documented: changing `build` requires updating
this string, the same contract `compile` carries.

Verified by 13 mutations: both reported bypasses red, the three metacharacters
a denylist would have missed (`>`, `$(…)`, `#`) red, all cycle-4 regressions
still red, and a JSONC comment still green.
…ssed away

Rewriting the gate test for exact equality shortened "pinning `compile` alone
left the webview half free to drift behind esbuild while this test stayed
green" to "BOTH gates matter", dropping the measured episode that explains why
the second gate is pinned at all. Restored as a trailing clause, so the
paragraph does not grow.

Compressing a load-bearing fact into a shorter sentence is how three of this
file's four vacuities were introduced; doing it while fixing the fourth was
not the intended irony.
…local

"compile:webview is the only tsc pass over src/webview" is false: test/webview
and test/webview-browser both include ../../src/webview/**/*.ts, so `compile`
already compiles that source twice. What is unique is the ambient view --
compile:webview is the only pass using `types: ["vscode-webview"]` with no
node, which is what catches webview code reaching for a Node global. The
history claim was overstated for the same reason and is narrowed to the check
that actually drifted.

Also inlines `pkg.scripts.compile`, matching the other two script pins.
The adversarial review measured the boundary rather than assuming it, so write
it down where a reader will hit it: the chain is pinned, the contents of each
link are not, the test's own execution path is not, and dist/ is still
reachable outside the gate by design.

A tripwire that is trusted for more than it checks is how the original bug
survived -- test/build/ sat outside every tsconfig for months while its
@ts-expect-error pins read as enforcement.

The per-project content gap is filed as a follow-up; it needs a design, not
another assertion.
Cycle 7 found the same claim corrected in one place and left standing 24 lines
below ("the ONLY tsc pass over src/webview"), the correction ITSELF still
overclaiming, and the new boundary note granting protection it does not have.

The gate-2 rationale is now measured rather than reasoned: a `process.cwd()`
probe in src/webview/shell.ts reddens compile:webview AND test/webview-browser
but not test/webview, so the browser program already subsumes what
compile:webview checks. The gate is pinned as defence in depth against an
include-shrink nothing pins -- not because it uniquely catches Node globals,
which was the cited mechanism and is false.

The boundary note claimed the E2E include check was "the one exception" to
shrink/noCheck/strict; it is not an exception to that list at all -- it fires
when that config GAINS a src path. And CI runs `pnpm test:unit`, never the
composite `pnpm test`.

Every one of the five comment defects across this review has the same shape: an
unmeasured explanation of WHY something matters, wrapped around an assertion
that was correct. These edits remove mechanism claims rather than adding more.
Cosmetic only. One comment line ran to 90 columns against the file's 81-column
maximum, and "measured" appeared twice in the same paragraph -- the blanket
lead-in and, three lines later, the scoped attribution naming which three
claims were measured. Kept the scoped one.

No assertion, test name or fact claim touched.
@mtskf
mtskf merged commit 88f135a into main Aug 23, 2026
2 checks passed
@mtskf
mtskf deleted the chore/test-build-tsconfig branch August 23, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant