From 1a0e664aaedef192dd1c5021b586ba2d223be505 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Sun, 13 Sep 2026 22:25:11 +0800 Subject: [PATCH] test(cli): repoint the #5341 union specimen at a door that is still a union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `views[].list.sort` stopped being a `z.union` when the bare-string arm was retired: a two-arm union minus one arm is the surviving arm, so the door now emits that arm's own issues directly — `invalid_value` at `views.0.list.sort.0.order` plus `unrecognized_keys` at `views.0.list.sort.0` — and neither is `invalid_union`. The three assertions built on that specimen were pinning a shape the spec no longer produces: deterministically red on `main`, on every tree, in both `pull_request` and `merge_group` events. The specimen moves to `views[].list.gantt.tooltipFields[]`, which is the same two-arm shape the retired one had — `z.union([z.string(), ])` — and whose entry declares the aliases `name` -> `field` / `fieldName` -> `field` / `text` -> `label` / `title` -> `label`. Like `direction` -> `order` before it, that is a prescription edit distance cannot reach, on a key whose misspelling is otherwise dropped silently. Deliberately NOT done: retuning the assertions to the post-retirement two-issue shape. That spelling is green and guards nothing — the guarantee this file holds is that a rejection BEHIND A UNION reaches the terminal, and a specimen that no longer fails as a union cannot exercise it. The first assertion now carries a failure message that says so and prints the issues the bare count discards, so the next retirement is answered with a repoint rather than a retune. Co-authored-by: Claude --- packages/cli/test/format-zod-union.test.ts | 69 ++++++++++++++++------ 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/cli/test/format-zod-union.test.ts b/packages/cli/test/format-zod-union.test.ts index 2a578350895..3a55542ea2a 100644 --- a/packages/cli/test/format-zod-union.test.ts +++ b/packages/cli/test/format-zod-union.test.ts @@ -141,13 +141,32 @@ describe('[#5341] formatZodErrors expands invalid_union branches', () => { /** * The live specimen, on the surface `os validate` actually parses. * - * `views[].list.sort` is `z.union([z.string(), z.array()])` - * and the entry declares the #4721 alias `direction → order` — the same tuple - * under a different word, which is worth a prescription precisely because - * getting it wrong REVERSES the sort silently. Behind a union, that - * prescription was produced on every run and delivered on none. + * `views[].list.gantt.tooltipFields[]` is + * `z.union([z.string(), ])` and the entry + * declares the aliases `name → field` / `fieldName → field` / `text → label` / + * `title → label` — the same tuple under a different word, which is worth a + * prescription precisely because edit distance cannot reach it and getting it + * wrong drops the tooltip field SILENTLY. Behind a union, that prescription is + * produced on every run and delivered on none. + * + * ## ⚠️ Why this specimen moved, and what a future red here means + * + * It was `views[].list.sort` until #17053 / PR #17914 retired the bare-string + * arm on that door. A two-arm union minus one arm is not a union: `sort` folded + * to the surviving `z.array(...)` and started emitting that arm's own issues + * DIRECTLY — two of them, neither `invalid_union` — so the three assertions + * below were pinning a shape the spec no longer produces. Deterministically red + * on `main`, on every tree, in both `pull_request` and `merge_group` events. + * + * ⛔ The fix for that class is NOT to teach these assertions the post-retirement + * shape. The guarantee this file holds is that a rejection **behind a union** + * reaches the terminal; re-pointing the assertions at a door that is no longer a + * union keeps them green while guarding nothing — the exact outcome #5341 was + * built to prevent. A retired arm means REPOINT the specimen at a door that is + * still a `z.union` with a prescription behind it. The first assertion below + * says so in its own failure message, where the next reader will be standing. */ -const SORT_ALIAS_STACK = { +const TOOLTIP_ALIAS_STACK = { manifest: { id: 'union_probe', name: 'Union Probe', namespace: 'union_probe', version: '1.0.0', type: 'app' }, views: [ { @@ -156,9 +175,16 @@ const SORT_ALIAS_STACK = { list: { name: 'union_probe_list', label: 'Union Probe', - type: 'grid', + type: 'gantt', columns: ['name'], - sort: [{ field: 'name', direction: 'desc' }], + gantt: { + startDateField: 'start_at', + endDateField: 'end_at', + titleField: 'name', + // `name` is the alias for `field` — what an author reaches for when + // naming the field a tooltip row shows. + tooltipFields: [{ name: 'owner' }], + }, }, }, ], @@ -199,35 +225,44 @@ describe('[#5341] `os validate` delivers a union branch prescription', () => { // runs first. If the stack failed for some unrelated reason the terminal // assertion below could pass on the wrong error entirely. it('the specimen fails on exactly one issue, and that issue is the union', () => { - const result = ObjectStackDefinitionSchema.safeParse(SORT_ALIAS_STACK); + const result = ObjectStackDefinitionSchema.safeParse(TOOLTIP_ALIAS_STACK); expect(result.success).toBe(false); const issues = result.success ? [] : result.error.issues; - expect(issues).toHaveLength(1); + // ⛔ A red here is a REPOINT, not a retune: see this specimen's docblock. + // The message carries the issues the count throws away, so the next reader + // can see which door stopped being a union without re-running anything. + expect( + issues, + 'The specimen no longer fails AS A UNION — its door was most likely narrowed to a single arm.\n' + + '⛔ Do NOT retune these assertions to the shape below: that unguards #5341. Repoint the specimen\n' + + 'at a door that is still a `z.union` with a prescription behind it (see this constant\'s docblock).\n' + + `Issues actually produced: ${JSON.stringify(issues.map((i) => ({ code: i.code, path: i.path.join('.') })))}`, + ).toHaveLength(1); expect(issues[0]!.code).toBe('invalid_union'); // The prescription exists in the payload — it always has. Delivery is the // only thing #5341 is about. - expect(JSON.stringify(issues[0])).toContain('`direction` → `order`'); + expect(JSON.stringify(issues[0])).toContain('`name` → `field`'); }); it('prints the prescription, not a bare `invalid_union: Invalid input`', () => { - const { exitCode, output } = runCli('validate', SORT_ALIAS_STACK); - expect(exitCode, `os validate accepted a stack with an aliased sort key:\n${output}`).not.toBe(0); - expect(output).toContain('views.0.list.sort'); - expect(output).toContain('`direction` → `order`'); + const { exitCode, output } = runCli('validate', TOOLTIP_ALIAS_STACK); + expect(exitCode, `os validate accepted a stack with an aliased tooltip key:\n${output}`).not.toBe(0); + expect(output).toContain('views.0.list.gantt.tooltipFields'); + expect(output).toContain('`name` → `field`'); }, 120_000); it('leaves the `--json` payload exactly as it was — full, and nested', () => { // The machine path never had this defect: it passes `error.issues` through, // so the branch tree was always on it. Pinned here because the fix is one // `console.log` loop away from being "helpfully" moved into the payload. - const { exitCode, output } = runCli('validate', SORT_ALIAS_STACK, ['--json']); + const { exitCode, output } = runCli('validate', TOOLTIP_ALIAS_STACK, ['--json']); expect(exitCode).not.toBe(0); const payload = JSON.parse(output.slice(output.indexOf('{'))); expect(payload.valid).toBe(false); expect(payload.errors).toHaveLength(1); expect(payload.errors[0].code).toBe('invalid_union'); // The branch tree, untouched — and NOT flattened into extra `errors[]` rows. - expect(JSON.stringify(payload.errors[0].errors)).toContain('`direction` → `order`'); + expect(JSON.stringify(payload.errors[0].errors)).toContain('`name` → `field`'); }, 120_000); });