Uh oh!
There was an error while loading. Please reload this page.
test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304
Merged
Merged
Conversation
… repo-wide happy-dom's default document URL is http://localhost:3000, so a DOM test that renders a component reaching one of the `apiFetch ?? fetch` fallbacks resolves a relative /api/v1/... against a real socket. The resulting stack carries no `stderr | FILE > TEST` header and no user frame, so Vitest cannot attribute it. That anonymity — not the noise — is why the class was fixed four times and still reproduced in 12 of 16 green shards. Enforcement lives in `afterEach`, not in a throwing `fetch`: every one of these call sites catches by construction (best-effort reads), so a rejection from inside `fetch` is swallowed and the test stays green. A measured sweep of every project (dom 8 shards, dom-heavy, unit, apps/console) attributes the class to 21 files in 9 packages, none of which is a file the four closed cards fixed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
…url rewrite
Two defects found by running the gates rather than assuming them.
`tsconfig.vitest-setup.json` ships no `@types/node` by design, so the first
draft's `node:fs` / `node:path` / `process` / `import.meta.dirname` uses were
five type errors. The guard now declares the one structural type it needs and
drops the file-ledger entirely.
`new URL('.', import.meta.url)` is statically rewritten by Vite at transform
time, so the value reaching the run was `/@fs/...` rather than a real path.
Every path relative-isation missed, which failed all 21 known escapes at once.
Derived by string surgery on `import.meta.url` instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
This was referenced Sep 2, 2026
…tions The guard's docstring said KNOWN_ESCAPES "may only shrink" and nothing made that true: an author meeting the guard's red could go green by adding a line, which is how a burn-down ledger decays into the permanent quarantine it is not. Exports the set for the pin and adds the reconcile. A name in the ledger but not the pin means it grew (red). A name in the pin but not the ledger means a fix landed and the pin is stale (red), so shrinking is a deliberate two-line change rather than a silent one. An anchored non-vacuity floor covers the case both reconciles pass on two empty collections, and checks each pinned path still resolves on disk. No behaviour change to the guard beyond the export. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
The closing bracket sat on the same line as the last ledger entry, so the burn-down's intended edit — delete the line for a file you just fixed — turned into a syntax error on the last entry. Detached it, and matched the surrounding style (two-space indent, single quotes) in both lists. Also repairs a docstring wrap and states the two-line rule where a reader meets it: an entry leaves KNOWN_ESCAPES and PINNED_LEDGER together. No behaviour change; the ledger contents are byte-identical in membership. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
yinlianghui
marked this pull request as ready for review
September 2, 2026 05:58
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6640
Verified on
1836f41f8. Three files:vitest.setup.network-escape-guard.ts(new),scripts/__tests__/network-escape-ledger.test.ts(new, the shrink-only pin), and one import line invitest.setup.base.ts.Attribution first, per the triage ruling
A sweep of every project —
dom(all 8 shards),dom-heavy,unit,apps/console, eachVERDICT command-exit 0from the shared verify lock — attributes the class to 21 test files across 9 packages:app-shell(8),plugin-detail(4),plugin-kanban(3), and one each inplugin-view,plugin-calendar,plugin-grid,plugin-gantt,plugin-charts,examples/schema-catalog. They reach/api/v1/security/explain,/api/v1/meta/_drafts,/api/v1/meta/object,/api/v1/meta/object/task,/api/v1/ai/conversationsand/api/v1/automation/_status. The full table with per-file rows is on the card.This selects possibility 1, and falsifies possibility 2. Not one of the 21 is a file the four closed siblings fixed. Checked by symptom rather than by issue number: all eight landed doubles are still on
mainand still working (stubGlobal+unstubAllGlobalspresent, none appears in the sweep) —DatasetReportRenderer(#5225),DetailView+DetailView.invalidation(#3339),ObjectChart.compareTo(#4106),FlowCanvas+ the metadata-admin inspectors (#4688), plus theDatasetWidget/DashboardGridLayout.datasetPath/ calendarpropsContractsiblings. No fix was reverted, so the closed cards are not lying aboutmainand no annotation is owed them.The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed
ActionDefaultInspector,ConditionBuilder,HookDefaultInspector,PageBlockInspectorandViewVariantInspector, whileFlowNodeInspector.specKeysand.inactiveRetained— same directory, same mechanism — were simply not in its enumeration. Each fix closed the files someone had listed by hand, and the list is hand-made. That is why a fifth per-package fix is the wrong shape.Mechanism
happy-dom's default document URL is
http://localhost:3000; nothing in this repo configures it. So a DOM-env test rendering a component that reaches one of the roughly 18apiFetch ?? fetch/globalThis.fetchfallbacks in product code resolves a relative/api/v1/...against a real TCP socket.Two findings that shaped the instrument
1. The stacks are unattributable, and that — not the noise — is the defect. They arrive as
with no
stderr | FILE > TESTNAMEheader and no user stack frame, because it is an unhandled socket error raised below the layer Vitest captures per test. Vitest cannot tie it to a file. That is why the class survived four fixes: the output never said who was left.2. A throwing
fetchwould never fire. Every escaping call site isconst doFetch = apiFetch ?? fetchinsidetry { ... } catch { }— best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from insidefetchlands in that samecatchand is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted inafterEach, where no productcatchcan reach it. Measured: the ablation below fails only because of this choice.What it does and does not do
It silences nothing — the real request still goes out and the real ECONNREFUSED still prints, because those stacks are the evidence that a test reached for a socket. Beside each one the guard now writes an attributed line naming the owning file, so a bare stack in a truncated log is no longer an unowned red. It skips and quarantines nothing: every test still runs and asserts exactly what it asserted before.
KNOWN_ESCAPESis the 21 measured files and may only shrink — a new escape in any other file is red on its first run, which is the structural close of the class.Written straight to process stderr rather than through
console: under happy-domglobalThis.consoleis the window's virtual console and never reaches the terminal (measured — the line vanished entirely), and process stderr is also the only stream that puts the attribution beside the stack it explains.Verification
Ablation, on the committed tree: delete one entry from
KNOWN_ESCAPES, confirm the mutation landed by blob hash, run, restore, confirm the restored blob is byte-identical to the HEAD blob withgit diff HEADempty. Mutated:Test Files 1 failed, the guard naming file, test and URL.Green with the guard: the 21 known files (690 tests, 135 attributed lines, 0 guard failures) ·
unit(808 files, 12572 passed, 9 pre-existing skips) ·dom-heavy(34 files, 332 tests) ·domshard 5/16 (89 files, 1516 tests) ·domshard 3/8, the escape-heaviest (179 files, 2275 tests).type-check:vitest-setupearned its keep twice. It ships no@types/nodeby design, so the first draft'snode:fs/node:path/process/import.meta.dirnameuses were five real errors; the guard now declares the one structural type it needs. And re-running the suite after that rewrite caught the second defect: Vite statically rewrites thenew URL('.', import.meta.url)pattern at transform time, so the value reaching the run was/@fs/...rather than a real path — every path relative-isation missed and all 21 known escapes failed at once. Derived by string surgery onimport.meta.urlinstead.Patch round (review follow-up)
The ledger's docstring said it "may only shrink" and nothing made that true — an author meeting the guard's red could go green by adding a line, which is precisely how a burn-down ledger decays into the permanent quarantine it is not supposed to be.
KNOWN_ESCAPESis now exported (that export is its only behaviour change) and pinned byscripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:Plus an anchored non-vacuity floor, because both reconciles pass vacuously on two empty collections: the pin must be non-empty, the imported set must be non-empty, and every pinned path must still resolve on disk (so a rename cannot leave the two sides agreeing about files that no longer exist).
Proved by ablation on disk, each leg confirmed by blob hash and restored to a byte-identical blob with
git diff HEADempty:KNOWN_ESCAPES⇒Tests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."Tests 1 failed | 2 passed, "A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong."Gates on
1836f41f8, each quoting its own verdict line:type-check:vitest-setupexit 0 ·type-check:scriptsexit 0 (the new import edge fromscripts/__tests__to the root setup file is clean) ·check:control-bytes"OK (scanned 5998 tracked text file(s); skipped 85 binary)" · eslint on both changed files exit 0, 0 errors ·network-escape-ledger.test.ts+vitest-invocation-guard.test.ts"Test Files 2 passed (2) / Tests 33 passed (33)" · the 21 known files again "Test Files 21 passed (21) / Tests 690 passed (690)", 135 attributed lines, 0 guard failures ·check-changeset-presence"No source or published contract of a released package changed in this range, so no changeset is owed."Declared narrowing
The remaining
domshards were swept for attribution but not re-run against the final commit; CI runs the full farm regardless. Two runs were cut by the container's 10-minute foreground cap and recorded NOT MEASURED, then re-run split rather than quoted.Follow-up, deliberately not in this PR
The 21 known escapes are recorded, not excused. Each is fixed by serving its probe from a double — see
packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsxfor the shape — and deleting its line fromKNOWN_ESCAPESand from the pin. That is per-file work across 9 packages and belongs in its own cards, not riding on the instrument that found them.🤖 Generated with Claude Code
https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b