From c517c72fbf0a6ae579873077588453446fc14438 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 00:44:26 +0000 Subject: [PATCH] fix(console,cli,docs): retire three chart keys registered as stubs the charts plugin never fulfils `apps/console` registered ten chart variants as `registerLazy` stubs pointing at `@object-ui/plugin-charts`. That package registers eight keys, and three of the ten -- `line-chart`, `area-chart`, `advanced-chart` -- were not among them. An unfulfilled stub does not fail, it succeeds at being useless. At render, `SchemaRenderer`'s lazy branch re-checks `hasLazy(type)` on every pass and `Registry.register()` deletes a lazy entry only for the keys the loaded module actually registers -- so an unfulfilled key keeps its entry and every pass returns the placeholder. Measured through the real chain: `line-chart` painted `Loading line-chart...` permanently, with no alert and no error, rather than the OBJUI-001 panel. At authoring, a stub is enough to enter `getKnownTypes()`, so `check:doc-types` and the CLI's `KNOWN_SCHEMA_TYPES` snapshot blessed all three and `content/docs/plugins/plugin-dashboard.mdx` taught one of them. Removal rather than implementation: `line-chart` / `area-chart` duplicate families the plugin already draws as `chart` + `chartType`, `advanced-chart` named an internal module, and an authored-usage sweep over both repositories found exactly one occurrence -- the doc snippet corrected here -- against lit controls in the same commands. Both console stub loops move, because the doc gate's key universe is their union; the dashboard snippet is corrected to `chart` + `chartType: "line"`; the generated CLI snapshot is regenerated; and the `line-chart` leg of `node-slot-registered-arms-8499.test.ts` is re-pointed, reading the stub list rather than the file so the retirement comment cannot satisfy it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ --- .changeset/8760-unfulfilled-chart-stubs.md | 62 ++++++ .../unfulfilled-chart-stubs-8760.test.tsx | 202 ++++++++++++++++++ apps/console/src/preview-gallery.tsx | 9 +- apps/console/src/register-plugins.ts | 23 +- content/docs/plugins/plugin-dashboard.mdx | 22 +- ...fulfilled-chart-stubs-retired-8760.test.ts | 164 ++++++++++++++ packages/cli/src/utils/known-schema-types.ts | 6 - .../node-slot-registered-arms-8499.test.ts | 65 ++++-- 8 files changed, 524 insertions(+), 29 deletions(-) create mode 100644 .changeset/8760-unfulfilled-chart-stubs.md create mode 100644 apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx create mode 100644 packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts diff --git a/.changeset/8760-unfulfilled-chart-stubs.md b/.changeset/8760-unfulfilled-chart-stubs.md new file mode 100644 index 0000000000..9baccf564a --- /dev/null +++ b/.changeset/8760-unfulfilled-chart-stubs.md @@ -0,0 +1,62 @@ +--- +'@object-ui/console': minor +'@object-ui/cli': minor +'@object-ui/types': minor +--- + +Retire `line-chart`, `area-chart` and `advanced-chart` — three chart component keys the +console registered as lazy stubs that `@object-ui/plugin-charts` never fulfilled +(objectui#8760). + +**The defect, and why it outlived the checks.** `apps/console` registered ten chart +variants as `registerLazy` stubs pointing at `@object-ui/plugin-charts`. That package +registers eight keys; three of the ten were not among them. An unfulfilled stub does not +fail — it succeeds at being useless, in both of the places that decide whether a defect +is ever seen: + +- **At render.** `SchemaRenderer`'s lazy branch re-checks `hasLazy(type)` on every pass + and returns the `Loading …` placeholder. `Registry.register()` deletes a lazy + entry only for keys the loaded module actually registers, so for an unfulfilled key the + entry SURVIVES the load and every later pass takes the same branch. Measured on + `b775500af` through the real chain: `{ "type": "line-chart" }` painted + `role="status"` / `data-lazy-loading="line-chart"` / `Loading line-chart…`, + permanently. **Not** the `OBJUI-001` panel the card expected — no alert, no error, no + console warning. A skeleton that never resolves reads to a user as a slow network. +- **At authoring.** A stub is enough to put a key into `getKnownTypes()`, so + `check:doc-types` and the CLI's generated `KNOWN_SCHEMA_TYPES` snapshot both blessed + all three. `content/docs/plugins/plugin-dashboard.mdx` taught `"type": "line-chart"` + inside a `card` body, and every gate was green on it. + +So the failure was strictly worse than an unknown key: an unknown key is refused loudly at +authoring time, while these passed every check, were taught by the documentation, and +failed only at render in front of a user. + +**Why removal rather than implementation.** Both repairs were available and they are not +equivalent. Fulfilment would mint three new pieces of authorable surface: `line-chart` and +`area-chart` duplicate, under a second spelling, families the plugin already draws as +`{ "type": "chart", "chartType": "line" | "area" }`, and `advanced-chart` was never a +family at all — it named `AdvancedChartImpl`, an internal module. Measured demand for all +three is zero: a sweep of both repositories for authored nodes of these types returns +exactly one hit, the doc snippet corrected here, against lit controls in the same +commands (`"type": "bar-chart"` 5, `"type": "chart"` 12, `object-chart` 1 in the sibling +repo). No example app, fixture, seed document or deployment authors any of them. Under +声明即强制, a declaration with no delivery and no demand comes off rather than growing an +implementation to match it. + +**Breaking, for anyone who authored a retired key.** A document with +`{ "type": "line-chart" }` used to resolve in the registry and then draw nothing; it is +now refused by name — `objectui check` reports `Unknown schema type "line-chart"`, and +`SchemaRenderer` paints the `OBJUI-001` panel instead of an endless skeleton. That is a +louder failure for the same broken document, not a new one: no document that previously +DREW is affected. Migrate to `{ "type": "chart", "chartType": "line" | "area" }`, which +`CHART_TYPE_KEYWORD_FAMILIES` resolves. Scored `minor`, not `major`, per +AGENTS.md §版本号策略. + +**What moved.** The stub lists in `apps/console/src/register-plugins.ts` and +`apps/console/src/preview-gallery.tsx` (both loops, because the doc gate's key universe is +their union — retiring one alone would have changed nothing observable); the dashboard doc +snippet plus a note on how chart families are actually spelled; the regenerated +`KNOWN_SCHEMA_TYPES` snapshot (six entries, three bare and three namespaced); and the +`line-chart` leg of `node-slot-registered-arms-8499.test.ts`, whose premise this +retirement changed and which reads the stub list rather than the file so the ⛔ comment +left behind cannot satisfy it. diff --git a/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx b/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx new file mode 100644 index 0000000000..0442a1e835 --- /dev/null +++ b/apps/console/src/__tests__/unfulfilled-chart-stubs-8760.test.tsx @@ -0,0 +1,202 @@ +/** + * 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. + */ + +/** + * Every chart key this app registers as a lazy stub is FULFILLED by the module + * the stub points at (objectui#8760). + * + * ## The defect + * + * `register-plugins.ts` registered ten chart variants as `registerLazy` stubs + * pointing at `@object-ui/plugin-charts`. That package registers eight keys, + * and three of the ten — `line-chart`, `area-chart`, `advanced-chart` — were + * not among them. + * + * An unfulfilled stub does not fail; it succeeds at being useless, and it does + * so in the two places that decide whether a defect is ever seen: + * + * - AT RENDER. `SchemaRenderer`'s lazy branch is re-entered on every pass: + * it checks `hasLazy(type)` and returns the `Loading …` placeholder. + * `Registry.register()` deletes a lazy entry only for the keys the loaded + * module actually registers, so for an unfulfilled key the entry SURVIVES + * the load and every subsequent pass takes the same branch. MEASURED on + * `b775500af`, through this file's own chain: `{ "type": "line-chart" }` + * painted `role="status"` / `data-lazy-loading="line-chart"` / + * `Loading line-chart…`, permanently. Not OBJUI-001 — no alert, no error, + * no console warning. A skeleton that never resolves reads to a user as a + * slow network, which is why this outlived the card that first noticed it. + * - AT AUTHORING. The stub puts the key in `getKnownTypes()`, so + * `check:doc-types` and the CLI's generated `KNOWN_SCHEMA_TYPES` snapshot + * both bless it. `content/docs/plugins/plugin-dashboard.mdx` taught + * `"type": "line-chart"` inside a `card` body and every gate was green on + * it. + * + * So the failure was strictly worse than an unknown key: an unknown key is + * refused loudly at authoring time, while this one passed every check, was + * taught by the documentation, and failed only at render in front of a user. + * + * ## What this file pins, and what it deliberately does not + * + * The claim under test is the OUTCOME an author observes, never "the registry + * has N entries" or "the stub was registered" — those are the readings that let + * the defect exist for as long as it did. + * + * 1. THE INVARIANT. The stub list is read from this app's own SOURCE and each + * key is driven through the REAL loader (`ComponentRegistry.loadLazy`), + * then re-checked in the registry — which is exactly what `loadLazy`'s + * docblock tells callers to do, because it resolves "whether or not the + * loaded module actually registered the expected type". A key added to + * that list without a matching `register()` in `packages/plugin-charts` + * fails HERE, on the first render nobody has done yet. + * 2. THE RETIREMENT, as a render outcome. An authored node of a retired type + * now paints the loud OBJUI-001 refusal instead of the eternal skeleton. + * 3. THE CONTROL. `pie-chart` and `bar-chart` — fulfilled variants in the + * same sweep — are unchanged end to end: same registry identity, same + * drawn output, no alert and no placeholder. Each is asserted + * individually; neither reads zero on both sides of the change. + * + * The CLI half of (2) — `objectui check` refusing a document that authors a + * retired key, which is the "refused at authoring time" outcome on the + * PUBLISHED surface — is pinned in + * `packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts`. + * + * `AnyComponentSchema` is NOT part of this file's claim: it refused all three + * spellings before this change and refuses them after + * (`packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts` holds + * that, and already carried `area-chart` as a firing control). Claiming it here + * would be claiming a reading this card did not move. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { render } from '@testing-library/react'; + +import { ComponentRegistry } from '@object-ui/core'; +import { SchemaRenderer } from '@object-ui/react'; +// Module-scope import of the module the stubs point at, per AGENTS.md's +// flaky-test rule: every assertion below lands after a dynamic `import()` +// boundary, and paying that cost at import time puts it outside every +// test/hook timeout. The specifier is character-identical to the one +// `register-plugins.ts` hands `registerLazy`, so ESM hands the loader the very +// same module instance. It cannot mask the defect it guards against: the three +// retired keys are ones this module never registers under ANY load order. +import '@object-ui/plugin-charts'; +import '../register-plugins'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REGISTER_PLUGINS = join(HERE, '..', 'register-plugins.ts'); + +/** The three spellings objectui#8760 retired. */ +const RETIRED = ['line-chart', 'area-chart', 'advanced-chart'] as const; + +/** + * The chart stub list, read from the source it is declared in — never a copy. + * A copy is the failure this card is about: two declarations of one set, with + * nothing comparing them. + */ +function chartStubList(): string[] { + const source = readFileSync(REGISTER_PLUGINS, 'utf8'); + const marker = "() => import('@object-ui/plugin-charts')"; + const at = source.indexOf(`for (const variant of [`); + const loops = [...source.matchAll(/for \(const variant of \[([^\]]*)\]\)([\s\S]{0,220}?)\}/g)]; + const charts = loops.filter((m) => m[2].includes(marker)); + if (charts.length !== 1) { + throw new Error(`expected exactly one chart stub loop, found ${charts.length} (at ${at})`); + } + return [...charts[0][1].matchAll(/'([^']+)'/g)].map((m) => m[1]); +} + +/** Render one authored node and report only what an author could observe. */ +function paint(type: string, extra: Record = {}) { + const { container } = render(); + return { + alerts: container.querySelectorAll('[role="alert"]').length, + placeholders: container.querySelectorAll('[data-lazy-loading]').length, + elements: container.querySelectorAll('*').length, + text: (container.textContent ?? '').replace(/\s+/g, ' '), + }; +} + +const ROWS = [ + { m: 'Jan', v: 4000 }, + { m: 'Feb', v: 3000 }, + { m: 'Mar', v: 6000 }, +]; + +describe('objectui#8760 — every chart stub this app registers is fulfilled', () => { + it('the source read finds the list, and finds it non-empty', () => { + // Non-vacuity for every assertion below: a reader that has stopped reading + // must throw or come back empty, never yield a silently passing zero. + const list = chartStubList(); + expect(list.length, 'the stub-list read went vacuous').toBeGreaterThanOrEqual(5); + expect(list, 'the reader is looking at the chart loop').toContain('pie-chart'); + for (const retired of RETIRED) expect(list).not.toContain(retired); + }); + + it('drives each stub through the REAL loader and re-checks the registry', async () => { + const list = chartStubList(); + const unfulfilled: string[] = []; + for (const key of list) { + // The call, not the name: `loadLazy` resolves whether or not the module + // registered the key, so the reading that matters is the one taken after. + await ComponentRegistry.loadLazy(key); + if (ComponentRegistry.get(key) === undefined) unfulfilled.push(key); + } + expect( + unfulfilled, + 'a lazy stub resolves to nothing — the key renders `Loading …` forever', + ).toEqual([]); + expect(list).toHaveLength(7); + }, 20000); +}); + +describe('objectui#8760 — a retired key is refused loudly instead of drawing nothing', () => { + it.each(RETIRED)('`%s` has no stub and no registration', async (type) => { + expect(ComponentRegistry.hasLazy(type)).toBe(false); + expect(ComponentRegistry.hasLazy(type, 'plugin-charts')).toBe(false); + await ComponentRegistry.loadLazy(type); + expect(ComponentRegistry.get(type)).toBeUndefined(); + expect(ComponentRegistry.getKnownTypes()).not.toContain(type); + }); + + it.each(RETIRED)('an authored `%s` node paints OBJUI-001, not an endless skeleton', (type) => { + const shot = paint(type, { data: ROWS, xAxisKey: 'm', series: [{ dataKey: 'v' }] }); + // The half that changed. Before this card BOTH numbers were the other way + // round: 0 alerts and 1 placeholder, on every render pass, forever. + expect(shot.alerts, `${type} did not refuse`).toBe(1); + expect(shot.placeholders, `${type} is still stuck on a lazy placeholder`).toBe(0); + expect(shot.text).toContain(`Unknown component type: ${type}`); + expect(shot.text).toContain('OBJUI-001'); + }); +}); + +describe('objectui#8760 — CONTROL: the fulfilled variants are unmoved, each on its own', () => { + // Two controls, verified individually rather than as a pair: each resolves to + // a DIFFERENT renderer, and each is non-zero on both sides of the change, so + // neither can be the "reads 0 either way" kind that proves nothing. + it.each([ + ['pie-chart', 'ChartRenderer'], + ['bar-chart', 'ChartBarRenderer'], + ])('`%s` still resolves to `%s`', async (type, renderer) => { + await ComponentRegistry.loadLazy(type); + const impl = ComponentRegistry.get(type); + expect(impl).toBeDefined(); + expect((impl as { name?: string }).name).toBe(renderer); + expect(ComponentRegistry.getKnownTypes()).toContain(type); + }); + + it.each(['pie-chart', 'bar-chart'])('`%s` still draws — no alert, no placeholder', async (type) => { + await ComponentRegistry.loadLazy(type); + const shot = paint(type, { chartType: 'pie', data: ROWS, xAxisKey: 'm', dataKey: 'v', series: [{ dataKey: 'v' }] }); + expect(shot.alerts, `${type} started refusing — the control moved`).toBe(0); + expect(shot.placeholders, `${type} fell back to a lazy placeholder`).toBe(0); + expect(shot.elements, `${type} drew nothing at all`).toBeGreaterThan(0); + }); +}); diff --git a/apps/console/src/preview-gallery.tsx b/apps/console/src/preview-gallery.tsx index d8ead5f96d..a2db2ad96b 100644 --- a/apps/console/src/preview-gallery.tsx +++ b/apps/console/src/preview-gallery.tsx @@ -38,7 +38,14 @@ for (const variant of [ category: 'view', }); } -for (const variant of ['chart', 'bar-chart', 'line-chart', 'pie-chart', 'area-chart']) { +// ⛔ `line-chart` / `area-chart` are RETIRED here too (objectui#8760): this +// list is a second `registerLazy` site for the same plugin, and the doc gate's +// key universe is the UNION of every such loop. Retiring them from +// `register-plugins.ts` alone would have left both keys blessed by +// `check:doc-types` from THIS file, so the retirement would have changed +// nothing an author can observe. Chart families are reached as +// `{ "type": "chart", "chartType": "line" | "area" }`. +for (const variant of ['chart', 'bar-chart', 'pie-chart']) { ComponentRegistry.registerLazy(variant, () => import('@object-ui/plugin-charts'), { namespace: 'plugin-charts', category: 'chart', diff --git a/apps/console/src/register-plugins.ts b/apps/console/src/register-plugins.ts index 2e59e86148..936d138067 100644 --- a/apps/console/src/register-plugins.ts +++ b/apps/console/src/register-plugins.ts @@ -74,7 +74,28 @@ ComponentRegistry.registerLazy('chart', () => import('@object-ui/plugin-charts') }); // Additional chart variants registered by @object-ui/plugin-charts so the // renderer can lazy-load when any chart type appears in a schema. -for (const variant of ['object-chart', 'bar-chart', 'pie-chart', 'donut-chart', 'radar-chart', 'scatter-chart', 'line-chart', 'area-chart', 'advanced-chart', 'chart:bar']) { +// +// ⛔ Every key in this list must be one `@object-ui/plugin-charts` ACTUALLY +// REGISTERS. A stub for a key the loaded module never registers does not fail +// — it succeeds at being useless (objectui#8760). `Registry.loadLazy` resolves +// "whether or not the loaded module actually registered the expected type", +// and the renderer's lazy branch re-checks `hasLazy` on every pass, so an +// unfulfilled stub leaves `SchemaRenderer` painting `Loading …` FOREVER: +// no OBJUI-001, no error, no console warning. Meanwhile the key is in +// `getKnownTypes()`, so `check:doc-types` and the CLI's `KNOWN_SCHEMA_TYPES` +// snapshot both bless it and the documentation is free to teach it. +// +// ⛔ `line-chart`, `area-chart` and `advanced-chart` are RETIRED from this list +// (objectui#8760) — they were stubs this package never fulfilled. Authors +// reach those chart families the way the plugin actually registers them: +// `{ "type": "chart", "chartType": "line" | "area" }`, which +// `CHART_TYPE_KEYWORD_FAMILIES` resolves. `advanced-chart` was never a family +// at all — it named `AdvancedChartImpl`, an internal module. +// ⛔ Do not re-add a key here without a matching `ComponentRegistry.register()` +// in `packages/plugin-charts/src`: `unfulfilled-chart-stubs-8760.test.ts` +// drives this very list through the real loader and fails on the first key +// that resolves to nothing. +for (const variant of ['object-chart', 'bar-chart', 'pie-chart', 'donut-chart', 'radar-chart', 'scatter-chart', 'chart:bar']) { ComponentRegistry.registerLazy(variant, () => import('@object-ui/plugin-charts'), { namespace: 'plugin-charts', category: 'chart', diff --git a/content/docs/plugins/plugin-dashboard.mdx b/content/docs/plugins/plugin-dashboard.mdx index 799d0c5117..3607cee3ad 100644 --- a/content/docs/plugins/plugin-dashboard.mdx +++ b/content/docs/plugins/plugin-dashboard.mdx @@ -194,6 +194,17 @@ rewrites each bare-name registry entry without its `label`/`category` metadata ### Dashboard with Charts +Charts are authored as `type: "chart"` with a `chartType` — `line`, `area`, +`bar`, `pie`, `donut`, `radar` or `scatter`. `chartType` is what selects the +family; there is no per-family node key for `line` or `area`. + + +This snippet used to teach `"type": "line-chart"`. That key was registered as a +lazy stub the charts plugin never fulfilled, so a document copied from here +resolved in the registry and then sat on `Loading line-chart…` forever +(objectui#8760). The key is retired; `chartType` is the spelling that draws. + + ```json { "type": "dashboard", @@ -207,8 +218,15 @@ rewrites each bare-name registry entry without its `label`/`category` metadata "type": "card", "title": "Sales Trend", "body": { - "type": "line-chart", - "data": [], + "type": "chart", + "chartType": "line", + "data": [ + { "month": "Jan", "revenue": 4000 }, + { "month": "Feb", "revenue": 3000 }, + { "month": "Mar", "revenue": 6000 } + ], + "xAxisKey": "month", + "series": [{ "dataKey": "revenue" }], "height": 300 } } diff --git a/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts b/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts new file mode 100644 index 0000000000..c7aadab8a5 --- /dev/null +++ b/packages/cli/src/__tests__/unfulfilled-chart-stubs-retired-8760.test.ts @@ -0,0 +1,164 @@ +/** + * 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 check` refuses the three chart keys objectui#8760 retired — the + * AUTHORING-TIME half of that retirement, on the published surface. + * + * ## Why this pin is the one that matters + * + * The card's grading rests on a comparison: "an unknown key is refused loudly + * at authoring time; a registered-but-unfulfilled key passes every check, is + * taught by the documentation, and fails only at render in front of a user." + * `line-chart`, `area-chart` and `advanced-chart` were on the wrong side of it. + * They were `registerLazy` stubs in `apps/console` that + * `@object-ui/plugin-charts` never fulfilled, and a stub is enough to put a key + * into `getKnownTypes()` — so the derivation behind `KNOWN_SCHEMA_TYPES` + * carried all three (measured on `b775500af`: `line-chart`, `area-chart`, + * `advanced-chart` and their three `plugin-charts:`-namespaced spellings) and + * this command was SILENT on a document that could only ever paint a skeleton. + * + * So the outcome under test is the one an author observes from outside the + * repository: the command now NAMES the type. Not "the key left the snapshot" — + * that is the mechanism, and + * `scripts/__tests__/known-schema-types-derivation-5115.test.ts` already holds + * it against the derivation. + * + * ## The controls + * + * Silence is the pre-repair reading, so a test that only asserts warnings could + * pass against a command that warns about everything. Two fulfilled variants + * from the SAME stub sweep — `bar-chart` and `pie-chart` — must stay silent, + * and so must the spelling the corrected documentation now teaches, + * `{ "type": "chart", "chartType": "line" }`. Each is checked on its own: all + * three are non-zero readings (a real registered key, judged), not an absence + * that would read the same either way. + * + * Fixtures live under `os.tmpdir()` and carry the `className` marker, for the + * two reasons `check-known-types.test.ts` states at length: a fixture inside + * this workspace would be scanned by the repo's own `pnpm check`, and a file + * with no ObjectUI marker key is not judged at all (objectui#5127) — which + * would leave the silence assertions green while measuring nothing. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { check } from '../commands/check.js'; +import { KNOWN_SCHEMA_TYPES } from '../utils/known-schema-types.js'; + +let cwd: string; +let lines: string[]; +let restoreLog: () => void; + +function writeSchema(name: string, body: Record): void { + writeFileSync(join(cwd, name), JSON.stringify({ className: 'p-0', ...body })); +} + +/** The escape byte chalk opens a CSI sequence with, spelled rather than typed. */ +const ESC = String.fromCharCode(27); +const ANSI = new RegExp(`${ESC}\\[[0-9;]*m`, 'g'); + +function unknownTypeWarnings(): string[] { + return lines.map((l) => l.replace(ANSI, '')).filter((l) => l.includes('Unknown schema type')); +} + +const RETIRED = ['line-chart', 'area-chart', 'advanced-chart'] as const; + +const ROWS = [ + { month: 'Jan', revenue: 4000 }, + { month: 'Feb', revenue: 3000 }, +]; + +beforeEach(() => { + cwd = mkdtempSync(join(tmpdir(), 'objectui-check-8760-')); + lines = []; + const original = console.log; + console.log = (...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }; + restoreLog = () => { + console.log = original; + }; +}); + +afterEach(() => { + restoreLog(); + rmSync(cwd, { recursive: true, force: true }); +}); + +describe('objectui#8760 — `objectui check` names the retired chart keys', () => { + it.each(RETIRED)('warns about `%s`, which the charts plugin never registered', async (type) => { + writeSchema(`${type}.json`, { + type, + data: ROWS, + xAxisKey: 'month', + series: [{ dataKey: 'revenue' }], + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([ + expect.stringContaining(`Unknown schema type "${type}" in ${type}.json`), + ]); + }); + + it.each(RETIRED)('`%s` is gone from the shipped vocabulary, bare and namespaced', (type) => { + // Non-vacuity for the assertion beside it: the snapshot must be a real, + // populated set, or "does not contain" is true of everything. + expect(KNOWN_SCHEMA_TYPES.length).toBeGreaterThan(100); + expect(KNOWN_SCHEMA_TYPES).not.toContain(type); + expect(KNOWN_SCHEMA_TYPES).not.toContain(`plugin-charts:${type}`); + }); +}); + +describe('objectui#8760 — CONTROL: what still passes, each verified on its own', () => { + it.each(['bar-chart', 'pie-chart'])( + '`%s` — a fulfilled variant from the same sweep — stays silent', + async (type) => { + // Individually non-zero on both sides: each is a key the charts plugin + // really registers, so it was judged and accepted before this change and + // is judged and accepted after it. + expect(KNOWN_SCHEMA_TYPES).toContain(type); + expect(KNOWN_SCHEMA_TYPES).toContain(`plugin-charts:${type}`); + writeSchema(`${type}.json`, { + type, + data: ROWS, + xAxisKey: 'month', + dataKey: 'revenue', + series: [{ dataKey: 'revenue' }], + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + }, + ); + + it('the spelling the corrected dashboard doc teaches is accepted', async () => { + // `content/docs/plugins/plugin-dashboard.mdx` taught `"type": "line-chart"` + // and now teaches this. The repair is only complete if the replacement + // survives the very command the removed key was hiding from. + expect(KNOWN_SCHEMA_TYPES).toContain('chart'); + writeSchema('sales-trend.json', { + type: 'chart', + chartType: 'line', + data: ROWS, + xAxisKey: 'month', + series: [{ dataKey: 'revenue' }], + }); + await check(cwd); + expect(unknownTypeWarnings()).toEqual([]); + }); + + it('the instrument can still hear a warning at all', async () => { + // The firing control for every silence above. Without it, a command that + // stopped judging types would satisfy all four of them. + writeSchema('nonsense.json', { type: 'not-a-real-component-zzz' }); + await check(cwd); + expect(unknownTypeWarnings()).toHaveLength(1); + }); +}); diff --git a/packages/cli/src/utils/known-schema-types.ts b/packages/cli/src/utils/known-schema-types.ts index aa42333eed..453628e466 100644 --- a/packages/cli/src/utils/known-schema-types.ts +++ b/packages/cli/src/utils/known-schema-types.ts @@ -39,7 +39,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'action:icon', 'action:menu', 'address', - 'advanced-chart', 'ai-form-assist', 'ai-recommendations', 'ai:feedback', @@ -56,7 +55,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'app-shell:marketplace:installed-list', 'app-shell:mcp:connect-agent', 'app:launcher', - 'area-chart', 'article', 'aside', 'aspect-ratio', @@ -227,7 +225,6 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'layout:page:card', 'layout:responsive-grid', 'li', - 'line-chart', 'list', 'list-view', 'loading', @@ -289,13 +286,10 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [ 'pivot', 'plugin-calendar:calendar-view', 'plugin-calendar:object-calendar', - 'plugin-charts:advanced-chart', - 'plugin-charts:area-chart', 'plugin-charts:bar-chart', 'plugin-charts:chart', 'plugin-charts:chart:bar', 'plugin-charts:donut-chart', - 'plugin-charts:line-chart', 'plugin-charts:object-chart', 'plugin-charts:pie-chart', 'plugin-charts:radar-chart', diff --git a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts index f4bb0b1361..f0a9721622 100644 --- a/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts +++ b/packages/types/src/__tests__/node-slot-registered-arms-8499.test.ts @@ -39,18 +39,20 @@ * ## `line-chart` is deliberately NOT armed — the card's premise fails for it * * The card lists `line-chart` among the eight as a "REGISTERED, LIVE renderer". - * Measured here, it is not. `apps/console/src/register-plugins.ts` registers it - * as a LAZY STUB pointing at `@object-ui/plugin-charts`, and that package never - * registers the key — `Registry.loadLazy`'s own docblock says the loader - * "resolves once the loader completes (whether or not the loaded module actually - * registered the expected type)". So the key is known to `check:doc-types` and - * resolves to nothing at render time; `scripts/check-doc-component-types.mjs` - * records the same reading, calling it "the `line-chart` widget objectui#7896 - * recorded in `packages/plugin-dashboard/README.md`". objectui#8499's triage - * admits arms only for things that "运行时已经正确渲染" — already render - * correctly at runtime — so an arm for `line-chart` would invent a capability - * rather than name one. The fourth `describe` pins the absence WITH its reason, - * so registering the key for real turns this red instead of leaving the gap. + * Measured here, it is not. objectui#8499's triage admits arms only for things + * that "运行时已经正确渲染" — already render correctly at runtime — so an arm + * for `line-chart` would invent a capability rather than name one. + * + * ⚠️ THE REASON MOVED, and the fourth `describe` moved with it (objectui#8760). + * When this file was written, `apps/console/src/register-plugins.ts` registered + * `line-chart` as a LAZY STUB pointing at `@object-ui/plugin-charts` while that + * package never registered the key, so the key was KNOWN to `check:doc-types` + * and resolved to nothing at render. objectui#8760 retired the stub — together + * with `area-chart` and `advanced-chart`, the other two of the same shape — so + * today the key is absent from BOTH sides rather than half-present on one. The + * arm is still owed the day a renderer registers it for real, so the fourth + * `describe` now pins the retirement from both sources and fails if either + * half comes back. */ import { describe, it, expect } from 'vitest'; @@ -238,22 +240,47 @@ describe('objectui#8499 — the family arms are compared against their registrat }); describe('objectui#8499 — `line-chart` stays unarmed, and the reason stays checked', () => { - it('resolves in no arm', () => { - expect(AnyComponentSchema.safeParse({ type: 'line-chart' }).success).toBe(false); + /** + * The three spellings objectui#8760 retired. Each was a console `registerLazy` + * stub that `@object-ui/plugin-charts` never fulfilled; `area-chart` is also + * one of this file's `UNREGISTERED` firing controls above, which is the same + * reading taken from the union's side. + */ + const RETIRED_8760 = ['line-chart', 'area-chart', 'advanced-chart'] as const; + + it.each(RETIRED_8760)('`%s` resolves in no arm', (type) => { + expect(AnyComponentSchema.safeParse({ type }).success).toBe(false); + expect(AnyComponentSchema.safeParse({ type: 'div', children: [{ type }] }).success).toBe(false); }); - it('is a lazy stub in the console that `@object-ui/plugin-charts` never fulfils', () => { - // The premise the card asserts for all eight and that fails for this one. + it('is registered by neither the console nor `@object-ui/plugin-charts`', () => { // Both halves are read from source so the day someone registers the key for - // real, this goes red and the arm becomes owed. + // real — in EITHER place — this goes red and the arm becomes owed. + // + // The console half reads the stub list rather than the whole file, because + // the file still NAMES all three: objectui#8760 left a ⛔ comment saying + // they are retired, and a substring search over the source would match that + // comment and pass on a re-registration. const consoleSource = read(CONSOLE_PLUGINS); - expect(consoleSource, 'the console stub moved — re-derive the premise').toContain("'line-chart'"); + const stubbed = [ + ...consoleSource.matchAll(/registerLazy\(\s*'([^']+)'/g), + ...[...consoleSource.matchAll(/for \(const variant of \[([^\]]*)\]\)/g)].flatMap((m) => [ + ...m[1].matchAll(/'([^']+)'/g), + ]), + ].map((m) => m[1]); + // Non-vacuity: the reader must actually find the console's registrations. + expect(stubbed.length, 'the console stub read went vacuous').toBeGreaterThanOrEqual(20); + expect(stubbed, 'the reader is looking at the chart stubs').toContain('pie-chart'); const pluginSource = read(CHARTS_PLUGIN); const registered = [...pluginSource.matchAll(/register\(\s*\n?\s*'([^']+)'/g)].map((m) => m[1]); // Non-vacuity: the reader must actually find this module's registrations. expect(registered.length, 'the registration read went vacuous').toBeGreaterThanOrEqual(6); expect(registered).toContain('pie-chart'); - expect(registered).not.toContain('line-chart'); + + for (const type of RETIRED_8760) { + expect(stubbed, `${type} is stubbed in the console again`).not.toContain(type); + expect(registered, `${type} is registered by the charts plugin again`).not.toContain(type); + } }); });