Skip to content

Commit 9cdc992

Browse files
os-zhuangclaude
andauthored
test(sdui): check the contract in the direction that would have caught #3006 (#3013)
The console guard only looked one way: every tag in PUBLIC_BLOCKS must resolve. That direction cannot tell "not built yet" from "built, but the contract spells it wrong" — so record:line_items was filed as a known gap for a release while its renderer shipped, fully configured, in plugin-form. Two checks close the other direction. Every shipped record:* block is curated, or listed with a reason. Seven are deliberately out, each declaring zero inputs — nothing for an author or a model to configure. A new record:* registration now fails until someone decides which side it belongs on, so the vocabulary cannot quietly drift from what the platform can render. A companion assertion pins those seven at zero inputs (and at being eager registrations, so a lazy stub's "not known yet" cannot make it vacuous), letting one that grows a configurable surface re-open the decision instead of inheriting the exclusion. A curated tag that near-misses a registered block. `line_items` vs `record:line_items` differ only by namespace; one of the two spellings is always a typo. The check reports the candidate — "also try record:line_items" — rather than just "not covered". Both were verified against the real bug: reverting the tag to `line_items` fails them with exactly that diagnosis. Deduping the registry surfaced a second, latent issue — eleven record:* blocks in plugin-detail are registered as register('record:x', …, {namespace:'record'}), prefixing an already-prefixed name and yielding doubled record:record:x keys. It does not reach the contract (getPublicConfigs rewrites type to the curated tag), so this change only documents it where the deduping happens; the registrations are left for a separate change. Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp Co-authored-by: Claude <noreply@anthropic.com>
1 parent 547541c commit 9cdc992

2 files changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@object-ui/console": patch
3+
---
4+
5+
test(sdui): check the contract in the direction that would have caught #3006
6+
7+
The console contract guard only looked one way: every tag in `PUBLIC_BLOCKS`
8+
must resolve. That direction cannot tell "not built yet" from "built, but the
9+
contract spells it wrong" — so `record:line_items` was filed as a known gap for
10+
a release while its renderer shipped, fully configured, in plugin-form.
11+
12+
Two checks close the other direction:
13+
14+
- **Every shipped `record:*` block is curated, or listed with a reason.** Seven
15+
are deliberately out, each declaring zero `inputs` — nothing for an author or
16+
a model to configure. A new `record:*` registration now fails until someone
17+
decides which side it belongs on, so the vocabulary cannot quietly drift from
18+
what the platform can render. A companion assertion pins those seven at zero
19+
inputs, so one growing a configurable surface re-opens the decision instead of
20+
inheriting the exclusion.
21+
22+
- **A curated tag that near-misses a registered block.** `line_items` vs
23+
`record:line_items` differ only by namespace; one of the two spellings is
24+
always a typo. The check reports the candidate ("also try
25+
`record:line_items`") rather than just "not covered".
26+
27+
Both were verified against the real bug: reverting the tag to `line_items`
28+
fails them with exactly that diagnosis.
29+
30+
Grouping the registry by canonical `type` surfaced a second, latent issue —
31+
eleven `record:*` blocks in plugin-detail are registered as
32+
`register('record:x', …, { namespace: 'record' })`, prefixing an already-
33+
prefixed name and yielding doubled `record:record:x` keys. It does not reach
34+
the contract (`getPublicConfigs()` rewrites `type` to the curated tag), so this
35+
changeset only documents it where the grouping happens; the registrations are
36+
left for a separate change.

apps/console/src/__tests__/public-contract.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,88 @@ describe('console ↔ PUBLIC_BLOCKS coverage', () => {
147147
}
148148
});
149149
});
150+
151+
/**
152+
* `record:*` blocks that ship but are deliberately NOT in the AI vocabulary,
153+
* each with the reason it stays out. Every one of these currently declares
154+
* ZERO `inputs` — there is nothing for an author or a model to configure, so
155+
* putting them in the contract would advertise a block that can only be
156+
* emitted bare.
157+
*
158+
* This list exists to force a decision, not to park problems: registering a
159+
* new `record:*` block fails the test below until it is either curated or
160+
* added here with a reason. That is the property objectui#3006 needed and did
161+
* not have — `record:line_items` shipped fully configurable (5 inputs) and
162+
* simply never reached the contract, because nothing looked in this direction.
163+
*/
164+
const DELIBERATELY_UNCURATED: Record<string, string> = {
165+
'record:activity': 'no declared inputs — feed is derived from the record, nothing to author',
166+
'record:alert': 'no declared inputs — banner content comes from record state',
167+
'record:chatter': 'no declared inputs — feed is derived from the record',
168+
'record:discussion': 'no declared inputs — shares the chatter renderer',
169+
'record:history': 'no declared inputs — audit trail is derived from the record',
170+
'record:quick_actions': 'no declared inputs — actions come from object metadata',
171+
'record:reference_rail': 'no declared inputs — rail is derived from the record',
172+
};
173+
174+
describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => {
175+
const NS = 'record';
176+
177+
/**
178+
* Eleven of these blocks are registered as
179+
* `register('record:x', …, { namespace: 'record' })` — an already-prefixed
180+
* name that gets prefixed again — so the registry holds both `record:x` and a
181+
* doubled `record:record:x` for the same component. The doubling never
182+
* reaches the contract (`getPublicConfigs()` rewrites `type` to the curated
183+
* tag), but it is real in the registry, and counting raw keys would report
184+
* those components twice. Collapsing the repeated prefix is what makes the
185+
* set below one entry per block.
186+
*/
187+
const undouble = (key: string): string => {
188+
let k = key;
189+
while (k.startsWith(`${NS}:${NS}:`)) k = k.slice(NS.length + 1);
190+
return k;
191+
};
192+
193+
const shippedRecordBlocks = (): string[] => {
194+
const keys = ComponentRegistry.getKnownTypes().filter(
195+
(k) => ComponentRegistry.getMeta(k)?.namespace === NS,
196+
);
197+
return [...new Set(keys.map(undouble))].sort();
198+
};
199+
200+
it('curates every shipped record:* block, or records why not', () => {
201+
const curated = new Set<string>(PUBLIC_BLOCKS);
202+
const uncurated = shippedRecordBlocks().filter((tag) => !curated.has(tag));
203+
204+
expect(uncurated).toEqual(Object.keys(DELIBERATELY_UNCURATED).sort());
205+
});
206+
207+
it('keeps the deliberately-uncurated blocks unconfigurable', () => {
208+
// The stated reason for every exclusion is "nothing to author". If one of
209+
// these grows `inputs`, it became authorable and the exclusion needs
210+
// re-deciding rather than inheriting.
211+
for (const tag of Object.keys(DELIBERATELY_UNCURATED)) {
212+
// A pending lazy stub reports `inputs: undefined` meaning "not known
213+
// yet", which would make the assertion below vacuous. These are eager
214+
// registrations in plugin-detail; pin that, so the day one goes lazy this
215+
// fails loudly instead of silently passing.
216+
expect(ComponentRegistry.getConfig(tag)).toBeDefined();
217+
expect(ComponentRegistry.getMeta(tag)?.inputs ?? []).toEqual([]);
218+
}
219+
});
220+
221+
it('catches a curated tag that misses a registered block by its namespace', () => {
222+
// The objectui#3006 shape: PUBLIC_BLOCKS said `line_items`, the registry
223+
// said `record:line_items`, and the forward check could only report "not
224+
// covered" — which reads as "not built yet" and got filed as a known gap.
225+
// A near-miss is never intentional: one of the two spellings is a typo.
226+
const known = new Set(ComponentRegistry.getKnownTypes());
227+
const nearMisses = PUBLIC_BLOCKS.filter((tag) => !known.has(tag)).map((tag) => {
228+
const bare = tag.slice(tag.indexOf(':') + 1);
229+
return { tag, alsoTry: [...known].filter((k) => k === bare || k.endsWith(`:${bare}`)) };
230+
});
231+
232+
expect(nearMisses.filter((m) => m.alsoTry.length > 0)).toEqual([]);
233+
});
234+
});

0 commit comments

Comments
 (0)