Skip to content

test(cli): repoint the #5341 union specimen at a door that is still a union - #18043

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-18032-stale-union-assertion
Sep 13, 2026
Merged

hotlong merged 1 commit into
mainfrom
claude/issue-18032-stale-union-assertion

Conversation

@hotlong

@hotlong hotlong commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Closes #18032

Clause-②: no

The diff is exactly one path — packages/cli/test/format-zod-union.test.ts
(git diff origin/main...claude/issue-18032-stale-union-assertion --name-only). No packages/spec/src/**
hit, so no needs:contract-review hold. No governed surface either (today's register: docs/adr/** ·
.claude/** · skills/** · AGENTS.md · CLAUDE.md).

What was red, and why it is not a flake

test/format-zod-union.test.ts has been ejecting PRs from the merge queue and reddening ordinary
pull_request builds. It is a deterministic SEMANTIC failure, reproduced here on a dedicated worktree cut
from origin/main at 0252320ae, with zero PR content in the tree:

FAIL  |integration| test/format-zod-union.test.ts
  × the specimen fails on exactly one issue, and that issue is the union   line 205
  × leaves the `--json` payload exactly as it was — full, and nested       line 227
AssertionError: expected [ …(2) ] to have a length of 1 but got 2
Tests  2 failed | 11 passed (13)

Printed rather than inferred — the two issues the specimen really produces:

[0] code=invalid_value      path=views.0.list.sort.0.order
[1] code=unrecognized_keys  path=views.0.list.sort.0   (still carrying `direction` -> `order`)

Root cause, confirmed at the source rather than by attribution: ListViewSchema.sort in
packages/spec/src/ui/view.zod.ts is now a bare z.array(strictObject(...)). #17053 / PR #17914 retired the
bare-string arm, and a two-arm union minus one arm is the surviving arm — so that door cannot emit
invalid_union at all any more, and the [#5341] test name's premise died with it. Shard composition,
worker count and queue stacking are all non-load-bearing: they were correlations read off a deterministic red
through a narrowing filter.

The route taken — and why NOT the cheap one

Not 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; a specimen
that no longer fails as a union cannot exercise it, and #5341's guarantee would go unwatched — exactly what it
was built to prevent.

⭐ The specimen is repointed at a door that is still a union, with the same anatomy the retired one had:

retired specimen this specimen
path views[].list.sort views[].list.gantt.tooltipFields[]
shape string arm OR strict-entry arm string arm OR strict-entry arm
prescription direction -> order name -> field (also fieldName -> field, text -> label, title -> label)
why it deserves one edit distance cannot reach it; getting it wrong reverses the sort silently edit distance cannot reach it; getting it wrong drops the tooltip field silently
top-level issues 1, invalid_union 1, invalid_union (measured)

The asymmetry that makes the test meaningful is preserved too: the string arm complains once (wrong kind of
value, no prescription) while the aimed-at arm complains twice (missing field plus the aliased key), so the
branch-selection logic is genuinely exercised rather than short-circuited.

The first assertion now carries a failure message that prints the issues the bare count discards and names
the remedy — repoint, do not retune — so the next arm retirement is answered correctly by whoever is standing
there, without re-deriving any of this.

Ablation — the new specimen is not a vacuous green

Committed first, then mutated, then restored. The mutation deletes the mechanism #5341 added: the
'invalid_union' member of EXPANDABLE_ISSUE_CODES in packages/cli/src/utils/format.ts.

leg on-disk proof run
baseline Tests 13 passed (13), exit 0
mutated deleted-text count 1 -> 0; injected marker count 0 -> 1; git diff HEAD --name-only 0 -> 1 line Tests 6 failed | 7 passed (13), exit 1
restored git hash-object equals the HEAD blob (e3dda5c85…); git diff HEAD --name-only back to 0

The decisive line under mutation is the new live-specimen terminal assertion:
AssertionError: expected '\n◆ Validate\n…' to contain ' + "name -> field" + '. No rebuild leg is owed:
packages/cli/bin/run-dev.js runs the CLI from src/ through tsx, so the mutation reached the spawned child
directly, with no dist/ in the resolution path.

Direction declared before the run, and observed: turns red (the ordinary direction). Two assertions stayed
green under mutation on purpose — the schema-level one and the --json one measure the payload, not the
terminal, which is the division of labour the file documents.

Gates

Declared narrowing — verification ran UNLOCKED. scripts/pm/os-verify-lock.sh could not take the shared
verify lock on this host: no usable flock. The shared verify lock is declared Linux-only (flock is
util-linux, and a stock macOS does not ship it), so the commands below were run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for these runs, nor for any sibling
agent in this container while they ran.

gate verdict
pnpm --filter @objectstack/cli exec vitest run test/format-zod-union.test.ts 13 passed (13), exit 0
pnpm --filter @objectstack/cli build exit 0
pnpm --filter @objectstack/cli typecheck exit 0 (check:test-typecheck: OK)
pnpm --filter @objectstack/cli test (both tiers) 253 files · 3387 tests; format-zod-union.test.ts PASSES; 4 unrelated files red for a host reason — see below
48 commands derived by scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack 47 exit 0
pnpm check:dual-build-cjs-loads NOT MEASUREDPREREQUISITE NOT MET — this gate reads built output, and some package has no dist/; it needs a whole-repo pnpm build and reads only packages this diff does not touch. CI builds, so CI measures it.

The 4 unrelated red files are a darwin artifact, not a finding of this branch. All of them assert on
tmpdir() paths and compare /var/... against the /private/var/... the OS resolves to, or read a
darwin+node-26 chalk ESM warning: published-subpath-console.pin.test.ts,
published-subpath-hook-body.pin.test.ts, published-entry-node-env-source-reroute.test.ts,
serve-runtime-state-project-key.test.ts. This diff touches none of them and none of them imports the file it
touches, so by construction they are not this branch's — they are green on CI's Linux runners.

Changeset

skip-changeset, measured rather than assumed. packages/cli's manifest publishes ["dist","README.md", "CHANGELOG.md"]; after pnpm --filter @objectstack/cli build, the symbol this diff introduces
(TOOLTIP_ALIAS_STACK) has 0 hits across those paths, while the positive control formatZodErrors has
6. Nothing published moves.

Acceptance notes


Generated by Claude Code

… union

`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(), <strict { field,
label } entry>])` — 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 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 08a363aa215438814465ca2dbc95db5c879888c9packageMentionDocs.

@hotlong hotlong added domain:cli skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Sep 13, 2026
@hotlong
hotlong marked this pull request as ready for review September 13, 2026 14:38
@hotlong
hotlong added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit fb2f01d Sep 13, 2026
51 checks passed
@hotlong
hotlong deleted the claude/issue-18032-stale-union-assertion branch September 13, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:cli size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Queue-flake anchor: test/format-zod-union.test.ts

1 participant