Skip to content

test(plugin-report): serve the dimension-metadata probe from a double, not the network (#5225) - #5283

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5225-plugin-report-network-escape
Aug 19, 2026
Merged

test(plugin-report): serve the dimension-metadata probe from a double, not the network (#5225)#5283
os-support-ai merged 1 commit into
mainfrom
claude/issue-5225-plugin-report-network-escape

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5225

Test-only. Product code is untouched.

Premise re-checked on current main first

The card measured on a feature branch that has since merged (PR #5224), so the recheck was re-run on origin/main @ 41f498bcb before anything else:

pnpm exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx 2>&1 | grep -c ECONNREFUSED
-> 16 Tests 42 passed (42)

Still there. The per-file bisect was re-run too — over all 13 files in the package, not the 5 the card listed (the package has grown since): the escape is still confined to DatasetReportRenderer.test.tsx, every other file is 0.

DatasetReportRenderer.test.tsx -> ECONNREFUSED x16 | 42 passed
DatasetReportRenderer.chartChrome.test.tsx -> x0 | 19 passed
DatasetReportRenderer.chartMeasureLabel.test.tsx-> x0 | 10 passed
DatasetReportRenderer.chartNullCategory.test.tsx-> x0 | 10 passed
DatasetReportRenderer.localSelectI18n.test.tsx -> x0 | 7 passed
DatasetReportRenderer.measureLocale.test.tsx -> x0 | 10 passed
DatasetReportRenderer.rejectedFilterAlias.test.tsx -> x0 | 9 passed
ReportRenderer.test.tsx -> x0 | 3 passed
ReportRendererDispatcher.test.tsx -> x0 | 7 passed
ReportViewer.cells.test.tsx -> x0 | 7 passed
ReportViewer.printButtonSemantics.test.tsx -> x0 | 5 passed
report-aggregation-coverage.test.ts -> x0 | 7 passed
report-spec-parity.test.tsx -> x0 | 13 passed

16 is the grep line count; the real figure is 8 connection attempts — each failure prints two matching lines (Error: connect ECONNREFUSED … and code: 'ECONNREFUSED',).

Which module opens the connection

The card's three negative claims were re-verified here, each with a counter-probe so the zero is a reading rather than a typo:

claimhitscounter-probe
no fetch( in packages/plugin-report/src0DatasetReportRenderer → 111
no literal 3000 in packages/plugin-report/src0dataset → 13 files
neither in the three root setup files0import → 4 / 7 / 10

The caller was then located with a net.Socket.prototype.connect preload probe, which prints the JS stack under the TCP connect:

at loadObjectSchema packages/react/src/hooks/useDatasetDimensionLabels.ts:144
at loadDimensionFieldMeta packages/core/src/utils/chart-series.ts:1178
at GlobalWindow.fetch happy-dom/lib/window/BrowserWindow.js:2203
GET http://localhost:3000/api/v1/meta/object/task

So the path is:

DatasetReportRenderer (tabular / matrix / chart branches)
-> useDatasetDimensionLabels packages/plugin-report/src/useDatasetDimensionLabels.ts (a re-export)
-> useDatasetDimensionMeta packages/react/src/hooks/useDatasetDimensionLabels.ts
`const doFetch = apiFetch ?? fetch` <-- the escape
-> loadDimensionFieldMeta packages/core/src/utils/chart-series.ts
GET /api/v1/meta/object/:object

useDatasetDimensionMeta reads the host's authenticated apiFetch off SchemaRendererContext and, with no SchemaRendererProvider in the tree, degrades to the globalfetch on purpose — objectui#4121 property 1, so a standalone embed keeps rendering instead of crashing. Under happy-dom that global fetch is a real HTTP client, and vitest's happy-dom environment defaults the document URL to http://localhost:3000 (vitest/dist/chunks/index.DC7d2Pf8.js: url: happyDOM.url || "http://localhost:3000"), so the relative /api/v1/... resolves to a live request. That is where the 3000 comes from — no repo file names it.

Because the aliases in vitest.config.mts resolve @object-ui/react and @object-ui/core to their src, the stack above is source, not dist — nothing here can be a stale-build artefact.

Why the test's mock did not intercept it

The metadata read is a second data channel. dataSource.queryDataset — the prop double this file passes — serves the report ROWS; the dimension-label metadata never goes through dataSource at all. Only the cases whose mocked result carries object reach it, since that field is what the hook keys on: 8 of the file's renders (task x4, deal x2, opportunity x2), which is exactly the 8 attempts.

The read is best-effort — a failure leaves the rows exactly as the server sent them — which is why 42 tests were green while the request never once succeeded.

The fix

A recording double, the shape objectui#3339 and #4106 settled on and the one this package's own DatasetReportRenderer.localSelectI18n.test.tsx already uses. Explicitly not a blanket network stub, not a global error sink:

  • it records every URL it is handed and answers only /api/v1/meta/object/:name;
  • afterEach asserts the recorded set contains no non-metadata URL, so an escape to some other endpoint becomes a red test in this file instead of vanishing into the hook's catch {};
  • the default document declares no option-bearing fields, so deriveDimensionLabelMaps resolves nothing and relabelDimensions returns the rows by identity — byte-identical to what the failing request produced. No pre-existing assertion changes meaning;
  • the probe's shape, previously asserted by nobody, is pinned by three new tests: which object is probed (once, over the metadata route, with accept: application/json), that a result declaring no object probes nothing at all, and that a resolved document actually reaches the rendered cells — the success path that had never executed in this suite.

One ordering detail worth keeping: the hook unmounts (cleanup()) beforevi.unstubAllGlobals(). Vitest runs afterEach in reverse registration order, so the setup file's RTL cleanup runs after this file's hook; unstubbing first left the tree mounted with the real global back in place and a settling metadata effect escaped again — measured, one run in six leaked a single attempt.

Post-fix status of all 42 tests

All 42 pre-existing tests still pass, unchanged — none of them depended on the swallowed failure. Plus the 3 new pins: Tests 45 passed (45), ECONNREFUSED 0, stable across 8 consecutive runs. Package-wide: Test Files 13 passed (13) · Tests 152 passed (152) · ECONNREFUSED 0.

Reverse verification

Predicted before running, with the pre-fix file restored (git checkout origin/main -- …): 16 ECONNREFUSED lines, 42 passed, 0 failed — the direction here is not red/green, because the defect is a swallowed side effect rather than a wrong assertion; the observable is the connection count and the missing pins.

Observed: 16 / Tests 42 passed (42) / 0 occurrences of the new describe block. Predicted == observed on all three. The fix was then restored from the branch and proved byte-identical (git hash-object == git rev-parse HEAD:… == 4e522c49e).

Family: same root cause as #4106, not a fourth one

The shared mechanism across all three is the same and worth stating once: a hook that degrades to the global fetch outside a host provider, plus happy-dom's real fetch on a localhost:3000 default origin, plus a best-effort catch.

Why it is not merely noise

In this repo's shared containers a dev server on port 3000 is routine. If one is up, those 8 attempts connect — to whatever another agent happens to be running — and the tests' rendered labels change without a line of them changing (the double's third pin shows exactly how: a resolved document rewrites dimension cells). That is the argument for locating and answering the request rather than muting it.

Out of scope, filed

packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx still escapes through the same hook — 10 ECONNREFUSED lines (5 attempts) for showcase_task x3, showcase_deal x2, invoices x1, with 52 tests green. Different package, outside this card's write surface: filed as #5280, unassigned, not touched here.

Verification

At be8cbe607:

  • pnpm exec vitest run packages/plugin-report — 13 files / 152 tests passed, 0 ECONNREFUSED
  • pnpm --filter '@object-ui/plugin-report^...' build — dependency closure, exit 0
  • pnpm --filter @object-ui/plugin-report type-checktsc --noEmit && tsc -p tsconfig.test.json, exit 0
  • pnpm --filter @object-ui/plugin-report lint — 0 errors (85 pre-existing warnings; the two in the edited file are the pre-existing makeSourceanys at lines 37/41)
  • node scripts/check-control-bytes.mjs — OK (4664 files), plus a direct control-byte grep over the two changed files
  • node scripts/check-changeset-presence.mjs — satisfied by an empty-frontmatter changeset (the change is under a guarded src/, and the gate's own documented answer for a src/__tests__/**-only change)
  • node scripts/check-changeset-no-major.mjs — no major

Docs untouched, so #5136's content/docs/core/report-schema.mdx is unaffected: this PR changes nothing about what the renderer reads.


Generated by Claude Code

…, not the network (#5225)
`DatasetReportRenderer.test.tsx` made 8 real TCP connections per run to
`http://localhost:3000` (16 ECONNREFUSED lines - two per attempt), while
`packages/plugin-report/src` contains no `fetch(` and no literal `3000`.
Traced with a `net.Socket.prototype.connect` probe, the escape is:
DatasetReportRenderer (tabular / matrix / chart branches)
-> useDatasetDimensionLabels re-export of @object-ui/react's hook
-> useDatasetDimensionMeta packages/react/src/hooks/useDatasetDimensionLabels.ts:144
-> `const doFetch = apiFetch ?? fetch`
-> loadDimensionFieldMeta packages/core/src/utils/chart-series.ts:1178
GET /api/v1/meta/object/:object
With no `SchemaRendererProvider` in the tree the hook has no host `apiFetch`
and degrades to the GLOBAL fetch - deliberate and pinned (objectui#4121).
Under happy-dom that global fetch is a real HTTP client, and vitest's
happy-dom environment defaults the document URL to `http://localhost:3000`,
so the relative `/api/v1/...` resolved to a live request.
Why the test's own mock never intercepted it: the metadata read is a SECOND
data channel. `dataSource.queryDataset` serves the report ROWS; the dimension
metadata never goes through `dataSource`. Only the 8 cases whose mocked result
carries `object` reach it - that field is what the hook keys on. The read is
best-effort (`catch {}`), which is why 42 tests stayed green.
Answer it from a RECORDING double, the shape objectui#3339 / #4106 settled on
and this package's own `DatasetReportRenderer.localSelectI18n.test.tsx`
already uses. Not a blanket network stub: it records every URL, `afterEach`
fails on any non-metadata route, and the probe's previously-unasserted shape
is now pinned by three new tests. The default document declares no
option-bearing fields, so `deriveDimensionLabelMaps` resolves nothing and the
rows are returned by identity - byte-identical to what the failing request
produced. All 42 pre-existing tests still pass unchanged.
The double unmounts before restoring the real `fetch`: vitest runs `afterEach`
in reverse registration order, so the setup file's RTL cleanup runs after this
file's hook, and unstubbing first left the tree mounted with the real global
back in place. Measured - one run in six leaked a single attempt that way.
Product code is untouched. Same root cause as #4106 (whose fix landed only in
`packages/plugin-charts`, before #4389 extracted the call site into the shared
hook), not a fourth distinct one. `packages/plugin-dashboard`'s
`DatasetWidget.test.tsx` still escapes through the same hook - filed as #5280,
out of this card's scope.
Fixes#5225
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-D6ygOyh8.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.33KB7.05KB
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)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.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.88KB
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)506.27KB113.31KB
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)29.43KB7.15KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
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)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.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
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.04KB32.75KB
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)123.77KB30.07KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.27KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)19.96KB6.56KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)42.84KB11.77KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.34KB20.61KB
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.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)31.56KB10.70KB
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

@os-support-ai
os-support-ai marked this pull request as ready for review August 19, 2026 00:51
@os-support-ai
os-support-ai added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 162c4aaAug 19, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5225-plugin-report-network-escape branch August 19, 2026 00:52
os-support-ai pushed a commit that referenced this pull request Aug 19, 2026
…ork escape (#5280)
DatasetWidget.test.tsx's dimension-metadata probe (useDatasetDimensionMeta's
apiFetch ?? fetch fallback) escaped to the real network — 5 live connection
attempts per run, best-effort-swallowed so all 52 tests stayed green. Same
root cause as #5225 (packages/plugin-report); ports the same fix shape from
its reference PR #5283: a recording double answers the metadata route with
an option-free payload, afterEach fails on any non-metadata escape, and
three new tests pin the probe's own request shape. cleanup() runs before
vi.unstubAllGlobals() in the same afterEach to avoid the reverse-registration
flake #5225 measured (1/6 leaked attempts).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
os-support-ai pushed a commit that referenced this pull request Aug 19, 2026
…data network escape (#5299)
DashboardGridLayout.datasetPath.test.tsx's 'options.data provider widget'
negative-control test made a real network call — ECONNREFUSED on port 3000,
1 attempt per isolated run, best-effort-swallowed so all 13 tests stayed
green. `-t` bisected across all 13 cases in isolation: only this one test
escapes (3/3); the two #4613 legacy-retired-widget candidates the card named
do NOT escape alone (0/1 each), so useDatasetDimensionMeta's `!object` guard
was never the site, as the card's own re-derived reading suspected.
Root cause: the options.data.provider:'object' widget maps to component type
`object-chart`, whose ObjectChart.tsx still carries its own inline
`apiFetch ?? fetch` category-color probe (the ORIGINAL #4106 defect site,
never migrated to the useDatasetDimensionMeta hook #4389 extracted for the
dataset-bound path). With no SchemaRendererProvider in this file's render
tree, that probe degrades to the real global `fetch`.
Ports the same fix shape #5225's reference PR #5283 established and #5280
(PR #5300) ported: a recording double answers the metadata route, afterEach
fails on any non-metadata escape, and cleanup() runs before
vi.unstubAllGlobals() in the same afterEach to avoid the reverse-registration
flake #5280 measured. One new pin test asserts the probe's own request shape
— exactly one call to /api/v1/meta/object/invoices.
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
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.

plugin-report suite escapes to the real network on 127.0.0.1:3000 (ECONNREFUSED noise, same class as #3339 / #4106 / #4688)

1 participant

@os-support-ai