Skip to content

test(plugin-form): type-check its tests and clear its TEST_DEBT entry (#4040) - #4309

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4040-plugin-form
Aug 11, 2026
Merged

test(plugin-form): type-check its tests and clear its TEST_DEBT entry (#4040)#4309
yinlianghui merged 1 commit into
mainfrom
claude/issue-4040-plugin-form

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4040 — tranche 2, package 2 of 3. One package per PR, per the 裁决 on objectstack-ai/objectstack#4118 (PM, 2026-08-03): 小包先行, core(80)/react(76) 最后; 每包独立 PR; 守卫类 PR 必须附 pre-fix 代码报红的运行记录; TEST_DEBT 条目随包清零, 只减不增.

Measured before / after — re-measured, not read off the registry

errors
declared in TEST_DEBT12 (code-tier)
measured on this branch's base (421c145af), template as-is13 — 0 config-tier + 13 code-tier
after0

Tranche 1 found the registry two tranches stale for plugin-grid (declared 2, measured 4), so tranche 2 re-measures rather than trusts. Here the drift is smaller and in the same direction: 13, not 12, and unusually none of them is config-tier — no TS6059 source leak, no lib gap, no missing types. The template's paths: {} / types / composite: false are still required to keep it that way, they just do not happen to be hiding any error in this package.

Ten of the thirteen are one collision

describe.each over the four sectioned containers is the whole story:

src/recordSwapLoading.test.tsx(108,13): error TS2322: Type 'any' is not assignable to type 'never'.
The intersection 'TabbedFormSchema & SplitFormSchema & DrawerFormSchema & ModalFormSchema'
was reduced to 'never' because property 'formType' has conflicting types in some constituents.

ModalFormProps.schema is a ModalFormSchema, DrawerFormProps.schema a DrawerFormSchema, and so on — each pinned to its own formType literal. A describe.each tuple therefore hands JSX a union of four component types, and JSX resolves a union of components by intersecting their props: ModalFormSchema & DrawerFormSchema & … reduces formType to never, so every schema is rejected — including the correct one for the case actually running. Seven hits in recordSwapLoading.test.tsx, three in discardGuard.test.tsx.

The suites now name the surface the parametrisation really exercises — the same two props against each container — once per file:

typeSectionedFormContainer=React.ComponentType<{schema: any;dataSource?: DataSource}>;

schema is any there and cannot be anything else, which the comment says out loud rather than leaving as a silent cast: assigning React.FC< ModalFormProps > to a shared component type requires the shared schema type to be assignable to ModalFormSchemaandDrawerFormSchemaand the other two at once, and nothing but any satisfies that. It is a fact about these four having no common props base, not a cast papering over a mismatch. To keep the discriminant checked anyway, schemaFor's parameter goes from string to 'modal' | 'drawer' | 'tabbed' | 'split' — so a typo in a case row is now a compile error where before it was a string.

No public type was touched, and none looks wrong: four containers with four schema variants is the correct modelling. The absence of a shared props base is reported as an observation in the tranche report, not fixed here.

The other three each told the compiler less than the code

  • occSave.test.tsx stubbed dataSource.update with three parameters while the contract is update(resource, id, data, opts?: { ifMatch?: string }) (packages/types/src/data.ts:322) — and the assertion under test reads the fourth argument, ds.update.mock.calls[0][3]. vitest records the real arguments whatever the stub declares, so the case passed at runtime while the compiler was told the call tuple has length 3 (TS2493). The stub now declares _opts, so the case checks the option bag it is actually about.
  • deriveMasterDetail.test.ts built a field map as […nine plain fields].map(…).concat([[relation]]). Inference narrowed the nine to { type: string } entries and .concat() then rejected the relation for carrying reference (TS2769), which reads as "the fixture is wrong" when the fixture is exactly right. The entry type is written out.
  • LineItemsPanel.test.tsx had an unread o parameter on an update double (TS6133, from the repo's noUnusedParameters), renamed _o — the convention the sibling create double already uses.

Discrimination proof

1. The new test project can fail (the #3009 third failure mode: a tsconfig.test.json that exists but nothing runs). A provably-false line appended to occSave.test.tsx:

$ npx tsc -p tsconfig.test.json
src/occSave.test.tsx(229,7): error TS2322: Type 'string' is not assignable to type 'number'.
EXIT:2

2. The occSave guard's meaning changed, so it gets a real red run — twice, in both directions.

Types. Reverting only the stub's fourth parameter brings the hole straight back, at the assertion that is the case's entire point:

$ npx tsc -p tsconfig.test.json
src/occSave.test.tsx(120,36): error TS2493: Tuple type '[_o: string, _id: string, d: any]' of length '3' has no element at index '3'.
EXIT:2

Runtime. And the assertion discriminates — reverting the source so the unguarded path always sends an options object ({ ifMatch } instead of ifMatch ? { ifMatch } : undefined, src/occSave.tsx:146) turns it red for the right reason:

$ npx vitest run packages/plugin-form/src/occSave.test.tsx --maxWorkers=2
× record without updated_at degrades to an unguarded write (old behaviour)
AssertionError: expected { ifMatch: undefined } to be undefined
Tests 1 failed | 7 passed (8)

Both reverted; neither is in the diff.

3. The describe.each refactor did not collapse the case matrix. Replacing an inline as const tuple with a typed constant is exactly the kind of change that can silently drop rows, and the type error it was fixing would not notice. Verbatim from the verbose run:

$ npx vitest run packages/plugin-form/src/recordSwapLoading.test.tsx --reporter=verbose
✓ ModalForm — recordId swap > does not keep showing the previous record while the new one loads
✓ ModalForm — recordId swap > ignores a stale response that lands after a newer one
✓ DrawerForm — recordId swap > does not keep showing the previous record while the new one loads
✓ DrawerForm — recordId swap > ignores a stale response that lands after a newer one
✓ TabbedForm — recordId swap > does not keep showing the previous record while the new one loads
✓ TabbedForm — recordId swap > ignores a stale response that lands after a newer one
✓ SplitForm — recordId swap > does not keep showing the previous record while the new one loads
✓ SplitForm — recordId swap > ignores a stale response that lands after a newer one
✓ ModalForm — swapping records clears the unsaved-input guard > stops blocking unload once the previous record is gone
✓ DrawerForm — swapping records clears the unsaved-input guard > stops blocking unload once the previous record is gone
Tests 11 passed (11)

All four containers still run, and the two-container overlay suite still runs two.

Verification

$ pnpm --filter @object-ui/plugin-form type-check
> tsc --noEmit && tsc -p tsconfig.typetests.json && tsc -p tsconfig.test.json
EXIT:0
$ npx vitest run packages/plugin-form --maxWorkers=2 # repo root, per objectui#3378
Test Files 40 passed (40)
Tests 413 passed (413)
$ pnpm --filter @object-ui/plugin-form lint
✖ 541 problems (0 errors, 541 warnings) # all pre-existing warnings
$ node scripts/check-type-check-coverage.mjs
type-check coverage: 43/45 via `type-check`, 1 via their own build, 0 known-broken (0 errors outstanding), 1 not compiled.
test type-check coverage: 28/40 packages compile their tests, 12 declared debt (225 errors outstanding), 10 with a narrow type-assertion project.
$ node scripts/check-changeset-presence.mjs
5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s).
Every one of them has an EMPTY frontmatter — declared as releasing nothing.

No consumer sweep is reported, and that is not an omission: nothing outside src/**/*.test.* changed apart from the type-check script and the new project file, so there is no exported type for a downstream package to be checked against.

TEST_DEBT shrinks by exactly this package's line; no other entry is touched. The tsconfig.typetests.json next door stays chained — its one file is now read by both projects, which costs one extra compile and keeps the narrow project's reasoning where it was written.

Changeset

Empty frontmatter — check-changeset-presence.mjs arbitrated it, and it is a pass rather than a workaround: only test sources changed, so this releases nothing.


Generated by Claude Code

…#4040)
Tranche 2, package 2 of 3. `tsconfig.test.json` per the #3032 template, chained
from `type-check`; TEST_DEBT declared 12, measured 13 at this branch point, now 0.
Ten of the thirteen were one collision: `describe.each` over the four sectioned
form containers makes JSX intersect their prop types, reducing `formType` to
`never`. The parametrised suites now name the surface they exercise once, and
pin `formType` to a literal union instead of `string`.
The other three were stubs/fixtures declaring less than the code: the
`dataSource.update` double omitted the `opts` parameter the assertion reads
(`mock.calls[0][3]` against a declared 3-tuple), a `.map().concat()` field map
narrowed away the relation field's `reference`, and an unread `o` parameter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 12:40pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)29.5 KB350 KB
Entry fileindex-UCvw2nUF.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.27KB3.23KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)488.62KB108.26KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)16.38KB5.47KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.73KB17.54KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)121.07KB31.39KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.98KB59.76KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)109.93KB26.65KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.60KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 13:58
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 6044d13Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4040-plugin-form branch August 11, 2026 14:00
Sign up for freeto 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

@yinlianghui@litant-dev-proton