Skip to content

Commit 1bbb7bf

Browse files
fix(types): the @object-ui/types/zod mirrors keep an unknown key instead of silently dropping it (#26019)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNzn2LZs8bk8QMmkCUnkJdy)_ Part of #25923 — work item 2 (strip-mode mirrors) only. Items 1 (the flat calendar / gantt keys) and 3 (the `options` bag, which needs the stored-row census first) stay on the card for later dispatches, and #25923 remains open after this merges. Clause-②: yes Metadata-protocol: none ## What changed The `@object-ui/types/zod` mirrors held strip-mode objects (plain `z.object()`): a misspelled key parsed green and was silently dropped, so `FormFieldSchema.safeParse({ name, type, labell: 'oops' })` succeeded with `labell` gone, while `@objectstack/spec` refuses it by name. Every one of them is now `.passthrough()` (`z.looseObject`), like `BaseSchema` already was. The strict authoring face (`StrictAnyComponentSchema` / `deriveStrictAuthoringSchema`, what `objectui validate` uses) is unchanged and still refuses an unknown key by name, so the two faces now disagree only on keep-or-refuse, never on silently dropping. ### Re-measured with the card's probe | reading | before (base `38050ced`) | after | |---|---|---| | `*Schema` exports of `index.zod.ts` in strip mode (the card's probe) | **67** (the card said 74, the static scan 63) | **1** | | strip-mode object nodes reachable from those exports, local to `ui/packages/types` | 157 | 3 | | `z.object(` call sites in `ui/packages/types/src/zod/*.zod.ts` converted | — | 145 | The one remaining strip export is `HttpRequestSchema`, and the three remaining reachable nodes (`HttpRequestSchema`, the `$and`/`$or`/`$not` filter group, a filter-field object) are all spec-derived clones made by `stripImportedDefaults` from `@objectstack/spec` subschemas: they are not authored in this package, `HttpRequestSchema` is already in the parity file's `EXCLUSIONS`, and the new census reads source sites, which they are not. ### Landing point As expected by the card: `ui/packages/types/src/zod/*.zod.ts` (16 files) and `ui/packages/types/src/__tests__/zod-mirror-parity.test.ts`. No producer lives outside the package. ### The guard (beside `assertionNoVacuousEntry`) A new `describe` in `zod-mirror-parity.test.ts`, "no object in ../zod/ strips an unknown key (objectstack#25923)": - an AST census of every `z.object` / `z.looseObject` / `z.strictObject` call in `../zod/`, classifying each by its method chain (`.passthrough()`, `.strict()`, `.strip()`, `.catchall()`), fails on any site whose mode is strip; - a non-vacuity pin (more than 100 loose sites, one in `form.zod.ts`, at least one strict site); - a runtime pin that no registered `MIRRORS` value is an object with an undefined catchall. It is runtime, not a type-level `assertion…` like its neighbour, and the docblock says why: zod 4 types `$strip` and `$strict` identically, so a type-level "no strip" pin cannot tell them apart (measured: it flagged exactly the five strict entries). This is the existing instrument the card named, not a new gate. ### Type-surface consequence, and how the pins absorb it zod 4 gives a passthrough object's `z.input` / `z.infer` a `[k: string]: unknown` index signature. A declared `interface` is not assignable to a type carrying one, so the parity file's narrower-direction pin read every nested object member as narrower than its declaration (22 pins went red). The fix is one test-only operator, `WithoutCatchall` (`ui/packages/types/src/__tests__/zod-catchall-type.ts`), which removes exactly that signature (an index signature of `unknown` on an object that also has literal keys) and is the identity everywhere else. It is applied to the narrower direction only (`AcceptedOf`), so the wider direction still compares the raw input. `KnownDrift` moves from 55 entries / 93 keys to 53 / 90: `AppSchemaRendererNodeSchema` and `ReportSectionSchema` leave the ledger, `DashboardWidgetSchema` and `ReportComponentSchema` change their keys, each with a "LEFT / JOINED under objectstack#25923" note. Downstream: `pnpm exec turbo run type-check --filter='...@object-ui/types'` was 112/112 tasks green with the passthrough mirrors. ## Held back: the `FormField` index signature in `form.ts` The card also recommends dropping `FormField`'s `[key: string]: any`. It was measured on this branch and reverted, because it breaks consumers outside this dispatch's file surface: - `@object-ui/components` `src/renderers/form/form.tsx` lines 1884, 1959, 2055, 2195, 3228: `FormField` is not assignable to `FieldWithDeclaredDefault` (index signature missing); - `@object-ui/plugin-form` `src/ObjectForm.tsx:1068` writes `group`, which `FormField` does not declare; `src/WizardForm.tsx:845`, the same `FieldWithDeclaredDefault` error; - `@object-ui/plugin-grid` `src/ObjectGrid.tsx` lines 5963 and 6016, plus the type-check cascade of every package depending on those. Removing a published index signature also narrows what a TypeScript caller may write on `FormField`, so it is a `(narrowing)` Clause-② change on its own. It is reported to the seat as an open question rather than folded in here; `form.ts` is unchanged in this PR. ## File surface beyond the claim The claim named the strip-mode zod files, `form.ts`, the parity test and one changeset. The diff also touches: - `ui/packages/types/src/__tests__/zod-catchall-type.ts` (new, test-only; excluded from the build) and five sibling type tests that compare `z.input` with a declaration and now read it through the same operator (`ai-zod-arms-10859`, `designer-zod-arms-10859`, `registered-type-arms-10859-b2`, `sort-string-arm-retired-8221`, `base-schema-predicate-envelope-7530`); - 18 `ui/packages/types` tests that pinned the old strip behaviour ("the unknown key is dropped") and now pin passthrough ("the key is kept and still not refused"), each with a comment citing objectstack#25923; - two consumer tests that used the strip as their measuring instrument ("parse and see which keys survive"), now measured with the strict twin (`deriveStrictAuthoringSchema`), which refuses an undeclared key by name: `ui/examples/schema-catalog/test/component-fixture-declared-keys.test.ts` and `ui/packages/plugin-kanban/src/__tests__/columnCollapsedHonoured-9628.test.tsx`; - `scripts/sdui-manifest.record.json`: the registry-source digest moves because the registry sources changed; `sdui.manifest.json` itself is byte-identical. Each of these is a consequence of the mirrors no longer stripping; none changes runtime code outside `ui/packages/types/src/zod/`. ## Verification All on the final head `0d70f7e3` (branch merged with `main` at `6a6ebff9`); tests and type-checks ran under `scripts/pm/os-verify-lock.sh`: - `pnpm --dir ui exec vitest run --maxWorkers=2 packages/types/` plus the 120 `ui/` test files that import `@object-ui/types/zod` or a mirror schema: **510 files, 12867 tests, all passed**. Before the two instrument tests moved to the strict twin, that consumer set read 118 passed, 2 failed (5 tests). The full consumer battery is CI's. - `pnpm --filter @object-ui/types type-check`: exit 0. Downstream `turbo run type-check --filter='...@object-ui/types'`: 112/112 tasks successful (pre-merge head); `@object-ui/plugin-kanban` and `@object-ui/example-schema-catalog` type-check clean after the test edits. - The derived gate union (`node scripts/pm/dispatch-gates.mjs --commands`, 46 commands): all exit 0, reconciled with `--ran`: 46 derived, 46 run, 0 NOT-MEASURED, 0 UNRUN. ## Acceptance notes - Spec-owned strip nodes: the reachability walk also counts 18 strip-mode object nodes inside `@objectstack/spec` subschemas that the mirrors import by reference. They are the spec's, not this package's, and are out of scope here. - `exportConfigs` residue: `WithoutCatchall` does not walk into an object that has no catchall of its own (a partial record keyed by export format), because walking those was measured to hit TS2589 on the node tree. The residue is ledgered in `KnownDrift` on `ReportComponentSchema`, `ReportBuilderSchema` and `ReportViewerSchema`. - `ObjectForm.tsx:1068` writing an undeclared `group` key on a `FormField` is only visible once the index signature goes; it belongs to the `FormField` follow-up above. Serial note: #25926 waits for this PR, because both edit `ui/packages/types/src/zod/base.zod.ts`. --- _Generated by [Claude Code](https://claude.ai/code/session_01UDxCk99USU7v77odNZ66qE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3380671 commit 1bbb7bf

45 files changed

Lines changed: 567 additions & 276 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/types': patch
3+
---
4+
5+
fix(types): the `@object-ui/types/zod` mirrors keep an unknown key instead of silently dropping it
6+
7+
Clause-②: yes
8+
Metadata-protocol: none
9+
10+
- **What changed.** 145 objects in the zod mirrors were strip-mode `z.object()`: a misspelled key parsed green and vanished, so `FormFieldSchema.safeParse({ name, type, labell: 'oops' })` succeeded with `labell` gone while `@objectstack/spec` refused it by name. Every one is now `.passthrough()` (`z.looseObject`), like `BaseSchema` already was. The tolerant face keeps the key; the strict authoring face (`StrictAnyComponentSchema`, used by `objectui validate`) still refuses it by name, so the two faces disagree only on keep-or-refuse, never on silently dropping.
11+
- **What a caller sees.** Nothing new is refused: every document that parsed before parses now. A parsed result now carries the unknown keys its input carried, and the inferred `z.input` / `z.infer` types of those objects gain zod's `[k: string]: unknown` index signature. A declared `interface` is not assignable to a type with that signature; if you assign an interface-typed value to a variable typed by one of these mirrors' `z.input`, type it through the interface or pass it to `parse()`.
12+
- **Guard.** `zod-mirror-parity.test.ts` now fails on any strip-mode object in `ui/packages/types/src/zod/`.
13+
14+
What to do: nothing, unless you relied on a mirror dropping keys from its parse result; strip them yourself or read only the keys you need.

‎scripts/sdui-manifest.record.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@
4949
"ui/packages/types"
5050
],
5151
"registrySources": 922,
52-
"registrySourcesSha256": "7a4b546825cb499c56c72e11df824357786d9619b78c8835591fbe91a6ae6110"
52+
"registrySourcesSha256": "5fd9e0ebfb7dcc37cd51425ed367a00c968012c908c6351b3d736c99cb8b09b8"
5353
}
5454
}

‎ui/examples/schema-catalog/test/component-fixture-declared-keys.test.ts‎

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { describe, it, expect } from 'vitest';
7878
import {
7979
ButtonSchema,
8080
CommandItemSchema,
81+
deriveStrictAuthoringSchema,
8182
MenuItemSchema,
8283
RadioGroupSchema,
8384
SelectOptionSchema,
@@ -174,11 +175,15 @@ describe('toast fixtures: the variant value is a declared member (objectui#6157,
174175
);
175176
});
176177

177-
describe('command palette fixture: every item key survives a CommandItem parse', () => {
178+
describe('command palette fixture: every item key is a declared CommandItem key', () => {
178179
/**
179-
* `CommandItemSchema` is a bare `z.object`, so an undeclared key is STRIPPED
180-
* and `.success` stays true. Round-trip equality is what detects it.
180+
* `CommandItemSchema` is non-strict, so an undeclared key does not fail its
181+
* `.success`. It was a bare strip-mode `z.object` and round-trip equality
182+
* detected the dropped key; since objectstack#25923 it is `.passthrough()` and
183+
* KEEPS the key, so round-trip equality sees nothing either. The strict
184+
* authoring twin is the instrument: it refuses an undeclared key by name.
181185
*/
186+
const StrictCommandItemSchema = deriveStrictAuthoringSchema(CommandItemSchema);
182187
const items = (
183188
(schemaOf('components-form-command/file-command-palette')
184189
.groups as Json[])[0].items as Json[]
@@ -188,14 +193,14 @@ describe('command palette fixture: every item key survives a CommandItem parse',
188193
expect(items).toHaveLength(3);
189194
});
190195

191-
it.each([0, 1, 2])('item %i loses no authored key when parsed', (i) => {
192-
expect(CommandItemSchema.parse(items[i])).toEqual(items[i]);
196+
it.each([0, 1, 2])('item %i authors only declared keys', (i) => {
197+
expect(StrictCommandItemSchema.safeParse(items[i]).success).toBe(true);
193198
});
194199

195200
it('counter-probe: an item carrying the removed `shortcut` key fails this same check', () => {
196201
const withInventedKey = { ...items[0], shortcut: ['Ctrl', 'N'] };
197202
expect(CommandItemSchema.safeParse(withInventedKey).success).toBe(true);
198-
expect(CommandItemSchema.parse(withInventedKey)).not.toEqual(withInventedKey);
203+
expect(StrictCommandItemSchema.safeParse(withInventedKey).success).toBe(false);
199204
});
200205
});
201206

@@ -482,11 +487,20 @@ describe('catalog corpus: every menubar item uses the declared MenuItem spelling
482487
* expose under any stable public name — and an introspection helper that
483488
* silently resolved to the wrong node would report a confident, wrong
484489
* vocabulary. Feeding the schema every candidate key and reading back what
485-
* SURVIVES uses the strip behaviour itself as the instrument, so this control
486-
* measures the shipped schema instead of the shape of zod's internals.
490+
* SURVIVED used the strip behaviour itself as the instrument; since
491+
* objectstack#25923 the mirror is `.passthrough()` and every key survives, so
492+
* the instrument is now the strict authoring twin: a candidate key is
493+
* declared when the base item plus that key alone is ACCEPTED by it. It still
494+
* measures the shipped schema, not the shape of zod's internals.
487495
*/
488-
function declaredKeysOf(schema: { parse: (v: unknown) => unknown }, probe: Json): string[] {
489-
return Object.keys(schema.parse(probe) as Json).sort();
496+
const StrictMenuItemSchema = deriveStrictAuthoringSchema(MenuItemSchema);
497+
function declaredKeysOf(base: Json, candidates: Json): string[] {
498+
return [
499+
...Object.keys(base),
500+
...Object.keys(candidates).filter(
501+
(key) => StrictMenuItemSchema.safeParse({ ...base, [key]: candidates[key] }).success,
502+
),
503+
].sort();
490504
}
491505

492506
/** Depth-first over an items array, descending through submenu `children`. */
@@ -525,20 +539,14 @@ describe('catalog corpus: every menubar item uses the declared MenuItem spelling
525539
// both a `label` AND `separator: true` matches neither arm cleanly and is
526540
// not a meaningful "declared keys" instrument any more. `type` is no longer
527541
// part of this probe at all — see the dedicated refusal pins below, which
528-
// is where it moved (a hard refusal, not a strip, can't be read back by
529-
// `declaredKeysOf`'s "call `.parse` and see what survives" method, since a
530-
// refusal never returns a value to read keys off).
531-
const declaredCommandKeys = declaredKeysOf(MenuItemSchema, {
532-
label: 'probe',
533-
icon: 'file',
534-
disabled: false,
535-
shortcut: 'Ctrl+T',
536-
value: 'probe',
537-
});
538-
const declaredDividerKeys = declaredKeysOf(MenuItemSchema, {
539-
separator: true,
540-
value: 'probe',
541-
});
542+
// is where it moved (`declaredKeysOf` asks the strict twin about one
543+
// candidate key at a time; a key refused by a hard rule on the arm itself
544+
// would read as "undeclared" there for the wrong reason).
545+
const declaredCommandKeys = declaredKeysOf(
546+
{ label: 'probe' },
547+
{ icon: 'file', disabled: false, shortcut: 'Ctrl+T', value: 'probe' },
548+
);
549+
const declaredDividerKeys = declaredKeysOf({ separator: true }, { value: 'probe' });
542550
const items = allExamples().flatMap((e) => collectMenubarItems(e.schema, e.id));
543551

544552
it('label/icon/disabled/shortcut survive on the command arm; the unknown `value` key does not — the control', () => {
@@ -957,11 +965,13 @@ describe('catalog corpus: no `select` option authors the undeclared `type` key (
957965
expect(declared).not.toContain('type');
958966

959967
// ...and this is why the corpus assertion below is the instrument rather
960-
// than a `.success` probe: the schema is a bare `z.object`, so it STRIPS
961-
// the undeclared key and reports success.
968+
// than a `.success` probe: the schema is non-strict, so it reports success
969+
// on the undeclared key — it STRIPPED it while it was a bare `z.object`, and
970+
// keeps it since objectstack#25923 made it `.passthrough()`. Either way the
971+
// tolerant parse raises nothing.
962972
const parsed = SelectOptionSchema.safeParse({ label: 'Option 3', value: '3', type: 'option' });
963973
expect(parsed.success).toBe(true);
964-
expect(Object.keys((parsed as unknown as { data: Json }).data)).not.toContain('type');
974+
expect(Object.keys((parsed as unknown as { data: Json }).data)).toContain('type');
965975
});
966976

967977
it('no `select` option authors `type`', () => {

‎ui/packages/plugin-kanban/src/__tests__/columnCollapsedHonoured-9628.test.tsx‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import React from 'react';
6666
import { describe, it, expect } from 'vitest';
6767
import { render, screen, within, act } from '@testing-library/react';
6868
import '@testing-library/jest-dom';
69-
import { KanbanColumnSchema, ObjectKanbanSchema } from '@object-ui/types/zod';
69+
import { KanbanColumnSchema, ObjectKanbanSchema, deriveStrictAuthoringSchema } from '@object-ui/types/zod';
7070

7171
import { KanbanRenderer } from '../index';
7272
// Pay the board's lazy chunk at import time rather than inside a `findBy`
@@ -121,15 +121,16 @@ describe('both published faces declare the key (objectui#9628)', () => {
121121
expect(parsed.success && (parsed.data as { collapsed?: boolean }).collapsed).toBe(true);
122122
});
123123

124-
it('CONTROL — an undeclared lane key is dropped rather than kept, so the two above are readings', () => {
125-
const parsed = KanbanColumnSchema.safeParse({
126-
id: 'todo',
127-
title: 'To Do',
128-
cards: [],
129-
collapsedd: true,
130-
});
131-
expect(parsed.success).toBe(true);
132-
expect(parsed.success && 'collapsedd' in (parsed.data as object)).toBe(false);
124+
it('CONTROL — the strict twin refuses an undeclared lane key and accepts `collapsed`, so the two above are readings', () => {
125+
// The tolerant mirror keeps an undeclared key since objectstack#25923 made
126+
// every mirror object `.passthrough()` (it was dropped before), so survival
127+
// through a tolerant parse no longer tells a declared key from an
128+
// undeclared one. The strict authoring twin does: it refuses by name.
129+
const strict = deriveStrictAuthoringSchema(KanbanColumnSchema);
130+
const lane = { id: 'todo', title: 'To Do', cards: [] };
131+
expect(KanbanColumnSchema.safeParse({ ...lane, collapsedd: true }).success).toBe(true);
132+
expect(strict.safeParse({ ...lane, collapsedd: true }).success).toBe(false);
133+
expect(strict.safeParse({ ...lane, collapsed: true }).success).toBe(true);
133134
});
134135
});
135136

‎ui/packages/types/src/__tests__/accordion-item-authorable-keys.test.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,13 @@ describe('AccordionItem: `icon` is retired from the authoring surface (objectui#
6969
expect(item.value).toBe('section-1');
7070
});
7171

72-
it('drops `icon` when parsing through the Zod mirror', () => {
73-
// `z.object` is non-strict, so an authored `icon` is not an error — it is
74-
// simply not part of the declared surface any more, and parse output is
75-
// the observable statement of what that surface contains.
72+
it('keeps `icon` as an undeclared key when parsing through the Zod mirror', () => {
73+
// The mirror is non-strict, so an authored `icon` is not an error — it is
74+
// simply not part of the declared surface any more. It was DROPPED from the
75+
// parse output while the mirror was strip mode; since objectstack#25923 the
76+
// mirror is `.passthrough()`, so it rides through undeclared (the strict
77+
// authoring face refuses it by name). The shape test below is the statement
78+
// of what the declared surface contains.
7679
const parsed = AccordionItemSchema.parse({
7780
value: 'section-1',
7881
title: 'Section 1',
@@ -84,8 +87,8 @@ describe('AccordionItem: `icon` is retired from the authoring surface (objectui#
8487
value: 'section-1',
8588
title: 'Section 1',
8689
content: 'Content for section 1',
90+
icon: 'chevron',
8791
});
88-
expect('icon' in parsed).toBe(false);
8992
});
9093

9194
it('keeps `icon` out of the Zod schema shape', () => {

‎ui/packages/types/src/__tests__/ai-zod-arms-10859.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { fileURLToPath } from 'node:url';
4848

4949
import { describe, expect, it } from 'vitest';
5050
import { z } from 'zod';
51+
import type { WithoutCatchall } from './zod-catchall-type';
5152

5253
import {
5354
AIConfigSchema,
@@ -79,8 +80,9 @@ type Expect<T extends true> = T;
7980

8081
/** The mirror's own shape. */
8182
type ShapeOf<M> = M extends { shape: infer S } ? S : never;
82-
/** What a shape entry ACCEPTS (input side, so `.optional()` shows). */
83-
type InputOf<T> = T extends z.ZodType ? z.input<T> : never;
83+
/** What a shape entry ACCEPTS (input side, so `.optional()` shows), read without the
84+
* `.passthrough()` catchall every mirror object carries since objectstack#25923 (`./zod-catchall-type`). */
85+
type InputOf<T> = T extends z.ZodType ? WithoutCatchall<z.input<T>> : never;
8486
/** The declaration's DECLARED keys — any index signature dropped (`BaseSchema` carried one until objectui#8347). */
8587
type DeclaredKeys<D> = Extract<
8688
keyof { [K in keyof D as string extends K ? never : number extends K ? never : K]: D[K] },

‎ui/packages/types/src/__tests__/app-menu-item-shortcut-refusal-7719.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ describe('objectui#7719 — the JSON face refuses `shortcut` by name', () => {
212212

213213
/* ── (c) why it had to be a refusal — the SILENT STRIP, measured here ─────── */
214214

215-
describe('objectui#7719 — a bare non-declaration strips in silence; it refuses nothing', () => {
216-
it('an UNDECLARED sibling key is accepted and STRIPPED — it never reaches `.data`', () => {
215+
describe('objectui#7719 — a bare non-declaration refuses nothing', () => {
216+
it('an UNDECLARED sibling key is accepted — it is not refused', () => {
217217
// THE LOAD-BEARING ROW of this file: without it, "leaving `shortcut`
218218
// undeclared would not have refused it" is unfalsifiable prose. Here it is a
219219
// reading taken on this very schema. `keyboardShortcut` is declared nowhere;
@@ -228,10 +228,16 @@ describe('objectui#7719 — a bare non-declaration strips in silence; it refuses
228228
// PROVING REMOVAL: append `.passthrough()` to the object inside the `z.lazy`
229229
// and the second assertion reds (the key survives) while the first stays
230230
// green — which is exactly how this row tells the two mechanisms apart.
231+
//
232+
// objectstack#25923 made that object `.passthrough()` — every mirror object
233+
// is, so none drops an authored key in silence — and the key now SURVIVES
234+
// into `.data`, as on `DetailViewSchema`. The load-bearing half is
235+
// unchanged: the parse SUCCEEDS, so leaving `shortcut` undeclared would
236+
// still not have refused it.
231237
const r = MenuItemSchema.safeParse({ type: 'item', label: 'Profile', keyboardShortcut: 'Ctrl+P' });
232238
expect(r.success).toBe(true);
233239
if (!r.success) return;
234-
expect(Object.keys(r.data as Record<string, unknown>)).not.toContain('keyboardShortcut');
240+
expect(Object.keys(r.data as Record<string, unknown>)).toContain('keyboardShortcut');
235241
});
236242

237243
it('and the tombstoned key does NOT get that treatment — the pair is the whole argument', () => {

‎ui/packages/types/src/__tests__/base-schema-predicate-envelope-7530.test.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595

9696
import { describe, it, expect } from 'vitest';
9797
import type { z } from 'zod';
98+
import type { WithoutCatchall } from './zod-catchall-type';
9899
import type { BaseSchema } from '../base';
99100
import type { ExpressionWire } from '../expression';
100101
import { BaseSchema as Mirror } from '../zod/base.zod';
@@ -123,9 +124,13 @@ export type assertionWireShape = Expect<
123124
Equal< ExpressionWire, string | { dialect?: string; source: string } >
124125
>;
125126

126-
/** The two faces of the wire type are one union. */
127+
/**
128+
* The two faces of the wire type are one union — the zod face read without the
129+
* `.passthrough()` catchall its envelope carries since objectstack#25923
130+
* (`./zod-catchall-type`), which refuses nothing.
131+
*/
127132
export type assertionTwinParity = Expect<
128-
Equal< ExpressionWire, z.input< typeof ExpressionWireSchema > >
133+
Equal< ExpressionWire, WithoutCatchall< z.input< typeof ExpressionWireSchema > > >
129134
>;
130135

131136
/** The helper can FAIL -- the old union is not the new one. */

‎ui/packages/types/src/__tests__/chart-inline-data-retired.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,11 @@ describe('objectui#6896 — the counter-probe: a deletion would restore the sile
210210
});
211211

212212
it('an UNDECLARED key still rides through — the silence a tombstone is measured against', () => {
213+
// It rides through UNREFUSED; since objectstack#25923 the mirror is
214+
// `.passthrough()`, so the key is kept in the output rather than dropped.
213215
const result = ChartDataSeriesSchema.safeParse({ ...LIVE_SERIES, notAKeyAtAll: 'anything' });
214216
expect(result.success).toBe(true);
215-
if (result.success) expect(result.data).not.toHaveProperty('notAKeyAtAll');
217+
if (result.success) expect(result.data).toHaveProperty('notAKeyAtAll', 'anything');
216218
});
217219
});
218220

‎ui/packages/types/src/__tests__/chart-series-chart-type-alias-refusal-7694.test.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,15 @@ describe('objectui#7694 — when BOTH spellings are written the document is refu
161161
if (r.success) expect(r.data).toEqual({ name: 'r', type: 'line' });
162162
});
163163

164-
it('CONTROL — a truly undeclared key is STILL stripped in silence: the object is non-strict, which is why the arm exists', () => {
164+
it('CONTROL — a truly undeclared key is STILL accepted: the object is non-strict, which is why the arm exists', () => {
165165
// The deletion-not-chosen. This is what `chartType` looked like on the
166166
// base, and what it would look like again if someone "simplified" the arm
167-
// away. `chart-series-keys-7546.test.ts` (e) pins the same fact.
167+
// away. `chart-series-keys-7546.test.ts` (e) pins the same fact. It was
168+
// stripped while the mirror was strip mode; since objectstack#25923 the
169+
// mirror is `.passthrough()` and the key is kept.
168170
const r = ChartDataSeriesSchema.safeParse({ name: 'r', notAKeyAtAll: 'x' });
169171
expect(r.success).toBe(true);
170-
if (r.success) expect(r.data).not.toHaveProperty('notAKeyAtAll');
172+
if (r.success) expect(r.data).toHaveProperty('notAKeyAtAll', 'x');
171173
});
172174
});
173175

0 commit comments

Comments
 (0)