test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide - #7304

Merged
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution
Sep 2, 2026
Merged

test(infra): attribute and gate the ECONNREFUSED network-escape class repo-wide#7304
yinlianghui merged 4 commits into
mainfrom
claude/issue-6640-econnrefused-attribution

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 in vitest.setup.base.ts.

Attribution first, per the triage ruling

A sweep of every project — dom (all 8 shards), dom-heavy, unit, apps/console, each VERDICT command-exit 0 from 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 in plugin-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/conversations and /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 main and still working (stubGlobal + unstubAllGlobals present, none appears in the sweep) — DatasetReportRenderer (#5225), DetailView + DetailView.invalidation (#3339), ObjectChart.compareTo (#4106), FlowCanvas + the metadata-admin inspectors (#4688), plus the DatasetWidget / DashboardGridLayout.datasetPath / calendar propsContract siblings. No fix was reverted, so the closed cards are not lying about main and no annotation is owed them.

The sharpest illustration of what is actually broken sits in one directory: #4688 stubbed ActionDefaultInspector, ConditionBuilder, HookDefaultInspector, PageBlockInspector and ViewVariantInspector, while FlowNodeInspector.specKeys and .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 18 apiFetch ?? fetch / globalThis.fetch fallbacks 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

Error: connect ECONNREFUSED 127.0.0.1:3000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)

with no stderr | FILE > TESTNAME header 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 fetch would never fire. Every escaping call site is const doFetch = apiFetch ?? fetch inside try { ... } catch { } — best-effort by construction, which is exactly why the suite is green while escaping. A rejection raised from inside fetch lands in that same catch and is swallowed, leaving the test green and the guard silent. So the escape is recorded at the call and asserted in afterEach, where no product catch can 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_ESCAPES is 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-dom globalThis.console is 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 with git diff HEAD empty. 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) · dom shard 5/16 (89 files, 1516 tests) · dom shard 3/8, the escape-heaviest (179 files, 2275 tests).

type-check:vitest-setup earned its keep twice. It ships no @types/node by design, so the first draft's node:fs / node:path / process / import.meta.dirname uses 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 the new 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 on import.meta.url instead.

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_ESCAPES is now exported (that export is its only behaviour change) and pinned by scripts/__tests__/network-escape-ledger.test.ts, which reconciles the live set against a pinned literal of the 21 measured paths in both directions:

  • a name in the ledger but not the pin ⇒ the ledger grew. Red.
  • a name in the pin but not the ledger ⇒ a fix landed and the pin is stale. Red until the pin is updated too, so shrinking is a deliberate two-line change rather than a silent one.

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 HEAD empty:

  • add a bogus name to KNOWN_ESCAPESTests 1 failed | 2 passed, "The network-escape ledger GREW, and it may only shrink."
  • delete a real name without touching the pin ⇒ 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-setup exit 0 · type-check:scripts exit 0 (the new import edge from scripts/__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 dom shards 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.tsx for the shape — and deleting its line from KNOWN_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

… 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
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude