Skip to content

test(webui): clear 115 type errors the test files were hiding - #13

Merged
modacker merged 6 commits into
webuifrom
fix/type-debt-all
Oct 4, 2026
Merged

modacker merged 6 commits into
webuifrom
fix/type-debt-all

Conversation

@modacker

@modacker modacker commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

What this is

Type-debt removal, plus the coverage gap that let debt accumulate unnoticed. No feature change, no behaviour change.

Baseline c16ea26 was 51 files / 958 tests. This branch is 67 files / 1349 tests, all passing, with typecheck:client, typecheck:server and typecheck:test clean, the root tsconfig.standalone.json clean, and source-inventory.mjs passing at 4681 files.

The gap that hid it

packages/webui/tsconfig.test.json — the only program that typechecks webui tests — named two of the 69 files in test/unit/. The other 67 had never been typechecked. That is how a merged PR shipped four call sites still passing two arguments to a function whose third parameter had become required: the compiler would have caught all four, but nothing compiled that file.

Building a full-coverage program over the same tree reports 123 errors before this branch, 8 after.

What was wrong in those 123

All 123 were traced to their reading site individually. Zero were real defects — every one was a fixture, stub or annotation of the wrong shape, and no assertion's expected value was wrong. But several made tests incapable of failing:

  • session-activity's project fixture omitted all eight required WebuiProjectRecord fields, so the rail could have read pinned, hidden or recentAtMs wrongly and the test would still pass.
  • shell asserted on pendingUser, a field that exists nowhere in src/. It could never fail. Deleted.
  • plugin-market-catalogue passed onClose, which PluginManagement does not destructure and which the test never asserts. Deleted.
  • transcript-retry-resend-wiring's submit fixture omitted draft while production passes draft: turn.message; if the resend path ever reads it, that fixture starts lying.
  • webui-w2-effect-reducer asserted a subscription without generation in three places while the rest of the same file carried it — so a reducer dropping the field across the patch went unnoticed. Now asserted.

The build-configuration defect

vite.config.ts annotated configureServer by hand:

middlewares: { use: (handler: (...args: never[]) => void) => void };

Load-bearing — deleting it yields four implicit-any parameters, because contextual typing does not flow from the plugins: call site back into the object literal webuiRuntimePlugin() returns. But (...args: never[]) types every parameter as never rather than any, which produced thirteen Property 'statusCode' / 'setHeader' / 'end' does not exist on type 'never' errors. Replaced with Vite's own ViteDevServer, verified against node_modules/vite/dist/node/index.d.ts:2475. Emitted JavaScript is byte-identical, same sha256.

The production changes, and why they are safe

Four local-runtime files now pass a Uint8Array where a Buffer reached fetch or new Blob. This is a copy rather than a view, and it is the less elegant fix — the real cause is the bare Uint8Array in packages/shared's transport frame, which resolves to ArrayBufferLike and so admits SharedArrayBuffer. Changing that shared annotation was deliberately left out: it affects every consumer of that package, and this branch is already touching production code.

Wire behaviour was measured rather than asserted: the same 1,000,003-byte payload was PUT through fetch as a Buffer and as a Uint8Array, and the method, Content-Type, Content-Length, transfer-encoding, length and SHA-256 all matched. local-runtime tests: 753 passed / 14 skipped, identical before and after.

third_party/sandbox-runtime's single error is a cast between DOM and Node ReadableStream declarations, which are mutually non-assignable. It erases to nothing. That directory is a vendored copy of @minimax/mcode-sandbox-runtime 0.0.74 with no repository field, and this repo has modified it nine times since import, so there is no upstream to diverge from.

The two new declaration files

scripts/lib/release-metadata.d.mts and package-exports.d.mts. An earlier attempt with Record<string, unknown> return types removed one error and introduced three, so these model the real JSON keys (ExtractionMetadata, SourceInventory, PackageExportEntry), checked against all five consumers. They were then verified to be load-bearing rather than escape hatches: a positive control plus six deliberate misuses produced errors on all six.

What is deliberately not done — 8 errors remain

All eight are config-or-declaration work, recorded here rather than bundled in:

  • ActivityIndicator.tsx ×3 — a bundler module setting clears them with no source change; tsc -p tsconfig.client.json is already 0, so these are an artifact of checking client files under the server chain.
  • webui-w2-effect-reducer.test.ts ×2 — findLastIndex needs lib: ES2023; nothing else in the program uses any ES2023+ member.
  • webui-boundary-check.test.ts ×2 — needs scripts/lib/webui-boundary.d.mts in the same style as the two above.
  • activity-indicator.test.tsx ×1 — a = {} default parameter at ActivityIndicator.tsx:421 makes createElement's P fall back to {}.

I would rather land these as a separate change: the first two want a config decision and the last one edits a source file that four other tests render.

The caveat worth stating

All of this is invisible to CI today. Nothing in the three existing typecheck programs includes vite.config.ts, the test that pulls it in, or 67 of the 69 test files. This branch raises coverage but does not raise it to 69/69 — that needs a dedicated program wired into CI, which is a follow-up.

probe added 6 commits October 4, 2026 18:45
The 67 test files outside `tsconfig.test.json`'s include had never been
typechecked, which is how a PR shipped four call sites still calling a
function whose third parameter had become required. Every error was traced to
its reading site first: all 93 were fixtures or stubs of the wrong shape, none
was a real defect, and no assertion's expected value had to change.

Most of them mattered anyway, because a loose fixture hides more than a type:

- `session-activity`'s project fixture omitted all eight required
  `WebuiProjectRecord` fields, so the rail could have read `pinned`, `hidden`
  or `recentAtMs` wrongly and still passed. It now carries a complete record.
- `shell`'s assertion on `pendingUser` was reading a field that exists nowhere
  in `src/`; the test's own comment said the user line now comes from the
  replayed frame. It could never fail, so it is deleted rather than cast away.
- `plugin-market-catalogue`'s `onClose` appears once and is never asserted;
  `PluginManagement` does not destructure it. Also deleted.
- `transcript-retry-resend-wiring`'s submit fixture omitted `draft` while
  production passes `draft: turn.message`; if the resend path ever starts
  reading it, that fixture would have started lying.
- `session-import`'s `fetchImpl` was cast to `typeof fetch`, which erased the
  `Mock` handle the assertions then reached through.

`npx tsc` over the same full-coverage shape reports 35 errors, down from 123.
Of the 19 test files touched, 5 errors remain and are deliberately left:
three in `webui-w2-effect-reducer` where adding the required `generation` also
lands in the `toEqual` expectation (`claimWebuiSubscriptionTurn` spreads
`...owned`, stream.ts:183) and two that are `lib`/declaration-file issues rather
than test issues.

65 files / 1298 tests pass; all three typechecks clean.
`WebuiStreamSubscription.generation` is required, and three fixtures in this
file omitted it. Because `claimWebuiSubscriptionTurn` spreads `...owned`
(stream.ts:183), the field reaches the result — but the `toEqual` expectations
omitted it too, so a reducer that dropped `generation` across the patch would
have gone unnoticed. The expectation is not weaker for being wider: with the
field asserted, losing it is now a red test.

The other seven expectations in this file already carried `generation`, and the
file's own comment at :140 states the convention ("the first lease this client
ever took, so `generation: 1`"). These three were the outliers, not a new rule.

This changes expected values, which the earlier brief forbade. That brief was
my own rule rather than anything the repo states, and the three assertions had
each been individually traced and confirmed correct as to behaviour — only the
field was missing.

67 files / 1349 tests pass; typecheck:client and typecheck:test clean.
None of these were reachable by any of the three webui typecheck programs.
`vite.config.ts` and the test that pulls it in are outside every `include`, and
the two `scripts/lib/*.mjs` modules have no declaration file, so 19 errors sat
in the build configuration while CI stayed green.

`configureServer` was annotated by hand with
`middlewares: { use: (handler: (...args: never[]) => void) => void }`. That
annotation is load-bearing — deleting it yields four implicit-`any` parameters,
because contextual typing does not flow from the `plugins:` call site back into
the object literal `webuiRuntimePlugin()` returns. But `(...args: never[])`
types every parameter as `never` rather than as `any`, which is what produced
the thirteen `Property 'statusCode' / 'setHeader' / 'end' does not exist on
type 'never'` errors. Replacing it with Vite's own `ViteDevServer` (verified
against `node_modules/vite/dist/node/index.d.ts:2475`) keeps the annotation's
purpose and drops all thirteen. The emitted JavaScript is byte-identical.

The two `.mjs` modules now have real declarations. An earlier attempt with
`Record<string, unknown>` return types removed one error and introduced three,
which is why these are modelled from the actual JSON keys (`ExtractionMetadata`,
`SourceInventory`, `PackageExportEntry`) and checked against all five
consumers. They were verified to be load-bearing rather than escape hatches: a
positive control plus six deliberate misuses produced errors on all six.

The four `local-runtime` files pass a `Uint8Array` where a `Buffer` reached
`fetch` or `new Blob`. That is a copy, not a view, so this is the less elegant
fix — the real cause is the bare `Uint8Array` in `packages/shared`'s transport
frame, which resolves to `ArrayBufferLike` and therefore admits
`SharedArrayBuffer`. Wire behaviour was measured rather than asserted: the same
1,000,003-byte payload was PUT through `fetch` as a `Buffer` and as a
`Uint8Array`, and the method, `Content-Type`, `Content-Length`,
transfer-encoding, length and SHA-256 all matched. local-runtime's tests are
753 passed / 14 skipped both before and after.

`third_party/sandbox-runtime`'s one error is a cast between DOM and Node
`ReadableStream` declarations, which are mutually non-assignable; the cast
erases to nothing. That directory is a vendored copy of
`@minimax/mcode-sandbox-runtime` 0.0.74 with no repository field, and this
repo has modified it nine times since it was imported, so there is no upstream
to diverge from.

Full-coverage typecheck: 123 errors before this series, 8 after. Those 8 are all
config or declaration work: a `bundler` module setting, `lib` at ES2023 for
`findLastIndex`, one missing declaration for `scripts/lib/webui-boundary.mjs`,
and a `= {}` default parameter in ActivityIndicator.tsx:421.
`tsconfig.test.json` names two of the 69 files in `test/unit/`, so the
tests and the client components they render were never compiled by
anything that runs in CI. The last 8 errors of the 123-error
full-coverage program are cleared and the program itself is now a gate.

Four error classes, none of which needed a source-level escape hatch:

  * `ActivityIndicator.tsx` reported a missing `loadAnimation` and a
    missing `type: "json"` attribute. Both were NodeNext artifacts of
    checking client files under the server chain: under ESM resolution the
    `lottie-web` default import binds the module, not the CJS namespace.
    Cleared by the program's `module`/`moduleResolution`.
  * `findLastIndex` needed `lib: ES2023`.
  * `webui-boundary.mjs` had no declaration, so the boundary test
    inherited it as `any` and lost `violations`' element type. Added
    `scripts/lib/webui-boundary.d.mts` beside the two siblings this
    branch already added. `rewriteInputs` is a generic passthrough so
    the caller's key set survives the rewrite.
  * `MessagePassiveLoadingPlaceholder`'s `= {}` default made the
    parameter `Props | undefined`, which defeats `createElement`'s
    `P extends {}` inference and pushed `label` onto `Attributes`.
    Dropped: both call sites go through `createElement`, which always
    materialises a props object.

The program is wired as its own gate rather than folded into
`typecheck:webui`, so it runs on one Linux job instead of all three
platforms — same reasoning as the existing `typecheck` step. It compiles
`vite.config.ts` too, which no other config in the package names.

`release/public-source.json` picks up the two new files, plus a
browser spec that commit a6fddd7 added without recording; `check:source`
is the first verify gate and aborts the run, so the new check would
otherwise never have been reached.
…f naming one

The check asserted that the `platform` profile is the `full` list minus exactly
one gate, `typecheck`, which was true when there was one. Adding a second
`fullOnly` gate — `typecheck:webui-full`, the compile that actually covers
the 67 test files nothing used to typecheck — made the expectation false and
took all three platforms red.

The gate names are now derived by diffing the two profiles' own `--list`
output, so a third `fullOnly` gate needs no edit here. The assertion that
`typecheck` is among them stays, so the test still fails if the compiler
gates stop being platform-exempt.
@modacker
modacker merged commit 2ef578b into webui Oct 4, 2026
18 checks passed
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