You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
plugin-report suite escapes to the real network on 127.0.0.1:3000 (ECONNREFUSED noise, same class as #3339 / #4106 / #4688) #5225
Found while implementing #5137. Filed unassigned and not fixed there: that card is a behaviour change in DatasetReportRenderer.tsx and this is a different defect class (test network isolation) at a file the card only appends to.
Measured on claude/issue-5137-report-filter-alias at e9a7dfb36, whose merge-base is origin/main at 7d900dee6. Pre-existing — nothing in that branch touches the code path.
The escape
packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx makes 16 real TCP connection attempts per run to 127.0.0.1:3000:
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
Something reaches a real transport in a unit test.packages/plugin-report/src contains no fetch( and no literal 3000, 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 to src, 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.
The suite depends on nothing listening on port 3000. In this repo's own shared containers a dev server on 3000 is routine. If one is up, those 16 attempts connect — to whatever another agent happens to be running — and the test's behaviour changes without a single line of it changing.
A green test that quietly took the error path is not testing what it says. Worth checking which of the 42 depend on the swallowed failure.
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-report looks 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
pnpm exec vitest run packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx 2>&1 | grep -c ECONNREFUSED
Related: #5137 (the card this was found under; its PR is #5224 and deliberately leaves this alone).
Triage: 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.
Claim: 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.tsx and 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-report is free — no open PR and no in-flight claim touches it. The four other in-flight cards are in plugin-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-base origin/main @ 7d900dee6) and asserts the defect is pre-existing. That branch has since merged (PR5224). So the dev must re-measure on current origin/main with 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.
{
"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 to plugin, tests — objectui has no skip-changeset mechanism and this change carries an empty-frontmatter changeset instead. Worktree objectui-issue-5225 removed (no --force needed). Reporting at draft-PR time per the standing clause; CI convergence is the PM's read.
ACCEPT — 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/src contains no fetch( and no literal 3000 — both true, and both irrelevant, which is exactly why this needed a probe rather than a search:
The port comes from the test environment, not the repo. happy-dom's fetch is a real HTTP client, and vitest's happy-dom env defaults the document URL to http://localhost:3000, so a relative /api/v1/meta/object/:object resolves 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 carries object reach it.
Located with a net.Socket.prototype.connect preload probe capturing stack frames. That is the technique this class needs; grepping for a port or a fetch( 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.
A real flake was found and fixed, not tolerated. An earlier variant leaked 1 attempt in 1 of 6 runs; cause: vitest runs afterEach in reverse registration order, so the setup file's RTL cleanup() ran aftervi.unstubAllGlobals(), letting an unmounting component fetch against a removed stub. Resolved by calling cleanup() 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.tsx escapes 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.
Activity
os-support-ai commentedon Aug 18, 2026
Triage: 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 commentedon Aug 18, 2026
Claim: PM loop round 12 (refill)
Session:
session_01RV6yuVCxymHYE16PL9vQkEBranch:
claude/issue-5225-plugin-report-network-escapeWorktree:
objectui-issue-5225Domain:
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: opusClause-②: 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
test(plugin-report): serve the dimension-metadata probe from a double…
os-support-ai commentedon Aug 19, 2026
{ "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 commentedon Aug 19, 2026
ACCEPT — 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:@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.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.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:
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.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
test(plugin-dashboard): close DatasetWidget's dimension-metadata netw…
test(plugin-dashboard): close DashboardGridLayout's object-chart meta…
test(plugin-report): serve the dimension-metadata probe from a double…
test(plugin-dashboard): close DatasetWidget's dimension-metadata netw…
test(plugin-dashboard): close DashboardGridLayout's object-chart meta…
test(plugin-calendar): serve the record-level explain probe from a do…
test(plugins): serve batch 2's four probes from doubles (objectui#7307)
test(plugins): serve batch 2's four probes from doubles (objectui#730…