Skip to content

chore(types): plugin-dashboard's tests compile — the #4040 program closes at 40/40 - #4530

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4040-dashboard-test-typecheck
Aug 13, 2026
Merged

chore(types): plugin-dashboard's tests compile — the #4040 program closes at 40/40#4530
yinlianghui merged 1 commit into
mainfrom
claude/issue-4040-dashboard-test-typecheck

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4040 — the program card stays open for the PM to close with a summary.

@object-ui/plugin-dashboard was the last TEST_DEBT entry. Its 47 test files are now compiled by a tsconfig.test.json chained off type-check, and the registry table is empty.

The measured debt: 14, not 6

The ledger row said 6. Remeasured against origin/main @ 932cbcd6b with the sibling-convention project, tsc -p tsconfig.test.json reported 14 — the same direction the whole table was wrong in (i18n declared 13 / measured 103; react 27 / 43). Verbatim:

src/__tests__/DashboardRenderer.domProps.test.tsx(240,25): error TS7006: Parameter 'id' implicitly has an 'any' type.
src/__tests__/DashboardRenderer.domProps.test.tsx(266,25): error TS7006: Parameter 'id' implicitly has an 'any' type.
src/__tests__/DashboardRenderer.filters.test.tsx(36,57): error TS2493: Tuple type '[]' of length '0' has no element at index '0'.
src/__tests__/DashboardRenderer.filters.test.tsx(38,11): error TS2352: Conversion of type 'undefined' to type '{ runtimeFilter?: unknown; }' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
src/__tests__/DashboardRenderer.filters.test.tsx(38,35): error TS2493: Tuple type '[]' of length '0' has no element at index '1'.
src/__tests__/DashboardRenderer.filters.test.tsx(84,70): error TS2322: Type 'string' is not assignable to type '{ value: string | number | boolean; label: string | Record< string, string >; }'.
src/__tests__/DashboardRenderer.filters.test.tsx(84,78): error TS2322: Type 'string' is not assignable to type '{ value: string | number | boolean; label: string | Record< string, string >; }'.
src/__tests__/DashboardRenderer.filters.test.tsx(141,70): error TS2322: Type 'string' is not assignable to type '{ value: string | number | boolean; label: string | Record< string, string >; }'.
src/__tests__/DatasetWidget.chartConfig.dom.test.tsx(38,8): error TS2882: Cannot find module or type declarations for side-effect import of '@object-ui/plugin-charts/AdvancedChartImpl'.
src/__tests__/DatasetWidget.comboPresentation.test.tsx(52,38): error TS2307: Cannot find module '@object-ui/plugin-charts/normalizeChartSchema' or its corresponding type declarations.
src/__tests__/DatasetWidget.queryOptions.test.tsx(143,43): error TS2493: Tuple type '[]' of length '0' has no element at index '1'.
src/__tests__/DatasetWidget.test.tsx(565,36): error TS2493: Tuple type '[]' of length '0' has no element at index '1'.
src/__tests__/DatasetWidget.test.tsx(613,36): error TS2493: Tuple type '[]' of length '0' has no element at index '1'.
src/__tests__/ObjectDataTable.cells.test.tsx(45,60): error TS2571: Object is of type 'unknown'.

(The Record< string, string > spacings above are the GitHub sanitizer workaround; the compiler printed them closed up.)

What the 14 were — no source file touched

6x TS2493 / TS2352 — mocks that could not be read. queryDataset and find were declared vi.fn(async () => …), with no parameters, while DatasetWidget calls them as queryDataset(dataset, selection) / find(object, params). mock.calls therefore typed as an array of EMPTY tuples, so every calls[0][1] the drill and dashboard-broadcast assertions are built on read out of bounds — and because each read is cast, it was invisible at runtime too. The mocks now declare the call shape (DatasetCapableSource in ../DatasetWidget). Assertion strength unchanged; what changed is that the reads are now checked.

3x TS2322 — two fixtures spelling options: ['EMEA', 'APAC']. That bare-string form is a UI-side authoring shorthand normalizeFilterOptions lifts; GlobalFilterSchema declares only the { value, label } pair form. Re-spelled to the pair form. No coverage moved: the shorthand's own pin is packages/core/src/utils/__tests__/dashboard-filters.test.ts:46, in a package that already type-checks its tests, and neither dashboard case reads the option list — both broadcasts come from defaultValue.

2x TS7006 — onWidgetClick={(id) => …} inferring any. Annotated (id: string | null), which restates what DashboardRendererProps itself declares. The reason inference failed is a real defect and is not fixed here: DashboardRendererProps carries [key: string]: any, which puts string into its keyof, so React's PropsWithoutRef always takes the Omit branch, and Omit over an index-signature type keeps only the index signature. Measured with a throwaway probe compiled by the new project:

error TS2322: Type 'keyof DashboardRendererProps' is not assignable to type 'never'.
error TS2322: Type 'string | number' is not assignable to type 'never'.     <- keyof ComponentProps of DashboardRenderer
error TS2322: Type 'any' is not assignable to type 'never'.                 <- that type's ['onWidgetClick']
error TS2322: Type '((widgetId: string | null) => void) | undefined' is not assignable to type 'never'.   <- what the interface still declares

So the interface is right and no JSX call site is held to it. Filed as #4528 — with packages/plugin-list/src/ListView.tsx, the other survivor of the sweep #4422 explicitly left owed, and the note that #4438's guard test is scoped to packages/components/src and structurally cannot see either.

1x TS2571 — vi.importActual('react') answering unknown. The .createContext read was unchecked; it now names the module's type.

2 that are NOT test defects — the packaging gap, filed as #4529. Two suites import @object-ui/plugin-charts subpaths that the package does not publish (its exports map declares "." alone, and neither name is on the barrel); they resolve only through the repo vitest alias. This is #4325's shape one package over, and #4325's own answer — drop the import — does not transfer: normalizeChartSchema is a value the assertions run the emitted schema through, so removing it would not relocate the guarantee, it would replace the test with a restatement of the thing under test. Rather than rewrite test semantics to satisfy tsc (the #4471 discipline) or change another package's public surface from a test-surface card, this PR restates the vitest alias as one narrow, commented paths entry in the new test project, pointing tsc at the very files vitest loads. The comment says to delete it once #4529 is ruled either way, and the PM may prefer to re-scope: #4325's precedent went to "drop the subpath".

Wiring

packages/plugin-dashboard/tsconfig.test.json, mirroring the recently wired siblings (core / app-shell / plugin-list), chained as "type-check": "tsc --noEmit && tsc -p tsconfig.test.json". No tsconfig.typetests.json — that population is closed by #4291.

Two config choices are measured rather than copied:

  • types: ["node"] — load-bearing: dropping node turns five suites red with TS2304: Cannot find name 'global' (they install a global.fetch double). @testing-library/jest-dom is deliberately absent even though 18 suites use its matchers — measured too: removing it changes nothing, because the files that use them import it explicitly and a global augmentation reached by an import applies to the whole program.
  • lib stays at the root's ES2020, unlike plugin-list / plugin-grid / app-shell which needed ES2022 for Array.prototype.at. Nothing here does, so the tests are held to the same lib the shipped source targets.

include names src/**/*.d.ts as well: --listFiles confirms all 47 test files plus src/global.d.ts are in the program (of 1818 files).

The gate consequence, and what an empty table actually does

TEST_DEBT is now {}. I read the gate's code rather than assuming, because "empty" reads like "closed", and it is not:

That reasoning is written into the table's comment and pinned by a new self-test block, which asserts the table is empty and that no package with tests is uncovered (the first alone would be satisfied by deleting a row without paying it), plus three fixture cases for the three behaviours above.

Red-first

Gate, pre-fix (at 932cbcd6b):

✅  test type-check coverage: 40/41 packages compile their tests, 1 declared debt (6 errors outstanding), 0 with a narrow type-assertion project.

Gate, post-fix:

✅  type-check coverage: 44/46 via `type-check`, 1 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.
✅  test type-check coverage: 41/41 packages compile their tests, 0 declared debt (0 errors outstanding), 0 with a narrow type-assertion project.

The wiring is load-bearing — a planted type error in computeMetricDelta.test.ts, run through the package's own script exactly as CI does:

> tsc --noEmit && tsc -p tsconfig.test.json
src/__tests__/computeMetricDelta.test.ts(63,7): error TS2322: Type 'string' is not assignable to type 'number'.
 ELIFECYCLE  Command failed with exit code 2.

Note which half spoke: the build tsc --noEmit passed silently — that is the pre-#4040 state — and only the chained project caught it. Removed immediately after; the file is byte-identical to origin/main.

The ratchet fires — restoring the deleted row while the package is covered:

❌  type-check coverage regressed:
    • @object-ui/plugin-dashboard type-checks its tests now — delete its TEST_DEBT entry so the gap cannot reopen (and close #4118 if the list is empty).

and the new self-test case goes red with it:

FAIL  scripts/__tests__/check-type-check-coverage.test.ts > TEST_DEBT is empty ... > is empty — every package that has tests compiles them
AssertionError: expected [ '@object-ui/plugin-dashboard' ] to deeply equal []

Restored from a sha256-verified copy (c1d1cea2…, matched after restore).

Green / must-not-change

check result
pnpm run type-check (plugin-dashboard, both passes) exit 0
npx vitest run packages/plugin-dashboard/ 47 files, 388 tests passed
npx vitest run scripts/__tests__/check-type-check-coverage.test.ts 37 passed (33 before, +4 new)
node scripts/check-type-check-coverage.mjs exit 0, 41/41
node scripts/check-control-bytes.mjs exit 0 (4243 files)
npx eslint on all changed files exit 0; warnings unchanged, every one on a pre-existing line
check-changeset-presence / -no-major / -fixed exit 0

Changeset: .changeset/plugin-dashboard-test-typecheck.md with empty frontmatter — the gate demanded a file (5 files under a released package's src/), and this releases nothing.

Findings filed, both unassigned: #4528 (forwardRef prop erasure, plugin-dashboard + plugin-list), #4529 (plugin-charts unpublished subpaths).


Generated by Claude Code

…oses at 40/40

`@object-ui/plugin-dashboard` was the last TEST_DEBT entry. Its 47 test files
are now read by a `tsconfig.test.json` chained off `type-check`, and the
registry's table is empty.

Remeasured, not inherited: the ledger declared 6 errors and `tsc -p` reported
14 — wrong in the same direction the whole table was wrong in (i18n 13 -> 103,
react 27 -> 43). Twelve were fixed as type errors in the tests; two were a
packaging gap, filed as #4529.

What the type errors actually were, in both directions:

  - 6x TS2493/TS2352 — `queryDataset` and `find` mocks declared with NO
    parameters, so `mock.calls` typed as empty tuples and every `calls[0][1]`
    the drill and broadcast assertions are built on read out of bounds. Cast,
    so invisible at runtime too. The mocks now declare the call shape
    `DatasetWidget` really uses.
  - 3x TS2322 — two fixtures spelling `options: ['EMEA']`, a UI-side shorthand
    `GlobalFilterSchema` does not declare. Re-spelled to the pair form; the
    shorthand's own pin lives in core's `dashboard-filters.test.ts`, so no
    coverage moved.
  - 2x TS7006 — `onWidgetClick={(id) => …}` inferring `any`, because
    `DashboardRendererProps`' index signature collapses every declared prop
    through `PropsWithoutRef`. Annotated here; the underlying hole is #4528.
  - 1x TS2571 — `vi.importActual('react')` answering `unknown`, so the
    `.createContext` read was unchecked.

No source file was touched.

The gate's TEST_DEBT row is deleted in the same PR, per the ratchet, and its
self-test gains a block stating the terminal condition and what an empty table
does: it does NOT close the population (a declared entry still suppresses 5c —
this gate is declared/reasoned/shrink-only, not "forbidden"), it makes a SILENT
gap impossible, and section 6 still evicts a row whose package graduated.

Red-first:
  - gate pre-fix reported the debt row (39/40, 1 declared debt); post-fix
    41/41, 0 declared debt.
  - planting a type error in a dashboard test left `tsc --noEmit` green and
    turned the chained project red naming the exact line — the new project is
    the only thing that reads these files.
  - restoring the deleted row turns both the gate (section 6) and the new
    self-test case red.

Must-not-change, green both sides: 47 files / 388 tests pass, the gate's other
entries are unmoved, and the self-test suite is green (37 cases).
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 4:58am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-6ngvBCBX.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 156.23KB 42.29KB
fields (index.js) 230.14KB 57.12KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.07KB 17.65KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.95KB 31.53KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 188.40KB 50.10KB
plugin-kanban (index.js) 48.62KB 13.42KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.25KB 7.53KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)

Program note: the gate now prints 41/41 (its own number, used here in preference to the card's 40/40 shorthand) with 0 declared debt. #4040 stays open until this PR lands; the PM will close it with the program summary and evaluate closing objectstack#4118 (the row-tag source issue) at the same time.

Auto-merge armed (squash).


Generated by Claude Code


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants