Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records the 21 test
* files measured reaching a real socket on `67dadd6`. The guard's own docstring
* says that list "may only shrink" — and until this file existed, nothing made
* that true. An author who hit the guard's red could make it green by adding a
* line, which is exactly how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be. THAT is the failure this pin prevents;
* it does not re-measure escapes (that needs a real DOM run) and does not try.
*
* It reconciles the live set against the pinned literal in BOTH directions:
*
* - a name in the ledger but not in the pin -> the ledger GREW. Red.
* - a name in the pin but not in the ledger -> a real fix landed, and the
* pin is now stale. Red until the pin is updated too, which is the point:
* shrinking the ledger is a deliberate TWO-LINE change (delete from the
* ledger, delete from the pin), never a silent one.
*
* Plus an anchored non-vacuity floor, because both reconciles above pass
* vacuously if the imported set or the pin is empty — the classic way a pin
* keeps reporting green after the thing it pins stopped existing.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { KNOWN_ESCAPES } from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

/**
* The 21 files measured escaping on `67dadd6`, pinned verbatim.
*
* Provenance: a full sweep of every Vitest project (`dom` all 8 shards,
* `dom-heavy`, `unit`, `apps/console`) with an attribution ledger wrapping
* `fetch`. Do not add to this list. Deleting from it is the intended direction
* and must be done in lockstep with `KNOWN_ESCAPES`.
*/
const PINNED_LEDGER: readonly string[] = [
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
];

describe('network-escape ledger (objectui#6640) is shrink-only', () => {
it('has not GROWN: every name in KNOWN_ESCAPES is in the pin', () => {
const pinned = new Set(PINNED_LEDGER);
const added = [...KNOWN_ESCAPES].filter((file) => !pinned.has(file)).sort();

expect(
added,
[
'The network-escape ledger GREW, and it may only shrink.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'If the guard went red on your file, serve its probe from a double instead —',
'packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx is the shape',
"(vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()).",
'',
'Names added to KNOWN_ESCAPES but absent from PINNED_LEDGER:',
...added.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('has not gone STALE: every pinned name is still in KNOWN_ESCAPES', () => {
const stale = PINNED_LEDGER.filter((file) => !KNOWN_ESCAPES.has(file)).sort();

expect(
stale,
[
'A pinned escape is gone from KNOWN_ESCAPES — which is good news, banked wrong.',
'',
'Shrinking the ledger is deliberately a TWO-LINE change: delete the entry from',
'KNOWN_ESCAPES in vitest.setup.network-escape-guard.ts AND delete it from',
'PINNED_LEDGER in this file. This red is the second line asking to be written.',
'',
'Pinned but no longer in the ledger:',
...stale.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});

it('is not vacuous: the pin is non-empty and every pinned path exists on disk', () => {
// Both reconciles above are satisfied by two empty collections. Anchor the
// floor to a literal so that emptying either side is a red rather than a
// silent green — and check the paths resolve, so a rename cannot leave the
// pin agreeing with the ledger about files that no longer exist.
expect(
PINNED_LEDGER.length,
'PINNED_LEDGER is empty, so both reconciles above pass vacuously. If the ledger ' +
'genuinely reached zero, that is the win this whole instrument was built for — ' +
'delete the guard\'s KNOWN_ESCAPES machinery and this pin together, rather than ' +
'leaving a pin that asserts nothing.',
).toBeGreaterThan(0);

expect(
KNOWN_ESCAPES.size,
'KNOWN_ESCAPES is empty while PINNED_LEDGER is not — see the staleness test above.',
).toBeGreaterThan(0);

const missing = PINNED_LEDGER.filter(
(file) => !fs.existsSync(path.join(repoRoot, file)),
).sort();

expect(
missing,
[
'A pinned escape names a file that is not on disk.',
'',
'The ledger keys off the test file path, so a renamed or deleted file leaves an',
'entry that can never match and can never be burned down — it would sit here',
'looking like outstanding work that no longer exists.',
'',
'Missing paths:',
...missing.map((file) => ` ${file}`),
].join('\n'),
).toEqual([]);
});
});
1 change: 1 addition & 0 deletions vitest.setup.base.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@

import { vi } from 'vitest';
import { installI18nGlobalReset } from './vitest.setup.i18n-global';
import './vitest.setup.network-escape-guard';

// objectui#4514 — put react-i18next's GLOBAL default-instance pointer back
// after every test, so a provider-less render resolves the same way whether it
Expand Down
242 changes: 242 additions & 0 deletions vitest.setup.network-escape-guard.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
/**
* ObjectUI — network-escape guard (objectui#6640)
*
* Makes a test that reaches a REAL socket a NAMED, RED test instead of an
* anonymous stack on stderr.
*
* ## The class this closes
*
* happy-dom's DEFAULT document URL is `http://localhost:3000` — nothing in this
* repo configures it. So any DOM-env test that renders a component reaching one
* of the ~18 `apiFetch ?? fetch` / `globalThis.fetch` fallbacks in product code
* resolves a relative `/api/v1/...` against a real TCP socket, and prints:
*
* Error: connect ECONNREFUSED 127.0.0.1:3000
* at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1637:16)
*
* That stack has NO `stderr | FILE > TESTNAME` header and NO user stack frame —
* it is an unhandled socket error raised below the layer Vitest captures per
* test, so Vitest cannot tie it to a file. THE ANONYMITY IS THE DEFECT, not the
* noise: it is why this class was fixed four times (objectui#5225 / #3339 /
* #4106 / #4688, all still intact on main) and still reproduced in 12 of 16
* green shards. Each fix closed the files someone had listed by hand, and the
* output never said who was left. A measured sweep of every project found 21
* emitting files across 9 packages, none of them a file those four cards fixed.
*
* ## Why enforcement is in `afterEach` and NOT a throwing `fetch`
*
* The obvious instrument — make the escaping `fetch` reject loudly — DOES NOT
* WORK HERE, and would have shipped a guard that never fires. Every one of
* these call sites already tolerates failure by construction:
*
* const doFetch = apiFetch ?? fetch;
* try { ... } catch { /* best-effort: leaves the rows as the server sent *\/ }
*
* That tolerance is exactly why the suite is GREEN while escaping. A throw 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.
*
* ## What it does NOT do
*
* It does not silence anything: 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 (objectui#6640 ruling). It adds attribution beside them.
* It skips and quarantines nothing — every test still runs and asserts exactly
* what it asserted before.
*
* ## The burn-down list
*
* `KNOWN_ESCAPES` is the 21 files measured on `67dadd6`. They are not excused:
* each still emits, and now prints an ATTRIBUTED line naming itself, so a
* reader who meets a bare stack in a truncated run can tell whose it is. The
* list may only SHRINK — enforced mechanically by the reconcile pin in
* `scripts/__tests__/network-escape-ledger.test.ts`, which is the only reason
* the word "only" here is a fact rather than a hope. A file removed from it can
* never come back green, and a NEW escape in any other file is red on its first
* run. Fix one by serving the probe from a double (see
* `DatasetReportRenderer.test.tsx` for the shape), then delete its line here AND
* from `PINNED_LEDGER` in the pin — the two must move together.
*/
import { afterEach, expect } from 'vitest';

// No `node:*` imports, no `process` typings and no `import.meta.dirname` here,
// deliberately: `tsconfig.vitest-setup.json` — the gate that compiles this file
// — ships NO `@types/node` on purpose (it documents the measurement: adding it
// costs 12 errors inside third-party declarations). Every Node touch below goes
// through a locally-declared structural type instead, so the gate stays green
// without weakening it for the other root setup files.

/**
* This file sits at the repo root, so its own directory IS the repo root.
*
* Derived by STRING SURGERY on `import.meta.url`, not with `new URL('.',
* import.meta.url)`: Vite statically rewrites that exact pattern at transform
* time, and the value that survives into the run is `/@fs/...`, not a real path.
* Measured — it silently turned every path relative-isation into a miss, which
* failed all 21 known escapes at once.
*/
const REPO_ROOT = (() => {
const withoutScheme = import.meta.url.replace(/^file:\/\//, '');
const dir = withoutScheme.replace(/\/[^/]*$/, '/');
try {
return decodeURIComponent(dir);
} catch {
return dir;
}
})();

/** The sliver of `process` this file uses, declared rather than imported. */
type StderrHost = { process?: { stderr?: { write(chunk: string): void } } };

/**
* The attribution line goes to process stderr, NOT through `console`: under
* happy-dom `globalThis.console` is the window's virtual console and never
* reaches the terminal (measured — the line vanished entirely). Node writes the
* real ECONNREFUSED stack to process stderr, so this is also the only way to put
* the attribution in the SAME stream, beside the stack it explains.
*/
function writeStderr(message: string): void {
try {
(globalThis as StderrHost).process?.stderr?.write(message);
} catch {
/* the instrument must never break a run */
}
}

/** The origin happy-dom hands every relative URL when no test owns port 3000. */
const ESCAPE_ORIGIN = /^https?:\/\/(?:127\.0\.0\.1|localhost):3000(?:\/|$)/;

/**
* Files measured escaping on 67dadd6 (objectui#6640). ONLY SHRINKS.
* The comment on each line is the endpoint it reached.
*/
export const KNOWN_ESCAPES: ReadonlySet<string> = new Set([
// /api/v1/security/explain
'examples/schema-catalog/test/catalog-gallery-render.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.authoringCapabilityGate.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.inboxLinksTarget.test.tsx',
// /api/v1/meta/_drafts
'packages/app-shell/src/console/home/__tests__/HomePage.notificationDeepLink.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.inactiveRetained.test.tsx',
// /api/v1/meta/object
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
// /api/v1/automation/_status
'packages/app-shell/src/views/studio-design/StudioDesignSurface.designerRegistryMissing.test.tsx',
// /api/v1/ai/conversations
'packages/app-shell/src/views/studio-design/__tests__/studioSurfaceContext.test.tsx',
// /api/v1/security/explain
'packages/plugin-calendar/src/ObjectCalendar.navWidthDefault.test.tsx',
// /api/v1/meta/object/task
'packages/plugin-charts/src/ObjectChart.heightChain.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx',
// /api/task/42, /api/v1/security/explain
'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/__tests__/recordDetailsBodySource.test.tsx',
// /api/v1/security/explain
'packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-gantt/src/ObjectGantt.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-grid/src/__tests__/bulkDeleteVisibleWhen.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.navWidthDefault.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleI18n.test.tsx',
// /api/v1/security/explain
'packages/plugin-kanban/src/ObjectKanban.overlayTitleNoProviderFallback.test.tsx',
// /api/v1/security/explain
'packages/plugin-view/src/__tests__/ObjectView.namedViewSortArity.test.tsx',
]);

type Escape = { file: string; test: string; url: string };

/** Escapes seen since the current test started. */
let pending: Escape[] = [];

function relative(p: string | undefined): string {
if (!p) return '<unknown-test-file>';
const normalised = p.replace(/\\/g, '/');
return normalised.startsWith(REPO_ROOT) ? normalised.slice(REPO_ROOT.length) : normalised;
}

const realFetch = globalThis.fetch;

globalThis.fetch = function guardedFetch(input: any, init?: any) {
let raw: string;
try {
raw = typeof input === 'string' ? input : (input?.url ?? String(input));
} catch {
raw = '<unreadable-request>';
}
let absolute = raw;
try {
absolute = new URL(raw, (globalThis as any).location?.href ?? undefined).href;
} catch {
/* a non-URL input cannot be an escape; leave it as-is */
}

if (ESCAPE_ORIGIN.test(absolute)) {
let state: any = {};
try {
state = expect.getState() ?? {};
} catch {
/* called outside a test */
}
const escape: Escape = {
file: relative(state.testPath),
test: state.currentTestName ?? '<outside-a-test>',
url: absolute,
};
pending.push(escape);

// Known escapes get an ATTRIBUTED line next to the anonymous stack the real
// request is about to print. This is the half that cures the reported harm:
// a bare ECONNREFUSED in a truncated log no longer reads as an unowned red.
if (KNOWN_ESCAPES.has(escape.file)) {
writeStderr(
`[network-escape - known - objectui#6640] ${escape.file} -> ${escape.url}\n` +
` The ECONNREFUSED stack near this line belongs to that file. Serve the\n` +
` probe from a double, then delete its line from KNOWN_ESCAPES in\n` +
` vitest.setup.network-escape-guard.ts.\n`,
);
}
}

// Always pass through. The real connection attempt IS the evidence that a
// test reached for a socket; hiding it would keep the escape and remove the
// proof (objectui#6640 ruling).
return realFetch.call(globalThis, input, init);
} as typeof globalThis.fetch;

afterEach(() => {
const seen = pending;
pending = [];
const unknown = seen.filter((e) => !KNOWN_ESCAPES.has(e.file));
if (unknown.length === 0) return;

const byUrl = [...new Set(unknown.map((e) => e.url))];
const file = unknown[0].file;
throw new Error(
`Network escape: this test reached a REAL socket at ${byUrl.join(', ')}.\n` +
` file: ${file}\n` +
` test: ${unknown[0].test}\n` +
`\n` +
`happy-dom's default document URL is http://localhost:3000, so a relative\n` +
`fetch from a component under test resolves to a live TCP connection. The\n` +
`product call site catches the failure by design, so the test stayed green\n` +
`while printing an unattributable ECONNREFUSED stack — that is objectui#6640.\n` +
`\n` +
`Fix: serve the probe from a double rather than the network. See\n` +
`packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx for the\n` +
`shape (vi.stubGlobal('fetch', router) + vi.unstubAllGlobals()). Do NOT add\n` +
`this file to KNOWN_ESCAPES — that list only shrinks.`,
);
});
Loading