Skip to content

Commit 23c72be

Browse files
test(spec): pin that the devPlugins[] union refusal reaches the author named (#14975)
The nested `invalid_union` shape at `['devPlugins', 0]` is real, but it is not what an author reads: `formatZodIssue` descends the union and ranks the branches through `selectUnionBranches` (`shared/union-branch-policy.ts`), dropping the string arm as kind-mismatch-only and rendering the manifest branch verbatim — key name, surface and `Did you mean` rename included. Nothing pinned that, so the rendered half could regress, or be re-reported as a defect, without a single test moving. Three pins added beside the existing raw-shape pin (which is untouched): the rendered message names the key, the surface and the rename at `devPlugins.0`; that branch selection is structural rather than a one-fixture accident; and the accept side is unmoved. Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfe6fb6 commit 23c72be

1 file changed

Lines changed: 56 additions & 0 deletions

File tree

packages/spec/src/kernel/manifest-unknown-keys.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { describe, it, expect } from 'vitest';
3939

4040
import { ManifestSchema, PluginEnginesSchema } from './manifest.zod';
4141
import { ArtifactPackageSchema, ObjectStackDefinitionSchema } from '../stack.zod';
42+
import { formatZodError } from '../shared/error-map.zod';
4243

4344
/** A legal manifest, the shape every scaffold and example stamps. */
4445
const legal = () => ({
@@ -160,6 +161,61 @@ describe('#14192 — the refusal reaches every door the measurement listed', ()
160161
expect(nested, 'the named refusal is carried inside the union issue').toBeDefined();
161162
expect(nested!.keys).toEqual(['namesapce']);
162163
});
164+
165+
it('through `devPlugins[]` — the nested shape is NOT keyless: the author reads the key, the surface and the rename', () => {
166+
// #14722 proposed reshaping the union above, on the premise that
167+
// `formatZodError` flattens the nested refusal away and leaves the author a
168+
// bare `Invalid input` at this one door. Measured on `origin/main`
169+
// `3386493f`: it does not. `formatZodIssue` descends `invalid_union` and
170+
// ranks the branches through `selectUnionBranches`
171+
// (`shared/union-branch-policy.ts`, #8318) — which drops the string arm as
172+
// kind-mismatch-only and renders the manifest branch verbatim. So the raw
173+
// shape pinned directly above is real, and it is NOT what the author sees.
174+
//
175+
// This is the standing guard on that difference. The pin above may only
176+
// ever be called a defect again by a measurement that gets past THIS
177+
// assertion first — the reshape it invited costs either the accept set
178+
// (`z.discriminatedUnion` routes on a literal FIELD value, which a bare
179+
// string entry cannot carry) or the published JSON Schema for the key (a
180+
// hand-rolled `typeof` router on a `z.custom` base emits `items: {}` in
181+
// place of the whole `anyOf`), and buys a message the author already has.
182+
const result = ObjectStackDefinitionSchema.safeParse({ manifest: legal(), devPlugins: [typo()] });
183+
expect(result.success).toBe(false);
184+
if (result.success) return;
185+
const rendered = formatZodError(result.error);
186+
expect(rendered).toContain('devPlugins.0');
187+
expect(rendered, 'the offending key is named').toContain('namesapce');
188+
expect(rendered, 'the surface is named').toContain('this package manifest');
189+
expect(rendered, 'the rename is offered').toContain('Did you mean `namesapce` \u2192 `namespace`?');
190+
});
191+
192+
it('through `devPlugins[]` — that selection is structural, not an accident of this one fixture', () => {
193+
// The string arm's ONLY complaint about an object is `invalid_type` at the
194+
// branch root, so `isKindMismatchOnly` drops it for every object input —
195+
// the guarantee does not depend on the manifest branch happening to carry
196+
// exactly one issue. Two shapes that give it more than one:
197+
const alsoMissingRequired = () => {
198+
const { namespace: _ns, version: _v, ...rest } = legal();
199+
return { ...rest, namesapce: 'probe' };
200+
};
201+
const alsoWrongType = () => {
202+
const { namespace: _ns, ...rest } = legal();
203+
return { ...rest, namesapce: 'probe', version: 42 };
204+
};
205+
for (const entry of [alsoMissingRequired(), alsoWrongType()]) {
206+
const result = ObjectStackDefinitionSchema.safeParse({ manifest: legal(), devPlugins: [entry] });
207+
expect(result.success).toBe(false);
208+
if (result.success) continue;
209+
const rendered = formatZodError(result.error);
210+
expect(rendered).toContain('namesapce');
211+
expect(rendered).toContain('Did you mean `namesapce` \u2192 `namespace`?');
212+
}
213+
});
214+
215+
it('through `devPlugins[]` — the accept side is unmoved: a string entry and a legal manifest both parse', () => {
216+
expect(ObjectStackDefinitionSchema.safeParse({ manifest: legal(), devPlugins: ['some-dev-plugin'] }).success).toBe(true);
217+
expect(ObjectStackDefinitionSchema.safeParse({ manifest: legal(), devPlugins: [legal()] }).success).toBe(true);
218+
});
163219
});
164220

165221
describe('#14192 — the nested blocks inside `manifest:` are closed under the same measurement', () => {

0 commit comments

Comments
 (0)