diff --git a/.changeset/5157-object-map-config-strict.md b/.changeset/5157-object-map-config-strict.md new file mode 100644 index 0000000000..5f749ed24a --- /dev/null +++ b/.changeset/5157-object-map-config-strict.md @@ -0,0 +1,36 @@ +--- +'@object-ui/types': minor +--- + +feat(types): `ObjectMapConfigSchema` is `.strict()`, so `objectui validate` refuses an undeclared key in an `object-map` node's `map` block (objectui#5157) + +⚠️ **The accept set narrows.** `ObjectMapSchema.map` is `ObjectMapConfigSchema`, which used +to be a plain `z.object()`: a key it did not declare was stripped and the node parsed clean. +The card's own typo, `latitudeFieId`, therefore validated clean and was never read, and neither +`objectui validate` nor the runtime named it (only a TypeScript author's compiler did). The block is now closed, as ruled on objectui#5157 (letter A, carrying the earlier +ruling that limits the change to the `map` block): + +- **`safeValidateSchema` / `objectui validate`** refuse a `map` block carrying an undeclared + key with an `unrecognized_keys` issue at `map` that names the key, and exit 1. One level + down — the node inside a container's `children` — the refusal arrives as an + `invalid_union` at `children`, and the arm detail `objectui validate` prints under it + names the key at `children → 0 → map`. +- **`objectui check`** is advisory and only uses validation to recognise a file: a root + `object-map` document with no structural root key now moves into its "did not validate" + list; a document whose root carries a structural key (`children`, `className`, …) is not + validated by `check` and still passes it. +- **The runtime keeps warning, not throwing.** `ObjectMap` still renders; its existing + `safeParse` of the block now fails, so `[ObjectMap] Invalid map configuration` is warned + in the console, naming the key. +- `.shape` is unchanged, so `ObjectMap`'s `FLAT_MAP_CONFIG_KEYS` and the view flatten + whitelists see the same keys. + +**Measured breakage: none.** The pre-landing sweep the ruling required found no `map` block +in authored metadata (objectui examples, apps and docs; objectstack examples and apps) +carrying an undeclared key. Its population is held as a fixture in +`packages/types/src/__tests__/object-map-config-strict-5157.test.ts`, which re-parses every +block under the strict schema on each run; the fixture names the commits the population was +enumerated at, and a block added since is not in it. + +Other component sub-block schemas are untouched: whether they close the same way is a +separate decision. diff --git a/content/docs/plugins/plugin-map.mdx b/content/docs/plugins/plugin-map.mdx index 63eeb16538..263f80505a 100644 --- a/content/docs/plugins/plugin-map.mdx +++ b/content/docs/plugins/plugin-map.mdx @@ -146,6 +146,15 @@ to `onMarkerClick` is not `===` the authored object. } ``` +The block is closed (objectui#5157). A key it does not declare — a typo such as +`latitudeFieId` — is refused by `objectui validate` with an `unrecognized_keys` +issue at `map` that names the key (on a root node; a nested node reports under +`invalid_union` at `children`, with the key in the arm detail). At runtime +`ObjectMap` does not throw: it renders from the declared keys, and the console +warns `[ObjectMap] Invalid map configuration`, naming the same key. A typo that +leaves the block with no coordinate binding, as `latitudeFieId` does, draws the +refusal described under "An unconfigured map refuses" rather than a map. + ## Configuration ### An unconfigured map refuses diff --git a/packages/plugin-map/README.md b/packages/plugin-map/README.md index b55da658f4..397ef3e2c6 100644 --- a/packages/plugin-map/README.md +++ b/packages/plugin-map/README.md @@ -117,7 +117,16 @@ round-trip, so inline rows must be JSON-serializable. ## The `map` block -The declared configuration input. Every key is optional: +The declared configuration input. Every key is optional, and the block is +**closed** (objectui#5157): a key outside this table — a typo such as +`latitudeFieId` — is refused by `objectui validate` with an `unrecognized_keys` +issue at `map` that names it (on a root node; a nested node reports under +`invalid_union` at `children`, with the key in the arm detail). At runtime +`ObjectMap` does not throw: it renders from the declared keys and warns +`[ObjectMap] Invalid map configuration` in the console, naming the same key; the +undeclared key is never read. A typo that leaves the block with no coordinate +binding, as `latitudeFieId` does, therefore draws the "Map configuration +required" refusal below rather than a map. | Key | Description | | --- | --- | diff --git a/packages/plugin-map/src/ObjectMap.strictConfigWarn-5157.test.tsx b/packages/plugin-map/src/ObjectMap.strictConfigWarn-5157.test.tsx new file mode 100644 index 0000000000..791ae59e93 --- /dev/null +++ b/packages/plugin-map/src/ObjectMap.strictConfigWarn-5157.test.tsx @@ -0,0 +1,95 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * objectui#5157 — the RUNTIME face of `ObjectMapConfigSchema.strict()`. + * + * Ruled letter A on objectui#5157: the schema closes, so `safeValidateSchema` + * refuses an undeclared `map` key (pinned in `@object-ui/types`' + * `object-map-config-strict-5157.test.ts`), while the component itself keeps + * the channel it already had — it RENDERS, and `getMapConfig`'s `safeParse` + * of the block now fails and `console.warn`s the issue. Warn, not throw: a + * document the validator refuses still draws on a running page. + * + * Before the strict schema the same block parsed clean with the key stripped, + * so this warning never fired: the card's symptom was a quiet console: no + * warning named the key. The pin therefore reads the warning's ARGUMENTS, not its first line — + * the key arrives in the formatted issue passed after the prefix string. + */ + +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type { ObjectMapSchema } from '@object-ui/types'; +import { ObjectMap } from './ObjectMap'; + +type Slot = { children?: React.ReactNode }; + +vi.mock('react-map-gl/maplibre', () => ({ + default: ({ children }: Slot) =>
{children}
, + Map: ({ children }: Slot) =>
{children}
, + NavigationControl: () =>
, + Marker: ({ children, longitude, latitude }: Slot & { longitude: number; latitude: number }) => ( +
+ {children} +
+ ), + Popup: ({ children }: Slot) =>
{children}
, +})); + +/** The card's own typo: `latitudeFieId`, capital i where the l belongs. */ +const TYPO_KEY = 'latitudeFieId'; + +const ROWS = [{ id: '1', name: 'Ferry Building', location: { lat: 37.7955, lng: -122.3937 } }]; + +let warnSpy: ReturnType; + +beforeEach(() => { + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + warnSpy.mockRestore(); +}); + +/** Every argument of every `[ObjectMap] Invalid map configuration` call, serialised. */ +const invalidConfigWarnings = () => + warnSpy.mock.calls + .filter((args: unknown[]) => String(args[0]).includes('[ObjectMap] Invalid map configuration')) + .map((args: unknown[]) => args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')); + +const renderMap = async (map: Record) => { + const schema = { type: 'object-map', staticData: ROWS, map } as unknown as ObjectMapSchema; + const utils = render(); + await waitFor(() => expect(screen.queryByText('Loading map...')).toBeNull()); + return utils; +}; + +describe('(i) an undeclared `map` key still renders, and the warning names it (objectui#5157)', () => { + it('renders the map and places the marker the declared keys bind', async () => { + await renderMap({ locationField: 'location', titleField: 'name', [TYPO_KEY]: 'lat' }); + + expect(screen.getByLabelText('Map')).toBeTruthy(); + const markers = screen.getAllByTestId('map-marker'); + expect(markers).toHaveLength(1); + expect(markers[0]).toHaveAttribute('data-lat', '37.7955'); + }); + + it('warns through the existing channel, naming the undeclared key', async () => { + await renderMap({ locationField: 'location', titleField: 'name', [TYPO_KEY]: 'lat' }); + + const warned = invalidConfigWarnings(); + expect(warned.length).toBeGreaterThan(0); + expect(warned.join('\n')).toContain(TYPO_KEY); + }); + + it('says nothing about the configuration when every key is declared (control)', async () => { + await renderMap({ locationField: 'location', titleField: 'name' }); + + expect(screen.getAllByTestId('map-marker')).toHaveLength(1); + expect(invalidConfigWarnings()).toEqual([]); + }); +}); diff --git a/packages/types/README.md b/packages/types/README.md index 0468add616..56de4617af 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -127,7 +127,9 @@ type ButtonSchema = SchemaByType<'button'>; - The **rendering face** (`AnyComponentSchema`, `SchemaNodeSchema`, every named mirror) is tolerant: a node may carry keys the schema does not declare, because - renderer props ride through it. + renderer props ride through it. Some declared sub-blocks are closed on this face + as well — an `object-map` node's `map` block is one (objectui#5157) — so a key + misspelled inside one of them is refused by both faces. - The **strict authoring face** is a derived twin that closes every declared object, at every depth. It is meant for authoring-time checking — validating a document a person or an agent just wrote — where an undeclared key is far more diff --git a/packages/types/src/__tests__/object-map-config-strict-5157.test.ts b/packages/types/src/__tests__/object-map-config-strict-5157.test.ts new file mode 100644 index 0000000000..8c665e2a83 --- /dev/null +++ b/packages/types/src/__tests__/object-map-config-strict-5157.test.ts @@ -0,0 +1,186 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#5157 — `ObjectMapConfigSchema` is `.strict()`, so an `object-map` + * node whose `map` block carries an undeclared key is REFUSED on the validate + * face, by name, instead of parsing clean with the key stripped. + * + * Ruled letter A on objectui#5157 (carrying the earlier ruling that limits the + * change to the `map` block): `.strict()` goes on the schema itself, so the + * declared face, the runtime check `ObjectMap` performs and the validate face + * keep ONE accept set. The runtime half (the component still renders and warns, + * naming the key) is pinned beside the renderer, in `plugin-map`'s + * `ObjectMap.strictConfigWarn-5157.test.tsx`. + * + * Three things are pinned here: + * + * (ii) the validate face — `safeValidateSchema` refuses a `map` block carrying + * the card's typo with ONE `unrecognized_keys` issue at `map`, naming the + * key, and still accepts the clean block; + * (iii) the pre-landing sweep, held rather than remembered — every `map` + * block the sweep found in authored metadata parses clean under strict; + * and the boundary: `.shape` did not move, so every key list read from it + * sees the same keys. + */ + +import { describe, it, expect } from 'vitest'; +import { ObjectMapConfigSchema, ObjectMapSchema } from '../zod/objectql.zod.js'; +import { safeValidateSchema } from '../zod/index.zod.js'; + +/** The card's own typo: `latitudeFieId`, capital i where the l belongs. */ +const TYPO_KEY = 'latitudeFieId'; + +const CLEAN_NODE = { + type: 'object-map', + objectName: 'stores', + map: { latitudeField: 'lat', longitudeField: 'lng' }, +}; + +const TYPO_NODE = { + type: 'object-map', + objectName: 'stores', + map: { [TYPO_KEY]: 'lat', longitudeField: 'lng' }, +}; + +/** The same node, one level down: what a page or a view actually carries. */ +const NESTED_TYPO = { type: 'div', children: [TYPO_NODE] }; +const NESTED_CLEAN = { type: 'div', children: [CLEAN_NODE] }; + +/** Each issue reduced to what a consumer keys off: its kind, its place, the keys it names. */ +function issues(result: { success: boolean; error?: { issues: ReadonlyArray<{ code: string; path: PropertyKey[]; keys?: string[] }> } }) { + return (result.error?.issues ?? []).map((i) => ({ code: i.code, path: i.path, keys: i.keys })); +} + +describe('(ii) the validate face refuses an undeclared `map` key, by name (objectui#5157)', () => { + it('refuses the typo block with one `unrecognized_keys` issue at `map`, naming the key', () => { + const result = safeValidateSchema(TYPO_NODE); + expect(result.success).toBe(false); + expect(issues(result)).toEqual([{ code: 'unrecognized_keys', path: ['map'], keys: [TYPO_KEY] }]); + }); + + it('refuses the typo block on the node schema itself, at the same path', () => { + const result = ObjectMapSchema.safeParse(TYPO_NODE); + expect(result.success).toBe(false); + expect(issues(result)).toEqual([{ code: 'unrecognized_keys', path: ['map'], keys: [TYPO_KEY] }]); + }); + + it('refuses the typo block on the config schema, naming the key at the block root', () => { + const result = ObjectMapConfigSchema.safeParse(TYPO_NODE.map); + expect(result.success).toBe(false); + expect(issues(result)).toEqual([{ code: 'unrecognized_keys', path: [], keys: [TYPO_KEY] }]); + }); + + it('refuses the typo block one level down, inside a container', () => { + // The nested refusal arrives as the child slot's union issue, not as a + // named key: that is how the undiscriminated `children` union reports. + // Only the verdict is pinned here; how `objectui validate` renders it is + // measured on the pull request, not pinned. + expect(safeValidateSchema(NESTED_TYPO).success).toBe(false); + }); + + it('still accepts the clean block, alone and nested (control)', () => { + expect(issues(safeValidateSchema(CLEAN_NODE))).toEqual([]); + expect(safeValidateSchema(CLEAN_NODE).success).toBe(true); + expect(safeValidateSchema(NESTED_CLEAN).success).toBe(true); + }); +}); + +describe('`.strict()` closes the block without moving its shape (objectui#5157)', () => { + it('declares the same eight keys, so every key list read from `.shape` is unchanged', () => { + // `ObjectMap`'s `FLAT_MAP_CONFIG_KEYS` is this list minus `style`; the view + // flatten whitelists are pinned against it in their own packages. + expect(Object.keys(ObjectMapConfigSchema.shape).sort()).toEqual([ + 'center', + 'descriptionField', + 'latitudeField', + 'locationField', + 'longitudeField', + 'style', + 'titleField', + 'zoom', + ]); + }); + + it('accepts every declared key together', () => { + const everything = { + latitudeField: 'lat', + longitudeField: 'lng', + locationField: 'location', + titleField: 'name', + descriptionField: 'address', + zoom: 12, + center: [37.7749, -122.4194], + style: 'https://tiles.example.com/style.json', + }; + expect(issues(ObjectMapConfigSchema.safeParse(everything))).toEqual([]); + }); +}); + +/** + * (iii) The pre-landing sweep the first ruling on objectui#5157 required, held + * as a fixture so its verdict is re-derived on every run instead of quoted. + * + * ⚠️ The POPULATION is a snapshot, and nothing here re-enumerates it: it is + * every `map` block the sweep found in authored metadata, enumerated with + * `git ls-tree` and a `git grep` for a `map` key over objectui `examples` / + * `apps` / `content` at `40c076fc2d` and objectstack `examples` / `apps` at + * `3cf6449389`, with every grep hit reconciled by hand (translation labels, + * view names and catalog entry ids keyed `map` are not config blocks). A block + * added after those commits is NOT in this list. The three catalog entries are + * also re-read live from disk and validated whole by `plugin-map`'s + * `ObjectMap.catalogRecordSource-6939.test.tsx`. + * + * Each row names its source by path and a quoted anchor — ⛔ never a line + * address (AGENTS.md #11). + */ +const SWEEP: ReadonlyArray<{ repo: 'objectui' | 'objectstack'; path: string; anchor: string; block: Record }> = [ + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: '### Basic Usage with ObjectQL', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name', descriptionField: 'address' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: '### With Static Data', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name', descriptionField: 'address' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const storeMap', block: { latitudeField: 'latitude', longitudeField: 'longitude', titleField: 'storeName', descriptionField: 'storeAddress' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const placeMap', block: { locationField: 'coordinates', titleField: 'placeName', descriptionField: 'description' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const cameraMap', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name', zoom: 12, center: [37.7749, -122.4194] } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const retailStores', block: { latitudeField: 'store_lat', longitudeField: 'store_lng', titleField: 'store_name', descriptionField: 'store_address' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const staticLocations', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const clickableMap', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const storeLocator', block: { latitudeField: 'latitude', longitudeField: 'longitude', titleField: 'storeName', descriptionField: 'fullAddress', zoom: 10, center: [37.7749, -122.4194] } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const deliveryMap', block: { latitudeField: 'current_lat', longitudeField: 'current_lng', titleField: 'driver_name', descriptionField: 'delivery_address' } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const propertyMap', block: { latitudeField: 'property_lat', longitudeField: 'property_lng', titleField: 'property_address', descriptionField: 'property_details', zoom: 12 } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const venueMap', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'venueName', descriptionField: 'details', zoom: 11 } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const serviceMap', block: { latitudeField: 'customer_latitude', longitudeField: 'customer_longitude', titleField: 'customer_name', descriptionField: 'service_type', zoom: 10 } }, + { repo: 'objectui', path: 'content/docs/plugins/plugin-map.mdx', anchor: 'const mapConfig: ObjectMapConfig', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name', descriptionField: 'description', zoom: 12, center: [37.7749, -122.4194] } }, + { repo: 'objectui', path: 'content/docs/fields/location.mdx', anchor: '**2. The map over the same field.**', block: { locationField: 'location', titleField: 'name', zoom: 12, center: [37.7749, -122.4194] } }, + { repo: 'objectui', path: 'examples/schema-catalog/src/schemas/plugin-map/event-venue-finder.json', anchor: '"map"', block: { latitudeField: 'latitude', longitudeField: 'longitude', titleField: 'venue', descriptionField: 'info', zoom: 11 } }, + { repo: 'objectui', path: 'examples/schema-catalog/src/schemas/plugin-map/real-time-delivery-tracking.json', anchor: '"map"', block: { latitudeField: 'current_lat', longitudeField: 'current_lng', titleField: 'driver', descriptionField: 'destination', zoom: 12 } }, + { repo: 'objectui', path: 'examples/schema-catalog/src/schemas/plugin-map/store-locator-map.json', anchor: '"map"', block: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name', descriptionField: 'address', zoom: 9 } }, + { repo: 'objectstack', path: 'examples/app-showcase/src/ui/views/task.view.ts', anchor: "label: 'Work Locations (Map)'", block: { titleField: 'title', locationField: 'location' } }, + { repo: 'objectstack', path: 'examples/app-showcase/test/task-map-marker-title.test.ts', anchor: "name: 'showcase_task_map_direct_probe'", block: { titleField: 'title', locationField: 'location' } }, +]; + +describe('(iii) every `map` block the pre-landing sweep found parses clean under strict (objectui#5157)', () => { + it('holds the swept population, split as the sweep found it', () => { + // Non-vacuity, not a live count: the sweep's own population at the two + // commits named above. A row removed here without a reason turns this red. + expect(SWEEP.filter((row) => row.repo === 'objectui')).toHaveLength(18); + expect(SWEEP.filter((row) => row.repo === 'objectstack')).toHaveLength(2); + }); + + it.each(SWEEP.map((row) => [`${row.repo} ${row.path} (${row.anchor})`, row.block] as const))( + '%s parses clean as a map block', + (_label, block) => { + expect(issues(ObjectMapConfigSchema.safeParse(block))).toEqual([]); + }, + ); + + it.each(SWEEP.map((row) => [`${row.repo} ${row.path} (${row.anchor})`, row.block] as const))( + '%s validates as the `map` block of an object-map node', + (_label, block) => { + expect(issues(safeValidateSchema({ type: 'object-map', objectName: 'stores', map: block }))).toEqual([]); + }, + ); +}); diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index e2c5617349..d1b031dd3a 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -1865,6 +1865,29 @@ export type ListViewInferred = z.input; * so the declared authoring face and the validation the renderer performs are * ONE schema rather than two that can drift (objectui#5018). `ObjectMap` * imports this exact object; it no longer declares its own. + * + * ## `.strict()` — an undeclared key is REFUSED, not stripped (objectui#5157) + * + * The TypeScript twin `ObjectMapConfig` is a closed interface, so a misspelled + * key (`latitudeFieId`) was a compile error for a typed author and nothing at + * all for untyped metadata: this object stripped it and parsed clean, and + * nothing named it: the card's typo drew the generic "Map configuration + * required" refusal (objectui#8169), which names the key the author meant, not + * the one they wrote. Closing the block makes all three faces + * one accept set, as ruled on objectui#5157 (letter A, carrying the earlier + * "the `map` block only" ruling): + * + * - runtime: `ObjectMap`'s `safeParse` of the block now fails, so the + * component still renders and `console.warn`s the issue, which names the key; + * - validate: `ObjectMapSchema.map` is this object, so `safeValidateSchema` + * (and `objectui validate` with it) refuses the node with an + * `unrecognized_keys` issue at `map`; + * - `.shape` is untouched, so `ObjectMap`'s `FLAT_MAP_CONFIG_KEYS` (derived + * from it) and the view flatten whitelists (hand-listed, pinned against it) + * see the same keys as before. + * + * ⛔ The map block ONLY. Whether other component sub-block schemas close the + * same way is a separate decision the ruling kept out of this card. */ export const ObjectMapConfigSchema = z.object({ latitudeField: z.string().optional().describe('Field containing latitude'), @@ -1875,7 +1898,7 @@ export const ObjectMapConfigSchema = z.object({ zoom: z.number().optional().describe('Zoom level (1-20); declaring it opts out of the auto-fit'), center: z.tuple([z.number(), z.number()]).optional().describe('Center [lat, lng]; declaring it opts out of the auto-fit'), style: z.string().optional().describe('MapLibre style URL/spec (overrides the public demo default)'), -}); +}).strict(); /** * `77cb489b4` — the record-source refinement `ObjectMapSchema`, diff --git a/scripts/markdown-test-inputs.mjs b/scripts/markdown-test-inputs.mjs index 2812b0f535..2d4638741c 100644 --- a/scripts/markdown-test-inputs.mjs +++ b/scripts/markdown-test-inputs.mjs @@ -476,6 +476,18 @@ export const ADJUDICATED = new Map([ 'packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts', { reads: ['content/docs/plugins/plugin-kanban.mdx'] }, ], + // objectui#5157. Reads no markdown: the two document literals are PROVENANCE + // on the rows of its sweep fixture (every `map` block the pre-landing sweep + // found in authored metadata, copied into the test and parsed under the strict + // schema). The test imports no `fs` and opens neither page, so an edit to + // either one cannot move its verdict. + [ + 'packages/types/src/__tests__/object-map-config-strict-5157.test.ts', + { + reads: [], + notRead: ['content/docs/fields/location.mdx', 'content/docs/plugins/plugin-map.mdx'], + }, + ], [ 'packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts', {