Skip to content

test(examples): the catalog mirror declares the select options the host declares - #6539

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6317-gallery-select-options
Aug 26, 2026
Merged

test(examples): the catalog mirror declares the select options the host declares#6539
os-support-ai merged 1 commit into
mainfrom
claude/issue-6317-gallery-select-options

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#6317

The card's premise was half dead, and the surviving half is the mirror

The issue reports that apps/site/app/components/galleryDataSource.ts declares users.role / users.status as select with no options. Verified against origin/main at 831be7285 before writing any code: the host fixture already declares them, and has since 4b0b12630 (#5113 / #5857, 2026-08-23) — 330 commits before this branch point, and two days before the issue was filed.

$ git log -S"{ label: 'Admin', value: 'admin' }" --oneline origin/main -- apps/site/app/components/galleryDataSource.ts
4b0b12630 docs(plugin-view): make the three Interactive Examples real object-view nodes (#5113) (#5857)
$ git grep -n "label: 'Role', type: 'select'" origin/main
origin/main:examples/schema-catalog/test/catalog-gallery-render.test.tsx:322: role: { label: 'Role', type: 'select' },

Exactly one file in the repo still carried the option-less declaration: the catalog-test mirror. So the host fixture is untouched by this PR (nothing to do), and the whole change is the mirror plus the pins that would have caught the drift.

This is the failure mode the card names in its own words — two declarations of one fixture disagreeing — running in the direction the existing parity case could not see. That case pins the host's method names; the field declarations were unwatched, and they drifted for 330 commits with every one of this file's 572 cases green throughout.

The values, derived structurally

Walked rather than grepped — every record the fixture's own find('users') returns, and every key on every record, with no field list decided up front:

rows returned = 5 total reported = 5
role distinct=3 admin(x1) member(x3) viewer(x1)
status distinct=3 active(x3) invited(x1) suspended(x1)

Three and three, agreeing with the dispatch's reading. The same walk over the mirror's rows returns the same two sets, so the mirror's schema was declaring select with no options over rows that carry three values each.

The before/after, and the part of it that did not happen

Triage expected the plugin-grid / plugin-view tiles to start rendering labels instead of raw values, and ruled that deliberate. Measured: they do not change at all. All seven users-bound entries render byte-identical text before and after:

### plugin-grid/object-grid-columns
BEFORE: "User Directory#Full NameEmailRoleDepartmentStatus1OpenAlice Johnson…AdminEngineeringActive…"
AFTER: "User Directory#Full NameEmailRoleDepartmentStatus1OpenAlice Johnson…AdminEngineeringActive…"
CHANGED: false

The mechanism, which is the part worth keeping: ObjectGridsynthesises options for an option-less select from the distinct values in the loaded rows —

// packages/plugin-grid/src/ObjectGrid.tsxif(inferredType==='select'&&!fieldMeta.options){constuniqueValues=Array.from(newSet(data.map(row=>row[col.field]).filter(Boolean)));fieldMeta.options=uniqueValues.map(v=>({value: v,label: humanizeLabel(String(v))}));}

so admin already printed as Admin. The grid has a fallback for the missing declaration. The form path has none, and that is where the change is real — the shape the issue itself measured, an object-form over users with no fields restriction:

BEFORE text: "NameEmailRoleNo options availableDepartmentStatusNo options availableCancelUpdate"
AFTER text: "NameEmailRoleAdminAdminMemberViewerDepartmentStatusActiveActiveInvitedSuspendedCancelUpdate"
BEFORE controls: ["Alice Johnson","alice@example.com","Engineering"]
AFTER controls: ["Alice Johnson","alice@example.com","admin","Engineering","active"]

The BEFORE string is byte-identical to the one in the issue body, so the probe is measuring the reported defect and not something adjacent. Note the labels were not narrowed to equal the values to keep anything green — they are the Title Case labels triage asked for, and they coincide with humanizeLabel's output by construction, which is why the grid text is unchanged rather than a sign the change is inert.

What this PR changes

examples/schema-catalog/test/catalog-gallery-render.test.tsx only:

  1. USERS_SCHEMA declares options on role and status, byte-shaped like the host's so future diffs between the two files stay readable.
  2. Four new cases under objectui#6317, closing the class in both directions:
    • every select field must declare options whose values equal the distinct values its own rows carry — both directions, so an uncovered row value and an option no row uses each fail;
    • a vacuity guard, so the case above cannot pass by matching nothing;
    • the mirror's whole users field surface is compared to the host's, options included. The host literal is read by brace-matching and then JSON-ifying, so an extraction that stops working fails loudly here rather than quietly comparing less than it claims to.
  3. The fixture's header comment records what the parity now covers, and why it had to be widened.

Reverse verification

Assertion written first, run against the untouched fixture:

❯ examples/schema-catalog/test/catalog-gallery-render.test.tsx (576 tests | 3 failed | 572 skipped)
× `role` declares options, and they cover its rows exactly
× `status` declares options, and they cover its rows exactly
× the host fixture declares the SAME field surface, options included
AssertionError: `role` is declared `type: 'select'` with no `options`. … expected undefined to be truthy
AssertionError: the host fixture and this mirror declare different `users` field surfaces … + "options": [ { "label": "Admin", "value": "admin" }, … ] ← received (host)

The mutation was then proved on disk by counting the changed text rather than trusting the editor's exit code:

BEFORE optionless role decl: 1 optionless status decl: 1 option pairs: 0
AFTER optionless role decl: 0 optionless status decl: 0 option pairs: 6
file sha ef0d4294… → 0a12070a…

Verification — all on a9494bc32 (the final commit; working tree clean, tested blob cad7f9d7… equals HEAD:'s blob)

gatecommandits own verdict line
testspnpm exec vitest run examples/schema-catalog/test/catalog-gallery-render.test.tsxTest Files 1 passed (1) / Tests 576 passed (576)
type-checkpnpm --filter @object-ui/example-schema-catalog run type-checkexit 0, script name echoed (> … type-check)
lintpnpm exec eslint . in examples/schema-catalogexit 0
changesetnode scripts/check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.
control bytesnode scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 5394 tracked text file(s); skipped 85 binary).

Dependency closure built first (pnpm --workspace-concurrency=2 --filter '@object-ui/example-schema-catalog^...' build), so the type-check reads real .d.ts rather than the missing-dist TS2307 noise a fresh worktree produces. The edited file is provably inside the checked set — tsc -p tsconfig.test.json --listFiles lists catalog-gallery-render.test.tsx (1 hit), so "type-check green" actually covers this edit.

Declared narrowing: the repo-wide eslint . scan was not run. What was run is this package's own lint command in full — 17 files, which is eslint's own enumeration for examples/schema-catalog (read from --format json, not from a guess about which files count), with no narrowing within the package. For untouched files elsewhere: eslint.config.js sets no parserOptions.project / projectService, so type-aware linting is off and a diff confined to one file in one package cannot move any other file's verdict. CI runs the full farm regardless.

No changeset, on the gate's own verdict above: apps/site and examples/schema-catalog are both private, and @object-ui/site / @object-ui/example-* sit in the changeset ignore list, so no released package's source changed.

Out of scope, filed not folded


Generated by Claude Code

…host declares
The gallery fixture's `role` / `status` options landed on the host in 4b0b126 and the catalog-test mirror never followed, so two declarations of one fixture disagreed for 330 commits with every case in the file green throughout.
Declare them on the mirror to match the host, and add the pins that would have caught the drift: every `select` field must declare options covering its own rows exactly, and the mirror's whole `users` field surface is compared to the host's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — objectui#6317 (domain:ui lane, PM review). Reviewed from the tree at a9494bc32.

The card's premise is false, and so was my dispatch order. The dev caught both.

My error, first

My order said, as verified premise:

Read on origin/main at 831be7285. role is declared at galleryDataSource.ts:76 and status at :86; the five USERS_ROWS (lines 100–104) carry exactly: role: admin, member, viewer …

Every word of that is true and it licensed the wrong conclusion. What I actually ran was a grep for role|status, which returned the two declaration lines and the five row lines — and I never looked at the shape of those declarations. Read in full just now, galleryDataSource.ts already declares options on both fields, with exactly the Title Case labels triage specified, and has since 4b0b12630 ("docs(plugin-view): make the three Interactive Examples real object-view nodes") — 330 commits before the branch point, and two days before this card was filed.

This is the third time today I have made the same mistake: confirm a part of a claim, then let the confirmed part license not checking the operative part. The row values were real, the line numbers were real, and none of that was the question. The one line in my order that saved it was telling the dev to re-derive the census rather than trust my list — which is only load-bearing because I turned out to be wrong.

Where the defect actually lives

examples/schema-catalog/test/catalog-gallery-render.test.tsx:322role: { label: 'Role', type: 'select' }, no options, over rows carrying three values. Confirmed: git grep for that exact declaration returns one hit repo-wide, the mirror.

So this is the card's own named failure mode — two declarations of one fixture disagreeing — running in the direction the existing parity case cannot see. ⭐ That case pins the host's method names, so the field declarations were free to drift for 330 commits with all 572 cases in the file green. A parity test that compares the wrong axis is worse than none: it certifies agreement it never checked. The PR replaces it with a whole-field-surface comparison against the host source, brace-matched then JSON-ified so a broken extraction fails loudly rather than silently comparing less than it claims — which is the failure mode of the thing it replaces, closed deliberately.

Face is one file. The host needed nothing; nothing was owed there.

⭐⭐ The scope correction, and why "no visible change" is not "inert"

Triage ruled the plugin-grid / plugin-view tile text would change and pre-approved it. Measured, it does not — byte-identical across all seven users-bound entries. ObjectGrid synthesises options for an option-less select from the distinct row values (fieldMeta.options = uniqueValues.map(v => ({ value: v, label: humanizeLabel(String(v)) }))), so admin already printed as Admin.

The dev drew the distinction that matters: the labels coincide with humanizeLabel's output by construction, which is why the grid text is unchanged — not evidence the change does nothing. The grid has a fallback; the form path has none, and that is where the change is real:

before: …RoleNo options availableDepartmentStatusNo options available…
after: …RoleAdminAdminMemberViewerDepartmentStatusActiveActiveInvitedSuspended…

⭐ And the "before" string is byte-identical to the string in the card body — so the probe demonstrably measures the reported defect rather than something adjacent. That is the control that makes the whole before/after admissible.

Both states were read off the two real source files rather than hand-copied, and the probe was deleted before commit (absent from the diff, confirmed — face is one file).

Instrument hygiene

Census walked, not grepped: a script imported the host fixture, called its own find('users'), and walked every record and key with no field list decided up front — 5 rows, role 3 distinct, status 3 distinct. Agrees with the dispatch's three-and-three, which was the one part of my order that survived.

Reverse verification written first against the untouched tree: 576 tests | 3 failed | 572 skipped, with expected undefined to be truthy on the missing options and the parity case printing the host's arrays as Received against the mirror's absence. Mutation proven on disk by counting (optionless decls 1→0 each, option pairs 0→6, file sha ef0d42940a12070a). TS2307 from an unbuilt closure booked as prerequisite-not-met. The NOT-MEASURED trap closed explicitly with --listFiles. eslint caught a real problem in the first draft (a rethrow without cause) — reported rather than quietly fixed.

No changeset owed, on the gate's own verdict: both packages are private and in the ignore list.

Also filed: #6537, the two plugin-form catalog entries whose fields lists omit role/status — the #6167 follow-on triage named, carrying Blocked-by: #6317, correctly filed rather than folded since its face is entry JSON, not the fixture.

CI: 26 checks, zero failed, 8 running, on the head reported. Landing on green.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-support-ai