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

Test-only change: the five network-escape files in batch 1 of objectui#7307 now serve their `/api/v1/security/explain` and `/api/v1/meta/object/<name>` probes from a recording double instead of a real socket, and their lines leave `KNOWN_ESCAPES` and `PINNED_LEDGER` together. No published behaviour changes — no product source is touched.
93 changes: 91 additions & 2 deletions examples/schema-catalog/test/catalog-gallery-render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@
* entries render `role="alert"` because that is what an Alert IS — the
* assertion is correct for a dashboard tile and wrong for the corpus.
*/
import { describe, it, expect, afterAll } from 'vitest';
import { render, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
import { render, waitFor, cleanup } from '@testing-library/react';
import '@object-ui/components';
// Mirrors apps/site/app/components/registerCatalogBlocks.ts, in its order.
import '@object-ui/plugin-dashboard';
Expand Down Expand Up @@ -517,6 +517,95 @@ function authoredTitles(node: unknown, acc: string[] = []): string[] {
const entries = allExamples();
const occurrences = (haystack: string, needle: string) => haystack.split(needle).length - 1;

/* ────────────────────────────────────────────────────────────────────────────
* objectui#7307 — this file's `/api/v1/security/explain` escape, served here.
*
* Nothing below asks for a security verdict, yet every run opened a REAL TCP
* connection to `http://localhost:3000`. Traced with a stack probe on the
* network-escape guard's attribution point:
*
* the grid-bearing tiles (`object-grid`, and `object-view` around it)
* -> ObjectGrid packages/plugin-grid/src/ObjectGrid.tsx:1407
* -> useRecordCrudVerdicts packages/plugin-grid/src/hooks/useRecordCrudVerdicts.ts:199
* -> `const doFetch = apiFetch ?? fetch` <- the escape
* POST /api/v1/security/explain (batched, `recordIds` per page)
*
* The hook reads the host's AUTHENTICATED `apiFetch` off
* `SchemaRendererContext` and, with no host supplying one, degrades to the
* GLOBAL `fetch` by design — a standalone embed must keep rendering rather than
* crash. 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
* request. The gallery harness below supplies a `dataSource` on `SchemaRendererContext` but no `apiFetch`, so the fallback is taken. The read is best-effort (a network or parse failure leaves the verdict map empty — fail open), which is why the sweep stayed green while 18 requests per run always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on and
* `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx`
* carries. Deliberately NOT a blanket network stub: it records every URL it is
* handed and `afterEach` fails on any URL that is not the explain route, so an
* escape to somewhere else reds here instead of vanishing into that `catch`.
*
* What it answers, and why that changes no assertion here: the permissive
* verdict, in the two response shapes the two hooks read (ADR-0090 D6 /
* ADR-0095 C2) — `{ record: { visible } }` for a single `recordId`,
* `{ records: [{ recordId, visible }] }` for a batched `recordIds`.
* `useRecordEditable` initialises `allowed` to `true` and its failure path
* leaves it there, and the ONLY consumer of the batched lookup is
* `resolveRowRecordCrudAffordance`, whose rule is `recordVerdict !== false` —
* so `true` and the absent verdict the failing request produced are the same
* value at every read site. This file asserts that each tile drew something other than a diagnostic panel; no tile's DOM is derived from the verdict.
* ──────────────────────────────────────────────────────────────────────────── */

const EXPLAIN_ROUTE = '/api/v1/security/explain';

/** Every URL this render handed the global `fetch`, in request order. */
let explainCalls: string[] = [];

/** Serve `POST /api/v1/security/explain` permissively; record everything. */
function installExplainDouble() {
explainCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown, init?: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
explainCalls.push(url);
if (url !== EXPLAIN_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
let body: { recordId?: unknown; recordIds?: unknown } = {};
try {
body = JSON.parse(String((init as { body?: unknown } | undefined)?.body ?? '{}'));
} catch {
/* a non-JSON body is not a request this route can answer */
}
const recordIds = Array.isArray(body.recordIds) ? body.recordIds : null;
return {
ok: true,
status: 200,
json: async () =>
recordIds
? { records: recordIds.map((recordId) => ({ recordId, visible: true })) }
: { record: { visible: true } },
};
}),
);
}

beforeEach(() => {
installExplainDouble();
});

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(explainCalls.filter((url) => url !== EXPLAIN_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. 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 verdict effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});

describe('objectui#4616 — every catalog entry renders in the docs gallery', () => {
/**
* NON-VACUITY CONTROL for the sweep as a whole. `it.each([])` reports nothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,95 @@ function readPanelWidth(): string {
return panel!.style.maxWidth;
}

/* ────────────────────────────────────────────────────────────────────────────
* objectui#7307 — this file's `/api/v1/security/explain` escape, served here.
*
* Nothing below asks for a security verdict, yet every run opened a REAL TCP
* connection to `http://localhost:3000`. Traced with a stack probe on the
* network-escape guard's attribution point:
*
* RecordDetailDrawer (the drawer this file opens)
* -> useRecordEditable packages/plugin-detail/src/useRecordEditable.ts:75
* -> `const doFetch = apiFetch ?? fetch` <- the escape
* POST /api/v1/security/explain
*
* The hook reads the host's AUTHENTICATED `apiFetch` off
* `SchemaRendererContext` and, with no host supplying one, degrades to the
* GLOBAL `fetch` by design — a standalone embed must keep rendering rather than
* crash. 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
* request. The read is best-effort (a network or parse failure leaves the record editable — fail open), which is why the three cases below stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on and
* `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx`
* carries. Deliberately NOT a blanket network stub: it records every URL it is
* handed and `afterEach` fails on any URL that is not the explain route, so an
* escape to somewhere else reds here instead of vanishing into that `catch`.
*
* What it answers, and why that changes no assertion here: the permissive
* verdict, in the two response shapes the two hooks read (ADR-0090 D6 /
* ADR-0095 C2) — `{ record: { visible } }` for a single `recordId`,
* `{ records: [{ recordId, visible }] }` for a batched `recordIds`.
* `useRecordEditable` initialises `allowed` to `true` and its failure path
* leaves it there, and the ONLY consumer of the batched lookup is
* `resolveRowRecordCrudAffordance`, whose rule is `recordVerdict !== false` —
* so `true` and the absent verdict the failing request produced are the same
* value at every read site. The drawer's width — everything this file asserts — is not derived from the verdict at all.
* ──────────────────────────────────────────────────────────────────────────── */

const EXPLAIN_ROUTE = '/api/v1/security/explain';

/** Every URL this render handed the global `fetch`, in request order. */
let explainCalls: string[] = [];

/** Serve `POST /api/v1/security/explain` permissively; record everything. */
function installExplainDouble() {
explainCalls = [];
vi.stubGlobal(
'fetch',
vi.fn(async (input: unknown, init?: unknown) => {
const url = String(
input && typeof input === 'object' && 'url' in input ? (input as { url: unknown }).url : input,
);
explainCalls.push(url);
if (url !== EXPLAIN_ROUTE) return { ok: false, status: 404, json: async () => ({}) };
let body: { recordId?: unknown; recordIds?: unknown } = {};
try {
body = JSON.parse(String((init as { body?: unknown } | undefined)?.body ?? '{}'));
} catch {
/* a non-JSON body is not a request this route can answer */
}
const recordIds = Array.isArray(body.recordIds) ? body.recordIds : null;
return {
ok: true,
status: 200,
json: async () =>
recordIds
? { records: recordIds.map((recordId) => ({ recordId, visible: true })) }
: { record: { visible: true } },
};
}),
);
}

describe('calendar drawer width with no declared `navigation` (objectui#6303)', () => {
beforeEach(() => {
drawerProps = null;
try { window.localStorage.clear(); } catch { /* ignore */ }
installExplainDouble();
});
afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the hook's best-effort `catch`.
expect(explainCalls.filter((url) => url !== EXPLAIN_ROUTE)).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. 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 verdict effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});
afterEach(() => cleanup());

it('half 1: the calendar injects no width of its own (so the drawer default applies)', async () => {
await openDrawer();
Expand Down
74 changes: 73 additions & 1 deletion packages/plugin-charts/src/ObjectChart.heightChain.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

import React from 'react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';

vi.mock('./ChartRenderer', () => ({
Expand All @@ -31,8 +31,80 @@ vi.mock('./ChartRenderer', () => ({

import { ObjectChart } from './ObjectChart';

/* ────────────────────────────────────────────────────────────────────────────
* objectui#7307 — this file's `/api/v1/meta/object/task` escape, served here.
*
* Nothing below asks for metadata, yet every run opened a REAL TCP connection
* to `http://localhost:3000`. Traced with a stack probe on the network-escape
* guard's attribution point:
*
* ObjectChart (option-colour effect) packages/plugin-charts/src/ObjectChart.tsx:390
* -> `const doFetch = apiFetch ?? fetch` <- the escape
* -> loadObjectSchema ObjectChart.tsx:411
* -> loadDimensionFieldMeta packages/core/src/utils/chart-series.ts
* GET /api/v1/meta/object/task
*
* That effect reads the host's AUTHENTICATED `apiFetch` off
* `SchemaRendererContext` and, with no `SchemaRendererProvider` in this tree,
* degrades to the GLOBAL `fetch` by design — a standalone embed must keep
* rendering rather than crash. 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 request. The read is best-effort (every
* failure leaves `optionMeta` null and the chart on the theme palette), which
* is why the assertion below stayed green while the request always failed.
*
* Answered from a RECORDING double — the shape objectui#5225 settled on and
* `packages/plugin-report/src/__tests__/DatasetReportRenderer.test.tsx`
* carries. Deliberately NOT a blanket network stub: it records every URL it is
* handed and `afterEach` fails on any URL that is not the metadata route, so an
* escape to somewhere else reds here instead of vanishing into that `catch`.
*
* The served document declares no fields, so nothing resolves and `optionMeta`
* settles null — the state the failing request already produced. The height
* assertion cannot see it either way: `ChartRenderer` is mocked to null.
* ──────────────────────────────────────────────────────────────────────────── */

const META_OBJECT_ROUTE = /^\/api\/v1\/meta\/object\/(.+)$/;

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

/** Serve `/api/v1/meta/object/<name>` with a field-less doc; 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);
const m = META_OBJECT_ROUTE.exec(url);
if (!m) return { ok: false, status: 404, json: async () => ({}) };
return {
ok: true,
status: 200,
json: async () => ({ item: { name: decodeURIComponent(m[1]), fields: {} } }),
};
}),
);
}

beforeEach(() => {
installMetaObjectDouble();
});

afterEach(() => {
// The double is a router, not a sink: an escape to any OTHER endpoint fails
// here instead of vanishing into the effect's best-effort `catch`.
expect(metaCalls.filter((url) => !META_OBJECT_ROUTE.test(url))).toEqual([]);
// Unmount BEFORE restoring the real `fetch`. 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 metadata effect settling in that window
// escapes again (objectui#7439).
cleanup();
vi.unstubAllGlobals();
});

describe('ObjectChart wrapper height chain (objectui#5451)', () => {
Expand Down
Loading
Loading