Skip to content

fix(tests): close the vitest invocation guard's package-cwd hole in the 11 standalone package configs - #5412

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5406-vitest-invocation-guard-coverage
Aug 20, 2026
Merged

fix(tests): close the vitest invocation guard's package-cwd hole in the 11 standalone package configs#5412
os-support-ai merged 2 commits into
mainfrom
claude/issue-5406-vitest-invocation-guard-coverage

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5406

The enumeration, measured rather than inherited

I ran pnpm exec vitest run from every directory carrying a vitest config on pristine origin/main (fd227ea), and classified each config by whether it leads back to the root one. 19 config files exist: the root vitest.config.mts plus 18 others.

routeconfigspackage-cwd run
no config in the package — lookup walks up to the root configpackages/app-shell, packages/mobile, ~30 moreREFUSED
config imports the root config (re-export, mergeConfig, or strip-one-key)packages/core, react, types, components, fields, plugin-dashboard, apps/console, examples/schema-catalog8REFUSED
standalone config that never mentions the root fileplugin-calendar, -charts, -detail, -form, -gantt, -grid, -kanban, -list, -map, -timeline, -view11ACCEPTED

Importing the root config executes its module scope, and the guard call sits there — so route 2 is covered as a side effect of the import, not by anything the package config says. Route 3 never imports the guard module at all, so it never runs.

The card's numbers do not survive measurement

  • It says 17 packages are uncovered. 17 is the count of allpackages/*/vitest.config.ts files (6 root-importing + 11 standalone), not the uncovered ones.
  • It names packages/core and packages/components as uncovered, "none of which re-export the root config". Both do import it and both were measured refused before any change; packages/core/vitest.config.ts is literally two lines re-exporting it.
  • plugin-grid, the third package it names, is genuinely uncovered.

The uncovered set is 11 — exactly what #5313 measured a day earlier, from the same starting point. See the PM note at the bottom.

Why these 11 are the worst place for the hole

They are not near-copies of the root config. Each declares happy-dom + globals + a local vitest.setup.ts (one line: import '@testing-library/jest-dom') and no alias table at all, where the root config maps roughly 40 @object-ui/* specifiers at a sibling package's src/. So a run launched there both collects the package's own files and resolves them differently from CI — the divergent-config false green this guard exists to refuse, arriving through the one door it was documented to have locked.

This also settles the question #5313 left open: a bare run in one of the 11 does not collect console's 22 foreign files. Their configs have no projects array, so vitest falls back to the default **/*.{test,spec}.?(c|m)[jt]s?(x) relative to the package root and collects the package's own suite. That is worse than #3378's shape, not better — the count looks right and the files look right.

Reverse verification — predicted, then observed

Predictions written before running: plugin-grid accepted before / refused after; packages/core refused in both (it imports the root config); packages/app-shell unknown, since whether vitest walks up for a config had to be measured, not assumed.

Before — from packages/plugin-grid:

pnpm exec vitest run src/__tests__/ObjectGrid.exportOptionsKeys.test.ts
RUN v4.1.10 /home/user/objectui-issue-5406/packages/plugin-grid # root is the PACKAGE
Test Files 1 passed (1)
Tests 5 passed (5) # exit 0, no guard output

After — the identical command:

==============================================================================
vitest 调用被拒绝:从包目录跑 vitest 会静默跑错测试集 (objectui#3378)
==============================================================================
vitest root: /home/user/objectui-issue-5406/packages/plugin-grid
仓库根: /home/user/objectui-issue-5406
exit=1

Observed matched predicted on all three, including the unknown: app-shell and mobile were already refused, so the docstring's upward-resolution claim is the one part of it that was true. Sweeping all 21 directories again after the change: 21/21 refused, 0 accepted.

Ablation on the new enforcement test, to show it can actually fail — predicted plain RED (the config texts are its only input, and removing a call can only add a finding):

# guard call deleted from packages/plugin-view/vitest.config.ts
FAIL scripts/__tests__/vitest-invocation-guard.test.ts > leaves no config able to skip the guard
+ "packages/plugin-view/vitest.config.ts",
Tests 1 failed | 29 passed (30)

Restored with git checkout HEAD -- ... (the fix was committed first, so the restore leg has a real restore point); git status clean afterwards and 30/30 green again. No build artifact sits between any edit and the thing under test on either leg: the enforcement test reads the config files as text from disk and vitest transpiles the test per run — nothing on either path resolves through a dist/.

The fix

  • The 11 standalone configs now call the guard themselves. Same effect as route 2, without the import.
  • New repoRootFrom(import.meta.url) in the guard module, instead of eleven hand-written path.resolve(__dirname, '../..'). That literal fails silently when the count is wrong: the resolved directory exists, the comparison still runs, and the guard keeps issuing verdicts computed against the wrong root. A landmark search either finds the directory holding vitest.config.mts or throws. It reads the config'simport.meta.url, not the guard module's, so it holds whether Vite hands the config to Node's native ESM loader or bundles it to a timestamp-*.mjs written alongside — and it adds nothing to the __dirname debt tracked in 28 个 vite/vitest config 用 __dirname,Vite 8 已警告它在未来 major 默认的 configLoader: 'native' 下不受支持 #3592.
  • The docstring and the root config's call-site comment now describe the three routes instead of asserting one of them, and carry the measured evidence.
  • The claim is enforced, not restated. The guard's own test walks every vitest.config.* in the repo and fails on any taking neither route, with a message that spells out the fix and says not to add an exemption. Liveness is pinned too (the walk reaches the root file, both subdirectories and both routes are represented), so it cannot go vacuously green.

One near-miss worth flagging, because it is the same defect class as the bug: the classifier's first spelling was text.includes('vitest.config.mts'), and all 11 standalone configs name the root file in the comment explaining why they do not import it. Every one classified as route 2 and the check went green over exactly the configs it exists to catch. It now matches the import specifier, and that distinction is pinned as its own assertion.

Nothing was exempted, and nothing legitimate broke

The card said to stop and report rather than carve an exemption. Nothing needed one:

  • CI runs pnpm test from the repo root only (ci.yml:435 explicitly rejects turbo run test). No workflow and no root script invokes turbo run test or pnpm --filter PKG test.
  • The 11 configs are not loaded by a root run — the root config's projects array names only apps/console.
  • The test scripts in those 11 packages now fail loudly, exactly like the other ~30 packages already did. That is the guard's documented stance ("until that is decided they fail loudly instead of lying"); their fate is 17 个包各自带 vitest.config.ts 与根 vitest.config.mts 行为分叉:先回答「哪些包真的需要本地配置」 #3240's question, untouched here.
  • The OBJECTUI_VITEST_GUARD=off escape hatch still stands down in a newly-guarded package (verified).
  • No package src/ is touched, no config's test semantics changed, no assertion weakened anywhere, and no skip list added.

Verification, all from the repo root, at 33fc791

gateresult
pnpm exec vitest run scripts/58 files, 1567 passed
pnpm exec vitest run packages/plugin-grid/ packages/plugin-view/98 files, 906 passed — the canonical path for two edited packages
pnpm exec vitest run scripts/__tests__/vitest-invocation-guard.test.ts30 passed (was 26)
pnpm type-check:scriptsexit 0 — the guard's inferred types flow into the .ts test
eslint over all 14 changed files0 errors (7 pre-existing 'path' is defined but never used warnings, present on main, left alone)
check:control-bytes / check:self-import / check:phantom-deps / check:skills-pathsexit 0
check-changeset-presence / check-changeset-no-majorexit 0
check:published-dist (full 275s build)exit 0 — no tooling artifact reaches a tarball

Measured that packages/*/vitest.config.ts sits in no tsc program (tsc --listFiles on both tsconfig.json and tsconfig.test.json: 0 hits), which is why the new import carries no @ts-expect-error — one there would be dead weight today and a TS2578 hazard the moment the file joins an allowJs program.

Files touched

scripts/vitest-invocation-guard.mjs, scripts/__tests__/vitest-invocation-guard.test.ts, vitest.config.mts (comment only), the 11 packages/plugin-*/vitest.config.ts, and one changeset.

Three doc surfaces each stated the guard lives "in vitest.config.mts" — AGENTS.md, QUICK_REFERENCE.md, skills/objectui/guides/project-setup.md. That is now one of twelve call sites, and stating only the root one is the habit that let the hole go unnoticed, so each got a one-clause correction. They use concrete paths rather than a packages/plugin-* glob because quick-reference-commands-4149.test.ts requires every path in a code span to resolve — it caught the glob, which is why the docs name packages/plugin-grid/vitest.config.ts and its ten siblings.

.github/workflows/ci.yml is untouched (#5403 owns it).

For the PM

Generated by Claude Code


Generated by Claude Code

`assertCanonicalVitestInvocation` refuses a package-cwd vitest run because
such a run uses a different config than CI does and can therefore pass a
suite CI would fail. Its docstring claimed every per-package config
re-exports the root one, so no package-level path could skip it.
Measured, by running `pnpm exec vitest run` from every directory carrying a
config: 8 package configs import the root config and were refused, 2
packages carry no config and resolve upward and were refused — and 11
standalone configs (plugin-calendar/-charts/-detail/-form/-gantt/-grid/
-kanban/-list/-map/-timeline/-view) never mention the root file, so the
guard never ran. From packages/plugin-grid, one such run printed
`Test Files 1 passed (1)` / `Tests 5 passed (5)` and exited 0, under a
config with no `@object-ui/*` alias table at all.
- the 11 standalone configs now call the guard themselves, via a new
`repoRootFrom(import.meta.url)` landmark search rather than a
hand-counted `../..` (which fails silently when the count is wrong)
- the guard's docstring and the root config's call-site comment now state
the three routes a config can take, instead of asserting one of them
- the claim is enforced, not restated: the guard's own test walks every
`vitest.config.*` in the repo and fails on any taking neither route
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…one (#5406)
AGENTS.md, QUICK_REFERENCE.md and the project-setup skill guide each stated
that the guard lives "in `vitest.config.mts`". After #5406 that is one of
twelve call sites, and stating only the root one is what let the hole go
unnoticed. Concrete paths rather than a `packages/plugin-*/…` glob, because
`quick-reference-commands-4149.test.ts` requires every path in a code span
to resolve.
Also adds the empty-frontmatter changeset declaring that this publishes
nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-BInyohVQ.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)506.94KB113.61KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.07KB32.77KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.19KB30.20KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.30KB53.06KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)20.08KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)36.10KB12.26KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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.08KB1.53KB
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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

assertCanonicalVitestInvocation does not cover the 17 packages carrying their own vitest.config.ts — contrary to its own docstring

2 participants

@os-support-ai@claude