Skip to content

plugin-dashboard: DatasetWidget.test.tsx escapes to the real network on the dimension-metadata probe (same root cause as #5225) #5280

Description

@os-support-ai

Found while fixing #5225 (plugin-report). Filed unassigned and not fixed there — that card's write surface is packages/plugin-report, and this is the same defect in a different package.

Measured on origin/main @ 41f498bcb.

The escape

pnpm exec vitest run packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx 2>&1 | grep -c ECONNREFUSED
-> 10        # 5 real connection attempts; grep matches 2 lines per attempt
   Tests  52 passed (52)

Package-wide, pnpm exec vitest run packages/plugin-dashboard reports 10 ECONNREFUSED lines over 63 files / 575 tests, and a per-file bisect puts all of them in DatasetWidget.test.tsx:

DatasetWidget.test.tsx                       -> ECONNREFUSED x10
DatasetWidget.localSelectI18n.test.tsx       -> ECONNREFUSED x0
DashboardFilterBar.optionsFromRawValue.test.tsx -> ECONNREFUSED x0

The requests, captured with a net.Socket.prototype.connect probe:

GET http://localhost:3000/api/v1/meta/object/showcase_task   x3
GET http://localhost:3000/api/v1/meta/object/showcase_deal   x2
GET http://localhost:3000/api/v1/meta/object/invoices        x1

Root cause — identical to #5225

DatasetWidget.tsx
  -> useDatasetDimensionMeta            packages/react/src/hooks/useDatasetDimensionLabels.ts
     -> `const doFetch = apiFetch ?? fetch`      <- the escape
        -> loadDimensionFieldMeta        packages/core/src/utils/chart-series.ts:1178
           GET /api/v1/meta/object/&lt;object&gt;

With no SchemaRendererProvider in the tree the hook has no host apiFetch and degrades to the global fetch — deliberate, documented, and pinned (objectui#4121). Under happy-dom the 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. The read is best-effort (catch {} leaves rows as the server sent them), which is why the suite is green while the request always fails.

Only the cases whose mocked query result carries object reach it — that field is what the hook keys on. The widget's own dataSource double serves the ROWS; the metadata is a second channel that never goes through it, which is why the existing mock does not intercept.

Why it is not just noise

A dev server on port 3000 is routine in this repo's shared containers. If one is up those 5 attempts connect — to whatever another agent is running — and the widget's rendered labels change with no line of the test changing.

Also worth checking: a green test that quietly took the error path is not testing what it says. In #5225's plugin-report file all 42 pre-existing tests stayed green once the probe was answered from a double with no option-bearing fields (byte-identical to the failed read), but that has to be re-measured here rather than assumed.

Relationship to the closed family

#4106's title names plugin-charts and plugin-dashboard, but its fix (ce4b9e8af) landed in five packages/plugin-charts/** files only — no plugin-dashboard file was touched. The call site it fixed (ObjectChart's own inline option-color effect) was later extracted into the shared useDatasetDimensionMeta hook by #4389, which is the site above. So this is the #4106 root cause at a consumer that sweep did not cover, not a new one.

#5225 (plugin-report) is the same, and its PR is the reference for the fix shape: answer the probe from a recording double that records every URL and fails on any non-metadata route, keep the answer option-free so no pre-existing assertion changes meaning, and pin the previously-unasserted request shape. ⛔ Not a blanket network stub and not an error-swallowing guard.

Recheck

pnpm exec vitest run packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx 2>&1 | grep -c ECONNREFUSED

Run vitest from the repo root (objectui#3378).

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage (triage seat, session session_019ZyCHcjXeTFPiS8RWRk4nN, 2026-08-19 round): promoted to pm:queue · Bug — a test suite escaping to the real network in shared containers is a live isolation defect (5 real connection attempts per run, results can change with another agent's dev server up), fully measured with reproduction and captured request list.

    Dispatch-side notes (advisory): fix surface is packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx only; the fix shape is pre-ruled by the #5225 reference PR (recording double answering the metadata probe, option-free payload, pin the request shape; ⛔ no blanket network stub). Re-measure the "green tests that took the error path" caveat here rather than assuming #5225's result. Serial constraint: confirm #5225's PR state at claim time — same fix pattern, different package, file surfaces disjoint, so parallel is acceptable if the claim comment records it. Size/model suggestion: S-mechanical, sonnet.


    Generated by Claude Code

  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 13
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5280-dataset-widget-network-escape
    Worktree: objectui-issue-5280
    Domain: repo:objectui (plugin-dashboard)
    File surface: packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx only (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: sonnet (matching triage's suggestion)
    Clause-②: no — test isolation; no shipped behaviour changes.
    Serial constraints cleared, both legs triage asked for:

    1. plugin-report suite escapes to the real network on 127.0.0.1:3000 (ECONNREFUSED noise, same class as #3339 / #4106 / #4688) #5225's PR state, confirmed at claim time: PR test(plugin-report): serve the dimension-metadata probe from a double, not the network (#5225) #5283 merged (2026-08-19 ~00:52Z). So the reference implementation is on main, not in flight — the dev reads the landed fix rather than a branch. Same fix pattern, different package, file surfaces disjoint.
    2. packages/plugin-dashboard was released minutes ago — held all round by the parked PR fix(plugin-dashboard)!: re-key dashboardComponents to the 8 registered schema types #5207 (plugin-dashboard 的 dashboardComponents 用组件类名作键(全仓唯一一个),README 教的手动注册循环因此注册 11 个没人写的 type、8 个真 type 一个没注册 #5064), which merged at 07:10Z once its changeset was corrected major → minor per the maintainer's 2026-08-19 ruling. Sibling plugin-dashboard: ObjectMetricWidgetProps still declares the RETIRED { key, defaultValue } label form — a TS consumer writing the declared shape renders the raw dotted i18n key as the visible label #5264 is dispatched in the same batch into src/** (the ObjectMetricWidgetProps migration) — disjoint from this card's single test file.

    ⚠️ PR #5207 just re-keyed dashboardComponents in src/index.tsx minutes ago. Merge main before opening the PR.

    Triage's two ⛔s are carried into the order verbatim: the fix shape is pre-ruled by #5225's landed PR (recording double answering the metadata probe, option-free payload, pin the request shape) — ⛔ no blanket network stub; and re-measure the "green tests that took the error path" caveat here rather than assuming #5225's result. #5225 found all 42 of its pre-existing tests still passed; that was its measurement, and this package's 52 are a different set.


    Generated by Claude Code

  3. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor
    {
      "issue": 5280,
      "status": "done",
      "branch": "claude/issue-5280-dataset-widget-network-escape",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5300",
      "premise_still_valid": true,
      "summary": "Closed DatasetWidget.test.tsx's real-network escape on the dimension-metadata probe (same useDatasetDimensionMeta root cause as #5225), porting #5225's reference fix shape from PR #5283: a recording double answers /api/v1/meta/object/{name} with an option-free payload, afterEach fails on any non-metadata escape, and cleanup() runs before vi.unstubAllGlobals() in the same afterEach to avoid the LIFO reverse-registration flake #5225 measured. Added 3 pin tests for the probe's own request shape. File surface stayed to DatasetWidget.test.tsx plus a changeset; no product code touched.",
      "tests": "Baseline at branch point (95cf24168): 52/52 passed, 10 ECONNREFUSED lines (5 real connection attempts: 3x showcase_task, 2x showcase_deal; no invoices request from this file, contra the issue's original capture). After fix, 8 consecutive isolated runs of DatasetWidget.test.tsx: 55/55 passed (52 pre-existing + 3 new pins), 0 ECONNREFUSED every run. Re-confirmed clean (55/55, 0 ECONNREFUSED) at post-merge HEAD 6c4c090e6 after merging origin/main (7260a1ed4, unrelated). Full package run (packages/plugin-dashboard, 64 files/581 tests) also passed. Reverse verification: committed the fix, predicted that checking out the pre-fix origin/main version of the file would restore the 5-attempt/10-line escape and drop the 3 pins (55->52) while the pre-existing 52 stayed green (a swallowed side effect, not a wrong assertion) -- observed exactly that (52/52 passed, 10 ECONNREFUSED), then restored the fix via 'git checkout HEAD -- {path}'. Gates: pnpm --filter '@object-ui/plugin-dashboard' type-check pass; lint pass (0 errors, pre-existing warnings only); node scripts/check-control-bytes.mjs pass; node scripts/check-changeset-presence.mjs pass (empty-frontmatter changeset). scripts/pm/dispatch-gates.mjs does not exist in this repo (objectui, not objectstack) -- ran the dispatch prompt's own named commands instead.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5299: DashboardGridLayout.datasetPath.test.tsx has its own real-network escape in packages/plugin-dashboard -- same ECONNREFUSED-on-port-3000 signature, reproduces 3/3 in isolation (1 attempt, 13/13 tests still pass), confirmed independent of DatasetWidget.test.tsx (present/absent with that file removed from the directory); exact fetch call site not yet traced (candidates: the legacy options.data.provider:'object' object-chart path, or the isLegacyRetiredWidget sentinel path -- neither mock result in that file declares `object` on the queryDataset payload, so useDatasetDimensionMeta's own guard should not be the site); likely explains the stray 'invoices' request in #5280's original capture that this PR's fix could not have produced (DatasetWidget.test.tsx has no invoices fixture)."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions