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
13 changes: 13 additions & 0 deletions .changeset/network-escapes-batch6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
---

Test-only (objectui#7307 batch 6, the last): `FlowNodeInspector.specKeys.test.tsx`
now serves its `GET /api/v1/meta/object` probe from a recording double instead of
a real socket, which empties the network-escape burn-down ledger. The ledger is
therefore RETIRED with it — the guard's `KNOWN_ESCAPES` set, its attributed-stderr
branch for listed files and its known/unknown split are gone, exactly as the
ledger's own non-vacuity floor prescribed for reaching zero, and
`scripts/__tests__/network-escape-ledger.test.ts` now pins that the machinery
stays gone. The standing guard from objectui#6640 is untouched and now covers
every file: any test that reaches a real socket is red on its first run. No
published runtime code changes, so nothing to release.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
* queries used to prove a control ABSENT can find controls that are present.
*/

import { describe, it, expect, vi, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, cleanup, fireEvent } from '@testing-library/react';
import * as Automation from '@objectstack/spec/automation';
import type { FlowNode as SpecFlowNode } from '@objectstack/spec/automation';
Expand All @@ -71,7 +71,84 @@ vi.mock('../previews/useObjectFields', () => ({
import { FlowNodeInspector } from './FlowNodeInspector';
import type { MetadataSelection } from '../preview-registry';

afterEach(cleanup);
/* ── The `meta/object` double (objectui#7307) ───────────────────
* `FlowNodeInspector` renders `FlowReferenceField` for every reference-kind key
* on the selected node, and that field resolves its combobox options through
* `useMetadataListOptions` (`FlowReferenceField.tsx:638`) -> `MetadataClient.list(type)`,
* i.e. `GET /api/v1/meta/object` over the authenticated wrapper, which resolves
* the GLOBAL `fetch` at call time (`packages/auth/src/createAuthenticatedFetch.ts`,
* the bare `await fetch(input, ...)`). Under happy-dom that global is a real HTTP
* client and the document URL defaults to `http://localhost:3000`, so the relative
* path resolved to a live socket: measured on `fc32921aa`, three escapes to
* `http://localhost:3000/api/v1/meta/object`, one per render, while this file
* stayed green — which is objectui#6640 exactly.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on, carried
* by `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx` and by
* this burn-down's earlier batches, most recently the sibling
* `FlowNodeInspector.inactiveRetained.test.tsx`. Deliberately NOT a blanket
* network stub: it records every URL it is handed and `afterEach` fails on any URL
* outside the route it serves, so an escape to somewhere else reds here instead of
* vanishing into the hook's `.catch`.
*
* What it answers, and why that changes no assertion here: an EMPTY registry, in
* the `{ type, items: [] }` envelope the server sends and `MetadataClient.list`
* parses (it also accepts a bare array; both parse to the same rows). Empty is
* load-bearing — the failing request landed in the hook's `.catch`, which sets
* `{ options: [], loading: false }`, so an empty registry yields byte-identical
* output to what these cases have always rendered, while a seeded one would put
* options into every reference combobox in the tree. This file asserts on the
* ABSENCE of a `Description` control and on what `onPatch` emits, so seeding
* options would be a silent change of subject. The route is matched on the
* PATHNAME because `MetadataClient.list` appends `?package=` / `?preview=draft`
* for scoped callers; the full URL is what gets recorded.
*
* `headers` is part of the answer, not decoration: the authenticated wrapper
* reads `response.headers.get('set-auth-token')` on every API call before the
* caller ever sees the body.
* ─────────────────────────────────────────────────────────── */

const META_OBJECT_ROUTE = '/api/v1/meta/object';

/** Every URL this file's renders handed the global `fetch`, in request order. */
let metaCalls: string[] = [];

/** The route key of a recorded URL: its pathname, without the scope query. */
const routeOf = (url: string) => url.split('?')[0];

/** Serve `GET /api/v1/meta/object` as an empty registry; record everything. */
function installMetaObjectDouble() {
metaCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
metaCalls.push(url);
if (routeOf(url) !== META_OBJECT_ROUTE) {
return { ok: false, status: 404, headers: new Headers(), json: async () => ({}) };
}
return { ok: true, status: 200, headers: new Headers(), json: async () => ({ type: 'object', items: [] }) };
}),
);
}

beforeEach(installMetaObjectDouble);

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into `useMetadataListOptions`'s `.catch`.
expect(metaCalls.filter((url) => routeOf(url) !== META_OBJECT_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch` — this replaces the bare
// `afterEach(cleanup)` that used to stand here, it does not drop it. Vitest
// runs `afterEach` hooks in reverse registration order, so this file's
// teardown runs before the root setup's RTL cleanup: unstubbing first would
// leave the tree mounted with the real global back in place, and a mount
// effect settling in that window escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});

interface ZodIssue {
code?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ beforeEach(() => {
// to everything below — this file observes an EMPTY section's skeleton, and
// the inline-edit affordance is not part of that.
//
// ⛔ Not `KNOWN_ESCAPES`: that list only shrinks, and its
// `record-details.emptySectionDefault.test.tsx` entry is the older sibling
// this file deliberately does not join.
// ⛔ There is no list to join instead: the guard's `KNOWN_ESCAPES` burn-down
// reached zero and was retired on objectui#7307, so serving the probe from a
// double is the ONLY way a file that reaches a socket goes green.
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: true,
status: 200,
Expand Down
191 changes: 102 additions & 89 deletions scripts/__tests__/network-escape-ledger.test.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,135 @@
/**
* The shrink-only pin for the network-escape ledger (objectui#6640).
* The network-escape ledger is RETIRED — and stays retired (objectui#6640, #7307).
*
* `KNOWN_ESCAPES` in `vitest.setup.network-escape-guard.ts` records what remains
* of 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.
* ## What this file used to be, and why it is not that any more
*
* It reconciles the live set against the pinned literal in BOTH directions:
* `vitest.setup.network-escape-guard.ts` used to export `KNOWN_ESCAPES`: the
* burn-down list of what remained of the 21 test files measured reaching a real
* socket on `67dadd6`. This file was its shrink-only reconcile pin, and it
* existed because the guard's docstring said the list "may only shrink" while
* nothing made that true — an author who hit the guard's red could make it green
* by adding a line, which is how a burn-down ledger decays into the permanent
* quarantine it is not supposed to be.
*
* - 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.
* objectui#7307 burned the list down batch by batch (PRs #7999, #8013, #8019,
* #8032, #8053) and its sixth and last batch emptied it. The old pin's own
* non-vacuity floor prescribed what to do then, verbatim: "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." So the set, the attributed-stderr branch
* that kept listed files green, and the known/unknown split are all gone.
*
* 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.
* ## Why a retirement pin rather than a deleted file
*
* The pressure the old pin resisted did not end with the list; it went UP. Every
* escape is now red on its first run with no list to join, so the cheapest wrong
* fix available to the next author who meets that red is to RE-CREATE the list —
* a one-line, green-on-arrival change that would silently re-open a tolerated
* path for the whole repo. That is the same failure the old pin was written for,
* one step later, and deleting this file would leave nothing resisting it.
*
* So this file now pins the ABSENCE of the machinery, plus the presence of the
* STANDING guard that was never part of the burn-down: the recording `fetch`
* wrapper and the `afterEach` that fails ANY escape in ANY file. Re-adding a
* list is red here; deleting the guard while deleting the list is red here too.
*
* ## How it reads the guard, and why not with a regex
*
* The absence assertions run over the guard's source with COMMENTS BLANKED, via
* `scripts/js-comment-mask.mjs` — the tree's one answer to "comment, or code?".
* That is load-bearing rather than tidy: the guard's own header now explains at
* length that there is no `KNOWN_ESCAPES` any more, so a naive text search finds
* the name in prose and this pin could never go green. Masking makes the
* assertion about CODE, which is what it means.
*
* Every negative below is paired with a positive control on the same read, so a
* mistyped path or an empty read cannot pass as "the thing is absent".
*/
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';
import { maskComments } from '../js-comment-mask.mjs';
import * as guard from '../../vitest.setup.network-escape-guard';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const GUARD_PATH = 'vitest.setup.network-escape-guard.ts';
const guardSource = fs.readFileSync(path.join(repoRoot, GUARD_PATH), 'utf8');
/** The guard's CODE — its prose, which legitimately names the retired set, blanked. */
const guardCode = maskComments(guardSource);

/**
* What remains of 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[] = [
'packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.specKeys.test.tsx',
];
describe('the network-escape burn-down list stays retired (objectui#7307)', () => {
it('is reading the real guard file, not an empty or wrong one', () => {
// The floor under every negative below. A negative over a bad read is the
// classic pin that reports green after the thing it pins stopped existing.
expect(guardSource.length).toBeGreaterThan(1000);
expect(guardCode, 'comment masking blanked the code as well as the prose').toContain(
'ESCAPE_ORIGIN',
);
// The control for the MASK itself: a phrase that exists only in the guard's
// header prose. Present in the raw source, absent from the masked code — if
// masking silently stopped working, this is what goes red, rather than the
// absence assertions below quietly passing over unmasked prose.
const PROSE_ONLY = 'THE ANONYMITY IS THE DEFECT';
expect(guardSource).toContain(PROSE_ONLY);
expect(guardCode, 'comment masking is not blanking comments').not.toContain(PROSE_ONLY);
});

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();
it('exports nothing — the ledger was its only export', () => {
// Runtime, not source: this is the SAME module instance the run is guarded
// by (`vitest.setup.base.ts` imports it for every project), so a re-exported
// list shows up here whatever the source looks like.
expect(Object.keys(guard as Record<string, unknown>).sort()).toEqual([]);
});

it('declares no allowlist of tolerated escapes in its code', () => {
// Catches the list coming back UNEXPORTED too, which the namespace read
// above cannot see — and that is the likelier re-introduction, since a
// private set needs no export to make a red green.
expect(
added,
guardCode.includes('KNOWN_ESCAPES'),
[
'The network-escape ledger GREW, and it may only shrink.',
'The network-escape burn-down list is back in vitest.setup.network-escape-guard.ts.',
'',
'A test that reaches a real socket is a defect to fix, not a line to add here.',
'It reached zero on objectui#7307 and was retired deliberately. A test that',
'reaches a real socket is a defect to fix, not a line to add to a list.',
'If the guard went red on your file, serve its probe from a double instead.',
'The remedy in full — the shape AND where its teardown has to go — is the',
"guard's own Fix: text in vitest.setup.network-escape-guard.ts. It is",
'deliberately NOT restated here: this line drifting out of step with that',
'one is objectui#7765, and one ruling written twice is how it rotted.',
'',
'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();
).toBe(false);

// The general shape, not just the old name: a re-introduction under a new
// spelling is the same defect. `new Set([...])` of test paths is what one
// looks like, and the guard's code holds no `Set` literal at all now.
const setLiterals = guardCode.match(/new Set\(\[/g) ?? [];
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'),
setLiterals,
'A set literal appeared in the guard. If it is a tolerated-escape list under a ' +
'new name, it is the retired ledger: fix the escape instead. If it is something ' +
'else entirely, this pin is what needs the deliberate edit.',
).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();
describe('the STANDING guard survived the retirement (objectui#6640)', () => {
it('still wraps the global fetch to record escapes', () => {
// Behavioural, on the live global: the setup file has run for this project,
// so the wrapper installed below is the one every test in the run uses.
expect(globalThis.fetch.name).toBe('guardedFetch');
expect(guardCode).toContain('globalThis.fetch = function guardedFetch');
});

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([]);
it('still registers the afterEach that fails an escape in any file', () => {
// The half the burn-down never owned, and the half that must outlive it: an
// escape anywhere is red on its first run.
expect(guardCode).toContain('afterEach(');
expect(guardCode).toContain('Network escape: this test reached a REAL socket');
expect(guardCode, 'the remedy the red points at').toContain(
'Fix: serve the probe from a double rather than the network',
);
});
});
5 changes: 4 additions & 1 deletion scripts/check-doc-example-shared-reader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@
* `KNOWN_HAND_SPELLINGS` below is an allowlist that only shrinks: an entry is a
* live defect with a card, never a waiver. It exists so that a first run finding
* real instances cannot force the prose fixes into the same PR as the gate — the
* same shape `scripts/__tests__/network-escape-ledger.test.ts` uses.
* same shape the network-escape ledger used (objectui#6640) until objectui#7307
* burned it to zero and retired it; `scripts/__tests__/network-escape-ledger.test.ts`
* now pins that its list is GONE, which is where a shrink-only ledger is supposed
* to end up.
*
* It landed carrying exactly one row, which is the gate's own first finding:
* objectui#7787, `navigation-overlay.tsx`'s file-header example, the copy of
Expand Down
Loading
Loading