From bb835ea72f2f5b176a1576ee5a19b0710643a4d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 00:29:51 +0000 Subject: [PATCH 1/2] fix(plugin-report): register under the namespace its consumers declare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/plugin-report` registered `report`, `spec-report` and `report-viewer` under namespace `report`, while `apps/console/src/register-plugins.ts` declared the lazy stubs for the same three short names under `plugin-report` and `packages/cli/src/utils/known-schema-types.ts` shipped the `plugin-report:*` spellings as renderable. `Registry.register` clears the lazy stub for the type IT registers, and that type was `report:report` — so the three `plugin-report:*` stubs were never cleared, no component was ever stored under them, and a schema authored with a whitelisted key resolved to nothing. The bare `report` key was also claimed twice under two different namespaces, so what it DECLARED depended on whether the plugin chunk had loaded yet (the objectui#6353 shape). Direction chosen by measurement: no `report:*` spelling is authored anywhere in this repo or the sibling `objectstack` checkout (0 hits), while the bare spellings are authored in 48 places. Move the package to `plugin-report`, retire the `report:*` keys, and regenerate the whitelist. The bare keys stay claimed deliberately and must not take `skipFallback`: both claimants now name the same full type, so suppressing either would strand the only spelling anything authors. Two pins outlive the fix: a fresh-`Registry` replay of the real declared metadata plus a console-shaped lazy stub in BOTH registration orders, checking the bare key's declared namespace after every step; and a derivation-based pin that fails if the plugin, the console stubs and the generated whitelist ever disagree again. Fixes #6416 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q --- .changeset/6416-plugin-report-namespace.md | 48 ++++ packages/cli/src/utils/known-schema-types.ts | 3 - .../report-bare-key-ownership.test.ts | 212 ++++++++++++++++++ packages/plugin-report/src/index.tsx | 40 +++- .../report-namespace-agreement-6416.test.ts | 101 +++++++++ 5 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 .changeset/6416-plugin-report-namespace.md create mode 100644 packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts create mode 100644 scripts/__tests__/report-namespace-agreement-6416.test.ts diff --git a/.changeset/6416-plugin-report-namespace.md b/.changeset/6416-plugin-report-namespace.md new file mode 100644 index 0000000000..41d0b87d0c --- /dev/null +++ b/.changeset/6416-plugin-report-namespace.md @@ -0,0 +1,48 @@ +--- +'@object-ui/plugin-report': patch +'@object-ui/cli': patch +--- + +`@object-ui/plugin-report` now registers its three components under namespace +**`plugin-report`**, the spelling its consumers already declare (objectui#6416). + +It used to register `report`, `spec-report` and `report-viewer` under namespace +`report`, while `apps/console` declared the lazy stubs for the same three short +names under `plugin-report` and the CLI's known-type whitelist shipped the +`plugin-report:*` spellings as renderable. Two things followed from the +disagreement: + +- **`plugin-report:report`, `plugin-report:report-viewer` and + `plugin-report:spec-report` could never be satisfied.** `Registry.register` + clears the lazy stub for the type IT registers, and that type was + `report:report`, so those three stubs were never cleared and no component was + ever stored under them: `get('report', 'plugin-report')` returned `undefined` + and `hasLazy('report', 'plugin-report')` stayed `true` forever. A schema + authored with any of the three whitelisted keys resolved to nothing — the + gate handed authors a green light for a key the runtime could not satisfy. +- **The bare `report` key was claimed twice under two different namespaces.** + `Registry.register` and `Registry.registerLazy` share the + `meta?.namespace && !meta?.skipFallback` branch, so what bare `report` + *declared* depended on whether the plugin chunk had loaded yet — the + objectui#6353 shape. + +**No authored metadata changes.** The direction was chosen by measurement: +nothing in this repository, and nothing in the sibling `objectstack` checkout, +authors a `report:*` spelling (0 hits), while the bare spellings are authored in +48 places. `type: 'report'`, `type: 'spec-report'` and `type: 'report-viewer'` +resolve exactly as before; the three unreachable `report:*` keys are retired and +the three `plugin-report:*` keys now name real components for the first time. + +`packages/cli/src/utils/known-schema-types.ts` is regenerated from the +registrations, dropping `report:report`, `report:report-viewer` and +`report:spec-report`. + +Two pins are the half that outlives the fix: +`packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts` replays +this package's real declared metadata and a console-shaped lazy stub into a +fresh `Registry` in **both** registration orders, checking the bare key's +declared namespace after every step, so order- and phase-independence are +properties under test rather than properties of the file the test imports. +`scripts/__tests__/report-namespace-agreement-6416.test.ts` re-derives both +sites from source and fails if the plugin, the console stubs and the generated +whitelist ever disagree again. diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index 6e4a845319..0a59f4133d 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -465,9 +465,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'report', 'report-designer', 'report-viewer', - 'report:report', - 'report:report-viewer', - 'report:spec-report', 'resizable', 'responsive-grid', 'richtext', diff --git a/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts b/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts new file mode 100644 index 0000000000..3f110d662a --- /dev/null +++ b/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts @@ -0,0 +1,212 @@ +/** + * 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. + */ + +/** + * Which namespace this package registers under, and who owns the bare + * `report` / `spec-report` / `report-viewer` keys (objectui#6416). + * + * This package used to register all three short names under namespace + * `report`, while `apps/console/src/register-plugins.ts` declared the lazy + * stubs for the same three names under `plugin-report` — the spelling every + * sibling plugin uses and the one the CLI's known-type whitelist ships. Two + * consequences followed, and both are pinned below: + * + * 1. THE BARE KEY WAS DOUBLE-CLAIMED, PHASE-DEPENDENTLY. Neither site passed + * `skipFallback`, so both also claimed the bare key (`Registry.register` + * and `Registry.registerLazy` share the `meta?.namespace && + * !meta?.skipFallback` branch). Before the chunk loaded, bare `report` + * declared namespace `plugin-report` (from the stub); after it loaded, the + * same key declared `report` (from this package). Which answer a host got + * depended on when it asked — the objectui#6353 shape. + * 2. THE NAMESPACED KEYS WERE UNSATISFIABLE. `register()` clears the lazy + * stub for the type it is registering, and the type it registered was + * `report:report` — so `plugin-report:report` was never cleared and no + * component was ever stored under it. `get('report', 'plugin-report')` + * stayed `undefined` and `hasLazy('report', 'plugin-report')` stayed + * `true` forever, while `packages/cli/src/utils/known-schema-types.ts` + * listed all three `plugin-report:*` spellings as renderable. + * + * The fix is the one direction the measurement chartered: nothing in this + * repository, and nothing in the sibling `objectstack` checkout, ever authored + * a `report:*` spelling (0 hits), while the bare spellings are authored in 48 + * places — so the package moves to `plugin-report` and the `report:*` keys are + * retired. Both claimants of each bare key now name the SAME full type, which + * is the shape all 27 other console-stub/plugin pairs in this repo already + * have, so the bare key has one owner by construction rather than by whichever + * phase the host happened to observe. + * + * WHY THE REPLAY (`in EITHER registration order`): asserting only today's + * resolved outcome cannot tell "declared" apart from "happened to be observed + * after the right step". The replay reads this package's REAL declared + * metadata back out of the registry — nothing here is a hand-copied mirror of + * `../index` — re-registers it into a fresh `Registry` alongside a + * console-shaped lazy stub, in both orders, and checks the bare key's declared + * namespace after EVERY step. Order- and phase-independence are then + * properties under test rather than properties of the file this test imports. + * + * The cross-site half — that `apps/console` and the generated CLI whitelist + * really do spell it `plugin-report` — is pinned from the repo's own + * registration derivation in + * `scripts/__tests__/report-namespace-agreement-6416.test.ts`. + */ +import { describe, it, expect, vi } from 'vitest'; +import { ComponentRegistry, Registry, type ComponentMeta } from '@object-ui/core'; +// Importing the package entry is what performs all three registrations, exactly +// as a host does. The renderers are compared by IDENTITY below, so these pins +// cannot be satisfied by a look-alike. +import { ReportRenderer, ReportViewer } from '../index'; + +/** + * The consumer-facing spelling: what `apps/console` declares for its lazy + * stubs and what the CLI whitelist ships. It is stated rather than read out of + * this package's own metadata on purpose — deriving it from the thing under + * test would make the comparison circular. + */ +const NS = 'plugin-report'; + +/** The three short names this package registers, and the renderer each owns. */ +const REGISTRATIONS: Array<[short: string, renderer: unknown]> = [ + ['report', ReportRenderer], + // Spec-native alias — same dispatcher, explicit name for spec-driven hosts. + ['spec-report', ReportRenderer], + ['report-viewer', ReportViewer], +]; + +const COLLISION_WARNING = 'bare-name fallback is being overwritten'; + +/** Every LOADED namespaced key whose short name is `short`. */ +function namespacedKeysFor(short: string): string[] { + return ComponentRegistry.getAllTypes() + .filter((t) => t.includes(':') && t.slice(t.indexOf(':') + 1) === short) + .sort(); +} + +/** + * The registration this package actually declared — component plus meta, read + * back from the registry rather than restated here. + */ +function declaredRegistration(short: string): { component: unknown; meta: ComponentMeta } { + const config = ComponentRegistry.getConfig(short, NS); + expect(config, `nothing is registered as "${NS}:${short}"`).toBeDefined(); + // `type` is the FULL type (`ns:name`); `register` re-derives it from the bare + // name + namespace, so replaying it would double the prefix. + const { type: _fullType, component, ...meta } = config!; + return { component, meta: meta as ComponentMeta }; +} + +/** The lazy stub `apps/console/src/register-plugins.ts` declares for `short`. */ +function consoleStubMeta(): ComponentMeta { + return { namespace: NS, category: 'view' } as ComponentMeta; +} + +describe('plugin-report registers under the namespace its consumers declare', () => { + it.each(REGISTRATIONS)( + '"%s" is registered under exactly one namespace, and it is plugin-report', + (short) => { + expect( + namespacedKeysFor(short), + `"${short}" must be registered as "${NS}:${short}" and nothing else — a second ` + + 'namespaced spelling is a key the CLI whitelist and the console stubs cannot ' + + 'both satisfy (objectui#6416)', + ).toEqual([`${NS}:${short}`]); + }, + ); + + it('no `report:*` key survives — that spelling is retired', () => { + expect(ComponentRegistry.getAllTypes().filter((t) => t.startsWith('report:'))).toEqual([]); + }); + + it.each(REGISTRATIONS)( + '"%s" resolves to its renderer by BOTH the bare and the namespaced key', + (short, renderer) => { + // The namespaced lookup is the one the console stub and the CLI whitelist + // name; before this fix it was `undefined` for all three. + expect(ComponentRegistry.get(short, NS)).toBe(renderer); + // The bare lookup is the only spelling anything in this repo authors. + expect(ComponentRegistry.get(short)).toBe(renderer); + }, + ); +}); + +// The pin the card is actually about. Both rows replay the SAME declared +// metadata into a fresh registry; only the sequence differs. The bare key's +// declared namespace is checked after EVERY step, so a mismatch between the +// stub and the real registration reddens here even though each step on its own +// succeeds. +const ORDERS: Array<[label: string, order: Array<'stub' | 'eager'>]> = [ + ['stub first — what the console does: boot stubs, then the chunk loads', ['stub', 'eager']], + ['eager first — a host that imports the package before declaring stubs', ['eager', 'stub']], +]; + +describe.each(REGISTRATIONS)( + 'bare "%s" ownership is declared, not decided by registration order', + (short, renderer) => { + it.each(ORDERS)('resolves the same way in EITHER order (%s)', (_label, order) => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + const fresh = new Registry(); + const { component, meta } = declaredRegistration(short); + + for (const step of order) { + if (step === 'stub') { + fresh.registerLazy(short, () => Promise.resolve({}), consoleStubMeta()); + } else { + fresh.register(short, component, meta); + } + // The invariant that used to break: whoever last touched the bare key + // must declare the SAME namespace, at every point in the sequence. + expect( + fresh.getMeta(short)?.namespace, + `after the "${step}" step in order [${order.join(', ')}], bare "${short}" declares a ` + + 'different namespace than the step before it — the bare key is double-claimed ' + + 'again (objectui#6416)', + ).toBe(NS); + } + + expect( + fresh.get(short), + `registering in the order [${order.join(', ')}] changed who answers bare "${short}"`, + ).toBe(renderer); + // The key the CLI whitelist declares renderable must name a real + // component once the chunk has loaded, in either order. + expect(fresh.get(short, NS)).toBe(renderer); + + // The registry's collision guard is the mechanism this class of fix + // uses, so its silence is part of the contract: a warning here means two + // registrations are fighting over the bare key again. + const collided = warn.mock.calls.some( + (args: unknown[]) => typeof args[0] === 'string' && args[0].includes(COLLISION_WARNING), + ); + expect(collided, 'the registry warned that the bare-name fallback was overwritten').toBe( + false, + ); + } finally { + warn.mockRestore(); + } + }); + + it('the real registration clears the console stub instead of stranding it', () => { + const fresh = new Registry(); + const { component, meta } = declaredRegistration(short); + fresh.registerLazy(short, () => Promise.resolve({}), consoleStubMeta()); + expect(fresh.hasLazy(short, NS)).toBe(true); + + fresh.register(short, component, meta); + + // Consequence 2: while the namespaces disagreed, `register()` deleted + // `report:` and left `plugin-report:` pending forever, so + // the whitelisted key was permanently unrenderable. + expect( + fresh.hasLazy(short, NS), + `"${NS}:${short}" is still a pending lazy stub after the module registered — the ` + + 'registration is landing under a different full type (objectui#6416)', + ).toBe(false); + expect(fresh.hasLazy(short)).toBe(false); + }); + }, +); diff --git a/packages/plugin-report/src/index.tsx b/packages/plugin-report/src/index.tsx index 841c300c7a..7c1223bc18 100644 --- a/packages/plugin-report/src/index.tsx +++ b/packages/plugin-report/src/index.tsx @@ -37,12 +37,46 @@ export type { // `queryDataset`, so the client-side path had no remaining consumers. export { mergeFilters } from './mergeFilters'; +// NAMESPACE — `plugin-report`, the spelling this package's consumers declare +// (objectui#6416). +// +// These three registrations used to name namespace `report`, while +// `apps/console/src/register-plugins.ts` declared the lazy stubs for the same +// three short names under `plugin-report` and +// `packages/cli/src/utils/known-schema-types.ts` shipped the `plugin-report:*` +// spellings as renderable. Two things followed from the disagreement: +// +// * `plugin-report:report` / `:report-viewer` / `:spec-report` could never be +// satisfied. `register()` clears the lazy stub for the type IT registers, +// and that type was `report:report`, so the `plugin-report:*` stubs were +// never cleared and no component was ever stored under them — +// `get('report', 'plugin-report')` stayed undefined and +// `hasLazy('report', 'plugin-report')` stayed true forever. +// * The bare `report` key was claimed twice under two different namespaces +// (`Registry.register` and `registerLazy` share the `meta?.namespace && +// !meta?.skipFallback` branch), so what bare `report` DECLARED depended on +// whether the chunk had loaded yet — the objectui#6353 shape. +// +// Direction chosen by measurement, not by preference: nothing in this repo or +// the sibling `objectstack` checkout authors a `report:*` spelling (0 hits), +// while the bare spellings are authored in 48 places, so the `report:*` keys +// are retired rather than the consumer-facing ones. Every sibling plugin +// already namespaces by package name. +// +// The bare keys stay claimed here ON PURPOSE and must NOT take `skipFallback`: +// after this change both claimants of each bare key — the console's lazy stub +// and the registration below — name the SAME full type, which is the shape all +// 27 other console-stub/plugin pairs in this repo have. Suppressing either +// claim would strand bare `report`, the only spelling anything authors. +// Pinned by `./__tests__/report-bare-key-ownership.test.ts` and +// `scripts/__tests__/report-namespace-agreement-6416.test.ts`. + // Register report component (dispatches dataset-bound vs legacy automatically) ComponentRegistry.register( 'report', ReportRenderer, { - namespace: 'report', + namespace: 'plugin-report', label: 'Report', category: 'Report', inputs: [ @@ -58,7 +92,7 @@ ComponentRegistry.register( 'spec-report', ReportRenderer, { - namespace: 'report', + namespace: 'plugin-report', label: 'Spec Report', category: 'Report', inputs: [ @@ -73,7 +107,7 @@ ComponentRegistry.register( 'report-viewer', ReportViewer, { - namespace: 'report', + namespace: 'plugin-report', label: 'Report Viewer', category: 'Report', inputs: [ diff --git a/scripts/__tests__/report-namespace-agreement-6416.test.ts b/scripts/__tests__/report-namespace-agreement-6416.test.ts new file mode 100644 index 0000000000..a60eff6108 --- /dev/null +++ b/scripts/__tests__/report-namespace-agreement-6416.test.ts @@ -0,0 +1,101 @@ +/** + * objectui#6416 — the three `plugin-report` component keys must be DECLARED and + * REGISTERED under the same namespace. + * + * `packages/plugin-report/src/index.tsx` registered `report`, `spec-report` and + * `report-viewer` under namespace `report`. `apps/console/src/register-plugins.ts` + * declared the lazy stubs for the same three short names under `plugin-report`, + * and `packages/cli/src/utils/known-schema-types.ts` — generated from both sites + * by `regenerate-known-schema-types.mjs` — therefore shipped SIX namespaced + * spellings for THREE components. Three of them (`plugin-report:*`) named + * nothing: `Registry.register` clears the lazy stub for the type it registers, + * and that type was `report:report`, so the `plugin-report:*` stubs stayed + * pending forever and a schema authored with one of them resolved to nothing. + * + * That is a declared-but-unenforceable surface in the whitelist itself — the + * gate handed authors a green light for a key the runtime can never satisfy, + * the exact failure direction `known-schema-types-derivation-5115.test.ts` + * exists to keep out of the list. + * + * The comparison here is EXTRACTIVE: both halves are re-read from source on + * every run by `deriveRegistryKeys` — the same derivation that feeds the + * whitelist and judges documentation snippets — so nothing in this file is a + * copy that can drift from the registrations it describes. The complementary + * pin, that the resulting bare keys have one owner in any registration order, + * is `packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts`. + */ + +import { describe, expect, it } from 'vitest'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// Plain-JS CI helper; types are inferred from the `.mjs` source by +// `tsconfig.scripts.json` (`allowJs`). See objectui#3494. +import { deriveRegistryKeys } from '../check-doc-component-types.mjs'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const derived = deriveRegistryKeys(repoRoot); + +/** The namespace the console stubs and the CLI whitelist name. */ +const NS = 'plugin-report'; +/** The three short names this plugin owns. */ +const SHORT_NAMES = ['report', 'report-viewer', 'spec-report'] as const; + +const PLUGIN_SITE = 'packages/plugin-report/src/index.tsx'; +const CONSOLE_SITE = 'apps/console/src/register-plugins.ts'; + +/** Sites that claim `key`, with the `:line` suffix stripped. */ +function filesClaiming(key: string): string[] { + const sites: string[] | undefined = derived.keys.get(key); + return [...new Set((sites ?? []).map((s) => s.replace(/:\d+$/, '')))].sort(); +} + +/** Every derived key whose short name (the part after the first `:`) is `short`. */ +function namespacedSpellingsOf(short: string): string[] { + return [...derived.keys.keys()] + .filter((k: string) => k.includes(':') && k.slice(k.indexOf(':') + 1) === short) + .sort(); +} + +describe('the derivation this pin trusts', () => { + it('resolves every registration site — an unresolved one would shrink the universe silently', () => { + expect(derived.findings).toEqual([]); + }); + + it('is not vacuous: it finds registrations across many files', () => { + // Guards the failure mode where a moved directory makes the walk empty and + // every set comparison below passes by comparing nothing to nothing. + expect(derived.counters.resolved).toBeGreaterThan(100); + expect(derived.keys.size).toBeGreaterThan(300); + }); +}); + +describe('plugin-report keys are registered under the namespace their consumers declare', () => { + it.each(SHORT_NAMES)('"%s" has exactly one namespaced spelling, and it is plugin-report', (short) => { + expect( + namespacedSpellingsOf(short), + `"${short}" resolves to one component, so it must have one namespaced spelling. A second ` + + 'one means the console stubs and the plugin registration disagree, and the whitelist ' + + 'ships a key nothing can satisfy (objectui#6416)', + ).toEqual([`${NS}:${short}`]); + }); + + it.each(SHORT_NAMES)('"%s" is claimed as `plugin-report:%s` by BOTH the stub and the plugin', (short) => { + // The bug this pins: `plugin-report:` used to be claimed by the + // console alone — declared renderable by the whitelist, satisfied by + // nothing, because the plugin registered `report:` instead. + expect(filesClaiming(`${NS}:${short}`)).toEqual([CONSOLE_SITE, PLUGIN_SITE].sort()); + }); + + it.each(SHORT_NAMES)('bare "%s" is claimed by both sites, so the lazy stub loads the real thing', (short) => { + // Both sites still claim the bare key — deliberately. They now name the same + // full type, so `register()` clears the stub it replaces. `skipFallback` on + // either one would strand the bare spelling, which is the only spelling + // anything in this repository authors. + expect(filesClaiming(short)).toEqual([CONSOLE_SITE, PLUGIN_SITE].sort()); + }); + + it('the `report:*` namespace is retired entirely', () => { + expect([...derived.keys.keys()].filter((k: string) => k.startsWith('report:')).sort()).toEqual([]); + }); +}); From d46f6c9e286fe0913460d0692da2da44ea593eca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 26 Aug 2026 00:33:37 +0000 Subject: [PATCH 2/2] test(plugin-report): read the declared namespace instead of presupposing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The replay fetched the eager registration with `getConfig(short, NS)`, which selects for the very namespace the per-step assertion then checks — so that assertion could never fail. Look the key up by short name and report whatever namespace was declared, so a namespace regression reddens inside the replay rather than only at the top-level spelling check. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q --- .../__tests__/report-bare-key-ownership.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts b/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts index 3f110d662a..c3c5e256d1 100644 --- a/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts +++ b/packages/plugin-report/src/__tests__/report-bare-key-ownership.test.ts @@ -89,12 +89,23 @@ function namespacedKeysFor(short: string): string[] { /** * The registration this package actually declared — component plus meta, read * back from the registry rather than restated here. + * + * The lookup deliberately does NOT pass `NS`: it finds the namespaced key by + * short name and reports whatever namespace was declared. Asking for + * `getConfig(short, NS)` instead would presuppose the answer, and the replay's + * per-step namespace check below could then never fail — it would be reading a + * value it had already selected for. */ function declaredRegistration(short: string): { component: unknown; meta: ComponentMeta } { - const config = ComponentRegistry.getConfig(short, NS); - expect(config, `nothing is registered as "${NS}:${short}"`).toBeDefined(); + const keys = namespacedKeysFor(short); + expect( + keys, + `"${short}" must have exactly one namespaced spelling for the replay to mean anything`, + ).toHaveLength(1); // `type` is the FULL type (`ns:name`); `register` re-derives it from the bare // name + namespace, so replaying it would double the prefix. + const config = ComponentRegistry.getConfig(keys[0]); + expect(config, `nothing is registered as "${keys[0]}"`).toBeDefined(); const { type: _fullType, component, ...meta } = config!; return { component, meta: meta as ComponentMeta }; }