Skip to content

Commit e0d5a2b

Browse files
committed
test(cli): re-point the union specimen at a door that is still a z.union
`packages/cli/test/format-zod-union.test.ts`'s live specimen parsed `views[].list.sort`, which stopped being a `z.union` when #17053 retired its bare-string arm: the union collapsed to its one surviving member and the specimen fell straight out of a plain `z.array(...)` as two ordinary issues, neither of them `invalid_union`. Two of the three tests in `[#5341] \`os validate\` delivers a union branch prescription` went red on the issue count, and the third — the terminal one — kept passing on the ordinary non-union path, so the block's own subject was unguarded. Re-point the specimen at `views[].list.gantt.tooltipFields`, which is still `z.union([z.string(), <strict entry>])`, whose strict arm carries the curated alias set `name → field` / `text → label`, and which is reachable from `ObjectStackDefinitionSchema` — the schema `os validate` parses. Adapting the assertions to the collapsed shape was the alternative and is the regression: it greens the file with its subject untested. The terminal test also gains the assertion that would have caught this: the union's own `invalid_union: Invalid input` verdict line must print alongside the prescription, so the prescription is proven to have arrived from behind a union rather than on the ordinary path. No spec change: #17914 is the ruled end-state and is untouched. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
1 parent 08a363a commit e0d5a2b

1 file changed

Lines changed: 68 additions & 16 deletions

File tree

packages/cli/test/format-zod-union.test.ts

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,48 @@ describe('[#5341] formatZodErrors expands invalid_union branches', () => {
141141
/**
142142
* The live specimen, on the surface `os validate` actually parses.
143143
*
144-
* `views[].list.sort` is `z.union([z.string(), z.array(<strict sort entry>)])`
145-
* and the entry declares the #4721 alias `direction → order` — the same tuple
146-
* under a different word, which is worth a prescription precisely because
147-
* getting it wrong REVERSES the sort silently. Behind a union, that
148-
* prescription was produced on every run and delivered on none.
144+
* The specimen's whole job is to reach a REAL `z.union` whose strict branch
145+
* carries a curated prescription. It is therefore pinned to a property of the
146+
* SCHEMA, not to any one key, and it moves when the schema does.
147+
*
148+
* ## Why it moved once already — read this before touching the assertions
149+
*
150+
* It used to be `views[].list.sort`,
151+
* `z.union([z.string(), z.array(<strict sort entry>)])`, with the #4721 alias
152+
* `direction → order`. #17053 retired the bare-string arm under ADR-0049
153+
* enforce-or-remove; the union collapsed to its one surviving member, and the
154+
* same stack now falls straight out of a plain `z.array(...)` as TWO ordinary
155+
* issues — an `invalid_value` on `order` and an `unrecognized_keys` on the
156+
* entry — neither of them `invalid_union`. The block's subject, *a rejection
157+
* reaching the terminal from behind a union*, was no longer exercised at this
158+
* door at all, and the terminal test below went on passing on the ordinary
159+
* path while what it exists to guard went unguarded. Re-pointing the specimen
160+
* is the repair; adapting the counts to the collapsed shape would have been
161+
* the regression, which is why the terminal test now also asserts the union's
162+
* own verdict line.
163+
*
164+
* ## The successor door, and the three things that qualify one
165+
*
166+
* `views[].list.gantt.tooltipFields`, verified on the tree this test runs
167+
* against:
168+
*
169+
* 1. it is still `z.union([z.string(), <strict entry>])` — the string arm is
170+
* the bare field name, the object arm the `{ field, label }` pair
171+
* objectui's GanttView tooltip resolver reads, and no ruling retires
172+
* either (contrast `view.sort`, whose string arm had one);
173+
* 2. the strict arm is a closed `strictObject` declaring the curated aliases
174+
* `name → field`, `fieldName → field`, `text → label`, `title → label`,
175+
* so a near-miss gets the #4001 campaign's prose instead of zod's default
176+
* report — the same reason `direction → order` was worth delivering;
177+
* 3. it is reachable from `ObjectStackDefinitionSchema`, the schema
178+
* `os validate` parses, so the terminal really renders it.
179+
*
180+
* ⚠️ If this block reds on the issue COUNT again, the DOOR has moved and the
181+
* formatter is fine: find another union that satisfies (1)–(3) and re-point
182+
* the specimen at it. ⛔ Never adapt the assertions to whatever shape the
183+
* schema now produces — that greens the file with its own subject untested.
149184
*/
150-
const SORT_ALIAS_STACK = {
185+
const GANTT_TOOLTIP_ALIAS_STACK = {
151186
manifest: { id: 'union_probe', name: 'Union Probe', namespace: 'union_probe', version: '1.0.0', type: 'app' },
152187
views: [
153188
{
@@ -156,9 +191,18 @@ const SORT_ALIAS_STACK = {
156191
list: {
157192
name: 'union_probe_list',
158193
label: 'Union Probe',
159-
type: 'grid',
194+
type: 'gantt',
160195
columns: ['name'],
161-
sort: [{ field: 'name', direction: 'desc' }],
196+
gantt: {
197+
startDateField: 'start_at',
198+
endDateField: 'end_at',
199+
titleField: 'name',
200+
// The near-miss: `name` is how `record:highlights` spells the same
201+
// idea, so an author carrying an entry across surfaces brings it
202+
// along. Declared as an alias precisely so the answer names the
203+
// replacement instead of reporting a stray key.
204+
tooltipFields: [{ name: 'owner' }],
205+
},
162206
},
163207
},
164208
],
@@ -199,35 +243,43 @@ describe('[#5341] `os validate` delivers a union branch prescription', () => {
199243
// runs first. If the stack failed for some unrelated reason the terminal
200244
// assertion below could pass on the wrong error entirely.
201245
it('the specimen fails on exactly one issue, and that issue is the union', () => {
202-
const result = ObjectStackDefinitionSchema.safeParse(SORT_ALIAS_STACK);
246+
const result = ObjectStackDefinitionSchema.safeParse(GANTT_TOOLTIP_ALIAS_STACK);
203247
expect(result.success).toBe(false);
204248
const issues = result.success ? [] : result.error.issues;
205249
expect(issues).toHaveLength(1);
206250
expect(issues[0]!.code).toBe('invalid_union');
207251
// The prescription exists in the payload — it always has. Delivery is the
208252
// only thing #5341 is about.
209-
expect(JSON.stringify(issues[0])).toContain('`direction` → `order`');
253+
expect(JSON.stringify(issues[0])).toContain('`name` → `field`');
210254
});
211255

212256
it('prints the prescription, not a bare `invalid_union: Invalid input`', () => {
213-
const { exitCode, output } = runCli('validate', SORT_ALIAS_STACK);
214-
expect(exitCode, `os validate accepted a stack with an aliased sort key:\n${output}`).not.toBe(0);
215-
expect(output).toContain('views.0.list.sort');
216-
expect(output).toContain('`direction` → `order`');
257+
const { exitCode, output } = runCli('validate', GANTT_TOOLTIP_ALIAS_STACK);
258+
expect(exitCode, `os validate accepted a stack with an aliased tooltip key:\n${output}`).not.toBe(0);
259+
// Paired deliberately, and the pair is what keeps this test from passing
260+
// for the wrong reason: the union's own verdict line is the evidence that
261+
// the prescription under it arrived from BEHIND a union. The prescription
262+
// assertion alone passes for any door that reports an unrecognized key
263+
// directly — which is exactly how this test kept passing after #17053
264+
// collapsed the previous specimen's union and left the block's subject
265+
// unexercised.
266+
expect(output).toContain('invalid_union: Invalid input');
267+
expect(output).toContain('views.0.list.gantt.tooltipFields');
268+
expect(output).toContain('`name` → `field`');
217269
}, 120_000);
218270

219271
it('leaves the `--json` payload exactly as it was — full, and nested', () => {
220272
// The machine path never had this defect: it passes `error.issues` through,
221273
// so the branch tree was always on it. Pinned here because the fix is one
222274
// `console.log` loop away from being "helpfully" moved into the payload.
223-
const { exitCode, output } = runCli('validate', SORT_ALIAS_STACK, ['--json']);
275+
const { exitCode, output } = runCli('validate', GANTT_TOOLTIP_ALIAS_STACK, ['--json']);
224276
expect(exitCode).not.toBe(0);
225277
const payload = JSON.parse(output.slice(output.indexOf('{')));
226278
expect(payload.valid).toBe(false);
227279
expect(payload.errors).toHaveLength(1);
228280
expect(payload.errors[0].code).toBe('invalid_union');
229281
// The branch tree, untouched — and NOT flattened into extra `errors[]` rows.
230-
expect(JSON.stringify(payload.errors[0].errors)).toContain('`direction` → `order`');
282+
expect(JSON.stringify(payload.errors[0].errors)).toContain('`name` → `field`');
231283
}, 120_000);
232284
});
233285

0 commit comments

Comments
 (0)