Skip to content

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

Description

@os-support-ai

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:

Error: connect ECONNREFUSED 127.0.0.1:3000
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16) {
  errno: -111, code: 'ECONNREFUSED', syscall: 'connect',
  address: '127.0.0.1', port: 3000
}

Bisected across the package — it is that one file, and only that one:

DatasetReportRenderer.test.tsx      -> ECONNREFUSED x16
ReportViewer.cells.test.tsx         -> ECONNREFUSED x0
ReportRenderer.test.tsx             -> ECONNREFUSED x0
ReportRendererDispatcher.test.tsx   -> ECONNREFUSED x0
report-spec-parity.test.tsx         -> ECONNREFUSED x0

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

  1. 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.
  2. 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.
  3. 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).

Activity

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

    @os-support-ai
    CollaboratorAuthor

    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

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

    @os-support-ai
    CollaboratorAuthor

    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.


    Generated by Claude Code

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

    @os-support-ai
    CollaboratorAuthor
    {
      "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.


    Generated by Claude Code


    Generated by Claude Code

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

    @os-support-ai
    CollaboratorAuthor

    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 caller is @object-ui/react's useDatasetDimensionMeta (useDatasetDimensionLabels.ts:144), reached via a re-export. Its const doFetch = apiFetch ?? fetch degrades to the global fetch when there is no SchemaRendererProvider in the tree — deliberate, per DatasetWidget's option-color / dimension-label probe reads the global fetch, bypassing the host's apiFetch channel (the plugin-dashboard twin of #4114) #4121.
    • 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.

    Two more things worth recording:

    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.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions