Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/strict-mode-parity-probes-4910.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
---

Test-only (objectui#4910). The two `element:*` spec-parity tests in
`packages/components/src/__tests__/` — `text-input-inputs-spec-parity.test.ts` and
`record-picker-inputs-spec-parity.test.ts` — re-express their undeclared-key contrast probe
so it holds on both `@objectstack/spec` pins.

`@objectstack/spec@17.0.0` GA flipped the `element:*` props schemas from strip mode to
strict (objectstack#4001 batch A): an undeclared prop used to parse green and vanish from
`data`, and now fails with `unrecognized_keys` naming the key. Each probe existed to keep
the surrounding key-reachability claim non-vacuous — it shows what an *unpublished* key
looks like, so "the declared key survives the parse" means something — and it asserted the
strip-mode half as its control, which GA falsifies.

Both probes now read the installed spec's refusal mode **behaviourally** (parse a payload
carrying a key no spec declares) and assert the same verdict under either mode: a named
`unrecognized_keys` refusal under GA, a silent drop under the pinned `17.0.0-rc.6`. Neither
arm is vacuous, and the probe never reads a version string, so it cannot go stale against a
pin it does not observe. The undeclared key is now carried alongside the declared one, which
is what makes either arm attributable to the undeclared key rather than to the declared key
having gone bad. This is the disposition already taken by
`packages/plugin-detail/src/__tests__/recordHighlightsInputs.spec-parity.test.ts`
(objectui#4648 / PR #4671).

No published behaviour changes: no runtime source, no registration, and no gate was touched
— only the two test files.
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ const filterDescription = () => input('filter')?.description ?? '';
*/
const withFilter = (filter: unknown) => ({ object: 'account', filter });

/**
* Does the installed spec REFUSE an undeclared top-level key, or drop it in
* silence? (objectui#4910, measured on both pins.)
*
* `@objectstack/spec` 17.0.0 GA flipped the `element:*` props schemas from
* strip mode to strict under objectstack#4001 batch A, so an undeclared prop
* now raises `unrecognized_keys` with a named message; the pinned
* `17.0.0-rc.6` still drops it in silence. The VERDICT under test is identical
* either way — an undeclared key is not an authoring surface, which is the
* whole reason "the declared key survives the parse" says anything — but the
* EVIDENCE differs, and asserting the wrong one turns this file red for a
* reason that has nothing to do with what it guards.
*
* Probed behaviourally rather than off a version string: the strictness IS the
* fact this file cares about, a probe cannot go stale against a pin it never
* reads, and the probe key is a name no spec would ever declare. `object` is
* carried because it is required, so a refusal here can only be the probe key.
* Same shape as `recordHighlightsInputs.spec-parity.test.ts`, which took this
* disposition first (objectui#4648 / PR #4671).
*/
const specRefusesUnknownTopLevelKeys = !ElementRecordPickerPropsSchema.safeParse({
object: 'account',
__objectui_4910_probe__: true,
} as never).success;

describe('element:record_picker — registry inputs vs @objectstack/spec', () => {
it('is registered with a non-empty `inputs` surface', () => {
expect(config()).toBeDefined();
Expand All @@ -83,23 +108,50 @@ describe('element:record_picker — registry inputs vs @objectstack/spec', () =>

it('publishes `filter`, which the renderer has read all along', () => {
// A KEY-reachability claim, so the criterion is that the key SURVIVES the
// parse — not that the parse succeeds. This props schema is a strip-mode
// `z.object`, so an UNDECLARED key parses green too and is simply absent
// from `data` afterwards; asserting `success` alone would prove nothing.
// parse — not that the parse succeeds. Neither refusal mode makes
// `success === true` proof on its own: under rc.6's strip mode an
// UNDECLARED key parses green as well, and under GA's strict mode a green
// parse only reports that no undeclared key was present. Survival is the
// claim on both pins.
expect(specTopLevelKeys()).toContain('filter');
const parsed = ElementRecordPickerPropsSchema.safeParse(withFilter({ status: 'open' }));
expect(parsed.success).toBe(true);
expect(parsed.data?.filter).toEqual({ status: 'open' });

// The contrast that makes the criterion meaningful: same green parse, key
// gone, no diagnostic. That is what `filter` looked like to every manifest
// consumer before it was declared here.
// The contrast that makes the criterion meaningful: the SAME payload plus a
// key the spec does not declare. Two contract spellings, one verdict — the
// undeclared key never becomes authoring surface (see
// `specRefusesUnknownTopLevelKeys`). Carrying `filter` alongside is
// load-bearing rather than tidy: it is what makes either arm attributable
// to `notASpecKey` instead of to the declared key having gone bad, and the
// green parse asserted just above is what proves the base is valid.
const undeclared = ElementRecordPickerPropsSchema.safeParse({
object: 'account',
...withFilter({ status: 'open' }),
notASpecKey: 1,
} as never);
expect(undeclared.success).toBe(true);
expect(Object.keys(undeclared.data ?? {})).not.toContain('notASpecKey');

if (specRefusesUnknownTopLevelKeys) {
// 17.0.0 GA: a loud refusal, and the STRONGER guarantee — the author now
// gets told. Asserted as an envelope (the code AND the key it names)
// because a bare "it failed" would be satisfied just as well by a
// rejection of `filter` or `object`, the halves that have to stay valid.
expect(undeclared.success).toBe(false);
expect(undeclared.error?.issues.map((i) => i.code)).toContain('unrecognized_keys');
const refused = undeclared.error?.issues.flatMap(
(i) => (i as unknown as { keys?: string[] }).keys ?? [],
);
expect(refused).toContain('notASpecKey');
expect(refused).not.toContain('filter');
expect(refused).not.toContain('object');
} else {
// The pinned rc.6: same green parse, key gone, no diagnostic. That is
// what `filter` looked like to every manifest consumer before it was
// declared here — and the silent-drop harm objectstack#4001 batch A
// retired.
expect(undeclared.success).toBe(true);
expect(Object.keys(undeclared.data ?? {})).not.toContain('notASpecKey');
expect(undeclared.data?.filter).toEqual({ status: 'open' });
}

expect(inputNames()).toContain('filter');
expect(filterDescription()).not.toBe('');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ const inputNames = () => inputs().map((i) => i.name);
const input = (name: string) => inputs().find((i) => i.name === name);
const defaultValueDescription = () => input('defaultValue')?.description ?? '';

/**
* Does the installed spec REFUSE an undeclared top-level key, or drop it in
* silence? (objectui#4910, measured on both pins.)
*
* `@objectstack/spec` 17.0.0 GA flipped the `element:*` props schemas from
* strip mode to strict under objectstack#4001 batch A, so an undeclared prop
* now raises `unrecognized_keys` with a named message; the pinned
* `17.0.0-rc.6` still drops it in silence. The VERDICT under test is identical
* either way — an undeclared key is not an authoring surface, which is the
* whole reason "the declared key survives the parse" says anything — but the
* EVIDENCE differs, and asserting the wrong one turns this file red for a
* reason that has nothing to do with what it guards.
*
* Probed behaviourally rather than off a version string: the strictness IS the
* fact this file cares about, a probe cannot go stale against a pin it never
* reads, and the probe key is a name no spec would ever declare. Same shape as
* `recordHighlightsInputs.spec-parity.test.ts`, which took this disposition
* first (objectui#4648 / PR #4671).
*/
const specRefusesUnknownTopLevelKeys = !ElementTextInputPropsSchema.safeParse({
__objectui_4910_probe__: true,
} as never).success;

describe('element:text_input — registry inputs vs @objectstack/spec', () => {
it('is registered with a non-empty `inputs` surface', () => {
expect(config()).toBeDefined();
Expand All @@ -68,20 +91,49 @@ describe('element:text_input — registry inputs vs @objectstack/spec', () => {

it('publishes `defaultValue`, which the renderer has read all along', () => {
// A KEY-reachability claim, so the criterion is that the key SURVIVES the
// parse — not that the parse succeeds. This props schema is a strip-mode
// `z.object`, so an UNDECLARED key parses green too and is simply absent from
// `data` afterwards; asserting `success` alone would prove nothing at all.
// parse — not that the parse succeeds. Neither refusal mode makes
// `success === true` proof on its own: under rc.6's strip mode an
// UNDECLARED key parses green as well, and under GA's strict mode a green
// parse only reports that no undeclared key was present. Survival is the
// claim on both pins.
expect(specTopLevelKeys()).toContain('defaultValue');
const parsed = ElementTextInputPropsSchema.safeParse({ defaultValue: 'acme' });
expect(parsed.success).toBe(true);
expect(parsed.data?.defaultValue).toBe('acme');

// The contrast that makes the criterion meaningful: same green parse, key
// gone, no diagnostic. That is what `defaultValue` looked like to every
// manifest consumer before it was declared here.
const undeclared = ElementTextInputPropsSchema.safeParse({ notASpecKey: 1 } as never);
expect(undeclared.success).toBe(true);
expect(Object.keys(undeclared.data ?? {})).not.toContain('notASpecKey');
// The contrast that makes the criterion meaningful: the SAME payload plus a
// key the spec does not declare. Two contract spellings, one verdict — the
// undeclared key never becomes authoring surface (see
// `specRefusesUnknownTopLevelKeys`). Carrying `defaultValue` alongside is
// load-bearing rather than tidy: it is what makes either arm attributable
// to `notASpecKey` instead of to the declared key having gone bad, and the
// green parse asserted just above is what proves the base is valid.
const undeclared = ElementTextInputPropsSchema.safeParse({
defaultValue: 'acme',
notASpecKey: 1,
} as never);

if (specRefusesUnknownTopLevelKeys) {
// 17.0.0 GA: a loud refusal, and the STRONGER guarantee — the author now
// gets told. Asserted as an envelope (the code AND the key it names)
// because a bare "it failed" would be satisfied just as well by a
// rejection of `defaultValue`, the half that has to stay valid.
expect(undeclared.success).toBe(false);
expect(undeclared.error?.issues.map((i) => i.code)).toContain('unrecognized_keys');
const refused = undeclared.error?.issues.flatMap(
(i) => (i as unknown as { keys?: string[] }).keys ?? [],
);
expect(refused).toContain('notASpecKey');
expect(refused).not.toContain('defaultValue');
} else {
// The pinned rc.6: same green parse, key gone, no diagnostic. That is
// what `defaultValue` looked like to every manifest consumer before it
// was declared here — and the silent-drop harm objectstack#4001 batch A
// retired.
expect(undeclared.success).toBe(true);
expect(Object.keys(undeclared.data ?? {})).not.toContain('notASpecKey');
expect(undeclared.data?.defaultValue).toBe('acme');
}

expect(inputNames()).toContain('defaultValue');
expect(defaultValueDescription()).not.toBe('');
Expand Down
Loading