Repository navigation
plugin-report suite escapes to the real network on 127.0.0.1:3000 (ECONNREFUSED noise, same class as #3339 / #4106 / #4688) #5225
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorAuthorMore actionsTriage: promoted to
pm:queue(type Bug already set by filer — kept). A unit suite making 16 real TCP attempts per run is a test-isolation defect with a proven family history (#3339 / #4106 / #4688, all fixed), and the "green through the error path" observation means some of the 42 assertions may not test what they claim. First step per the card: establish which dependency's render path issues the fetch before assuming any of the three closed root causes.Size/model suggestion: M (diagnosis-first),
mode:subagent.
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 12 (refill)
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-5225-plugin-report-network-escape
Worktree:objectui-issue-5225
Domain:repo:objectui(plugin-report)
File surface:packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsxand whatever the located escape path turns out to require — declare the widened surface before touching it (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Clause-②: no — test isolation; no shipped behaviour changes.
Serial constraints cleared:packages/plugin-reportis free — no open PR and no in-flight claim touches it. The four other in-flight cards are inplugin-view,app-shell/src/providers,packages/components, and.github/workflows. Sibling #5136 is dispatched in the same batch and only reads the report renderer to describe it (docs-only write surface,content/docs/core/report-schema.mdx) — no write overlap.Stale-premise check: the card measured on a feature branch (
claude/issue-5137-report-filter-alias@e9a7dfb36, merge-baseorigin/main@7d900dee6) and asserts the defect is pre-existing. That branch has since merged (PR5224). So the dev must re-measure on currentorigin/mainwith the card's own recheck command before doing anything else — a card whose only measurement came from a since-merged branch is exactly the shape that goes stale silently.
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 os-support-ai commented
on Aug 19, 2026 CollaboratorAuthorMore actions{ "issue": 5225, "status": "done", "branch": "claude/issue-5225-plugin-report-network-escape", "pr": "https://github.com/objectstack-ai/objectui/pull/5283", "premise_still_valid": true, "summary": "Re-measured on current origin/main @ 41f498bcb first: the escape is still live (16 ECONNREFUSED grep lines = 8 real connection attempts, 42 passed), and a re-run bisect over ALL 13 files in the package (the card listed 5; the package has grown) still confines it to DatasetReportRenderer.test.tsx. Located the caller with a net.Socket.prototype.connect preload probe: DatasetReportRenderer -> useDatasetDimensionLabels (a re-export) -> @object-ui/react's useDatasetDimensionMeta at packages/react/src/hooks/useDatasetDimensionLabels.ts:144, whose `const doFetch = apiFetch ?? fetch` degrades to the GLOBAL fetch with no SchemaRendererProvider in the tree (deliberate, objectui#4121), issuing GET /api/v1/meta/object/:object through core's loadDimensionFieldMeta. happy-dom's fetch is a real HTTP client and vitest's happy-dom env defaults the document URL to http://localhost:3000, so the relative path resolved to a live request; no repo file names 3000. The test's prop mock never intercepted it because the metadata read is a SECOND channel that never goes through dataSource.queryDataset - only the 8 renders whose mocked result carries `object` reach it. Fixed by answering it from a RECORDING double (the shape #3339/#4106 settled on, and the one this package's own localSelectI18n test already uses): it records every URL, afterEach fails on any non-metadata route, the default document declares no option-bearing fields so behaviour is byte-identical to the failed read, and three new tests pin the previously-unasserted request shape plus the success path that had never executed. Product code untouched. Same root cause as #4106 (whose fix landed only in plugin-charts, before #4389 extracted the call site into the shared hook), NOT a fourth distinct one.", "tests": "All at final commit be8cbe607 (working tree clean, == pushed head). (1) TARGET FILE: `pnpm exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` -> `Tests 45 passed (45)`, ECONNREFUSED 0, repeated 8/8 consecutive runs all 0 (an earlier variant leaked 1 attempt in 1 run of 6 - fixed by unmounting via cleanup() BEFORE vi.unstubAllGlobals(), since vitest runs afterEach in reverse registration order and the setup file's RTL cleanup would otherwise run after the unstub). ALL 42 PRE-EXISTING TESTS STILL PASS UNCHANGED - none depended on the swallowed failure; 45 = 42 + 3 new pins. (2) PACKAGE: `pnpm exec vitest run packages/plugin-report` -> `Test Files 13 passed (13) / Tests 152 passed (152)`, ECONNREFUSED 0. (3) REVERSE VERIFICATION, predictions recorded before the run: restoring the pre-fix file (git checkout origin/main -- path) should give 16 ECONNREFUSED lines, 42 passed, 0 FAILED - the direction here is NOT red/green, because the defect is a swallowed side effect, not a wrong assertion; the observable is the connection count and the disappearance of the new pins. OBSERVED: exactly 16 / `Tests 42 passed (42)` / 0 occurrences of the new describe. Predicted == observed on all three. Fix then restored from the branch and proved byte-identical (git hash-object == git rev-parse HEAD:path == 4e522c49e). NO REBUILD WAS NEEDED and none was done, and that is verified rather than assumed: vitest.config.mts aliases @object-ui/react and @object-ui/core to their src/, and the captured stack frames are `packages/react/src/hooks/useDatasetDimensionLabels.ts:144` / `packages/core/src/utils/chart-series.ts:1178` - source paths, so no dist/ artefact can be stale here. (4) GATES: `pnpm --filter '@object-ui/plugin-report^...' build` (dependency closure, run FIRST in the fresh worktree) exit 0; `pnpm --filter @object-ui/plugin-report type-check` exit 0 with the script echoed (`tsc --noEmit && tsc -p tsconfig.test.json`) so it is not a zero-match silent pass; `pnpm --filter @object-ui/plugin-report lint` 0 errors (85 pre-existing warnings, the 2 in the edited file are the pre-existing makeSource `any`s at lines 37/41); `node scripts/check-control-bytes.mjs` OK (4664 tracked files) plus a direct `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over both changed files (no hits); `node scripts/check-changeset-presence.mjs` PASS via an empty-frontmatter changeset (the gate's own documented answer for a src/__tests__-only change; it is under a guarded src/); `node scripts/check-changeset-no-major.mjs` PASS. objectui has no scripts/pm/dispatch-gates.mjs, so the gate union was derived from the changed paths by hand: exactly 2 files, one test file and one changeset, no governed surface touched.", "open_questions": [], "out_of_scope_findings": [ "filed as #5280: packages/plugin-dashboard/src/__tests__/DatasetWidget.test.tsx escapes through the SAME hook - 10 ECONNREFUSED lines (5 attempts: showcase_task x3, showcase_deal x2, invoices x1) with 52 tests green. Searched open issues for duplicates first; none. Filed unassigned, no label (concrete defect, left for PM triage). Not fixed here: different package, outside this card's declared write surface." ] }Draft PR: #5283 (head
be8cbe607, 2 files, +181/-2). Bot labels settled toplugin,tests— objectui has noskip-changesetmechanism and this change carries an empty-frontmatter changeset instead. Worktreeobjectui-issue-5225removed (no--forceneeded). Reporting at draft-PR time per the standing clause; CI convergence is the PM's read.
Generated by Claude Code
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorAuthorMore actionsACCEPT — PR #5283 flipped ready, enqueued, merged. All 21 checks resolved: 19 success, 2 skipped by design. Two files: the test and an empty-frontmatter changeset. Product code untouched.
The card's second question is answered, and the answer is the reassuring one: all 42 pre-existing tests still pass unchanged, so none of them depended on the swallowed failure. 45 = 42 + 3 new pins, one of which covers a success path that had never executed before.
Why the escape was hard to find, and why grep couldn't do it
The card recorded that
plugin-report/srccontains nofetch(and no literal3000— both true, and both irrelevant, which is exactly why this needed a probe rather than a search:- The caller is
@object-ui/react'suseDatasetDimensionMeta(useDatasetDimensionLabels.ts:144), reached via a re-export. Itsconst doFetch = apiFetch ?? fetchdegrades to the global fetch when there is noSchemaRendererProviderin the tree — deliberate, per DatasetWidget's option-color / dimension-label probe reads the globalfetch, bypassing the host'sapiFetchchannel (the plugin-dashboard twin of #4114) #4121. - The port comes from the test environment, not the repo. happy-dom's
fetchis a real HTTP client, and vitest's happy-dom env defaults the document URL tohttp://localhost:3000, so a relative/api/v1/meta/object/:objectresolves to a live request. No repo file names 3000 because nothing has to. - The prop mock never intercepted it because the metadata read is a second channel that never goes through
dataSource.queryDataset— only the 8 renders whose mocked result carriesobjectreach it.
Located with a
net.Socket.prototype.connectpreload probe capturing stack frames. That is the technique this class needs; grepping for a port or afetch(call finds nothing and proves nothing.Family verdict, checked rather than assumed: same root cause as #4106, whose fix landed only in
plugin-charts— before #4389 extracted the call site into the shared hook. Not a fourth distinct cause. The re-run bisect also covered all 13 files in the package (the card listed 5; it has grown) and still confines the escape to the one file.Two more things worth recording:
- The fix is a recording double, not a blanket stub — the shape plugin-detail 测试套件里有测试向
127.0.0.1:3000发起真实网络请求(ECONNREFUSED 噪音,未走 MSW) #3339/plugin-charts / plugin-dashboard 测试套件仍有真实网络逃逸(ECONNREFUSED) — 根因与 #3339 不同,需独立定位 #4106 settled on and one this package's ownlocalSelectI18ntest already uses. It records every URL andafterEachfails on any non-metadata route, so a future escape through a different path is caught rather than silenced. The default document declares no option-bearing fields, so behaviour is byte-identical to the failed read. - A real flake was found and fixed, not tolerated. An earlier variant leaked 1 attempt in 1 of 6 runs; cause: vitest runs
afterEachin reverse registration order, so the setup file's RTLcleanup()ran aftervi.unstubAllGlobals(), letting an unmounting component fetch against a removed stub. Resolved by callingcleanup()before the unstub, then verified 8/8 consecutive clean runs.
Reverse verification was framed correctly for the defect class: this is a swallowed side effect, not a wrong assertion, so the observable is the connection count plus the disappearance of the new pins — predicted 16 / 42 passed / 0, observed exactly that.
#5280 filed:
plugin-dashboard/src/__tests__/DatasetWidget.test.tsxescapes through the same hook — 10 ECONNREFUSED lines (5 attempts) with 52 tests green. Correctly left for triage rather than fixed across a package boundary this card did not declare.
Generated by Claude Code
- The caller is
- added 4 commits that reference this issue
on Aug 23, 2026 - added a commit that references this issue
on Sep 6, 2026 - added a commit that references this issue
on Sep 9, 2026
Found while implementing #5137. Filed unassigned and not fixed there: that card is a behaviour change in
DatasetReportRenderer.tsxand this is a different defect class (test network isolation) at a file the card only appends to.Measured on
claude/issue-5137-report-filter-aliasate9a7dfb36, whose merge-base isorigin/mainat7d900dee6. Pre-existing — nothing in that branch touches the code path.The escape
packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsxmakes 16 real TCP connection attempts per run to127.0.0.1:3000:Bisected across the package — it is that one file, and only that one:
The suite is green anyway (
42 passed), which is what makes it worth a card rather than a shrug: the connection failure is swallowed, so whatever assertion sits downstream is passing through an error path rather than the one it names.Why it is not just noise
packages/plugin-report/srccontains nofetch(and no literal3000, and none of the three root setup files (vitest.setup.base.ts,vitest.setup.dom-light.tsx,vitest.setup.dom.tsx) references either — so the call originates in a workspace dependency the vitest aliases resolve tosrc, reached by a render path where the test's mock data source is not the one consulted. Locating that path is the actual work here.Family
Same shape as #3339 (plugin-detail), #4106 (plugin-charts / plugin-dashboard, noted there as a different root cause from #3339) and #4688 (plugin-view / app-shell / plugin-designer) — all closed.
plugin-reportlooks like a site those sweeps did not cover. The root cause here has not been established, so it may well be a fourth distinct one; worth checking the closed three before assuming.Recheck
Related: #5137 (the card this was found under; its PR is #5224 and deliberately leaves this alone).