Uh oh!
There was an error while loading. Please reload this page.
test(plugin-detail): gate the schema chain, not just the fetch chain - #7076
Conversation
`RelatedList.unmaterializedSort.test.tsx` settled only the FETCH chain in both of its `#3950` refusal blocks — `columnSortable('name')` for the column headers, `h.schema?.type === 'data-list'` for the sort-button row — and then read a value that arrives on a different one. `withheldFromServerSort` derives its verdict from `objectSchema.fields[field]`, fetched by the effect at `RelatedList.tsx:430`; with nothing there yet it falls through to `isUnmaterializedFieldType(undefined)`, which is `false`, so `Total` keeps its header and its button. Green today only because that effect is declared before the fetch effect and both mocks resolve on the same microtask — nothing pins that ordering, and a flaky red in the merge queue ejects unrelated PRs. Measured with the timing fact mutated and nothing else: with the mock schema resolving after 50ms, `:123` reads `expected true to be false` and `:153` reads `expected <button>Total</button> to be null`, both with their stored-column controls present. Each block keeps its existing gate and adds the second one (objectui#6959's precedent, and its trap): the first proves the view committed with rows at all, which is what keeps the `toBeNull()` read non-vacuous; the second proves the schema the verdict is derived from has landed. The gate is not `toHaveBeenCalled()` — a mock call is issued one resolution before its value reaches state — it awaits the promise the component awaited and flushes through `act`. Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sam
commented
Aug 31, 2026
Dispatching seat review ( The scope extension to the sibling block at |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7007
Test-only. No runtime code changes;
packages/plugin-detail/src/RelatedList.tsxwas mutated only for the ablations below and restored fromHEADeach time (blob-hash verified), so it is not in this diff.The defect, established by measurement rather than by reading
Both
#3950refusal blocks inRelatedList.unmaterializedSort.test.tsxsettled the FETCH chain —columnSortable('name')for the headers,h.schema?.type === 'data-list'for the sort-button row — and then read a value that arrives on a DIFFERENT chain.withheldFromServerSortderives its verdict fromobjectSchema.fields[field], which the effect atRelatedList.tsx:430fetches throughgetObjectSchema. With nothing in state yet the predicate falls through toisUnmaterializedFieldType(undefined), which isfalse— "offer the sort" — soTotalstill has its header and its button.Green on
mainonly because that effect is DECLARED before the fetch effect and both mocks resolve on the same microtask. Nothing pins that ordering and nothing declares it as a requirement.The remedy: gate the schema chain as well — and the other option is not available
The card asked which of two remedies is correct before any edit. Making the assertion independent of the schema chain would mean removing the schema dependency from the value it reads — but that dependency IS the subject: the whole rule under test is "derive the refusal from the FETCHED field definition". An assertion independent of it would no longer measure
#3950. So: gate the second chain.Following #6959's precedent rather than diverging from it. Each block keeps its existing gate and adds a second one. The two prove different facts:
queryByRole(…)).toBeNull()read below from passing vacuously (the button row renders only whenrelatedData.length > 0).Trading one for the other would have left the block blind on whichever side it dropped, which is exactly the trap #6959 recorded.
⛔ The added gate is not
expect(getObjectSchema).toHaveBeenCalled(). A mock CALL is issued one resolution before its value reaches state — #6959's own lesson.settleObjectSchema()uses that only to get hold of the promise; the gate is awaiting the same promise the component awaited (the component registered its.then(setObjectSchema)on it first, so the test's continuation runs after that setter) and flushing the commit throughact.Line-number mapping, for anyone following the card
The helper added above the blocks moves both sites: the card's
:153is now:197, and the sibling at:123is now:164.Evidence
1. The flake reproduced BEFORE the fix — timing FACT mutated, assertion untouched. The mock schema resolving after 50ms instead of on the first microtask, on the unmodified base spelling:
The stored-column controls (
Quantity,Unit Price,Sequence) are present in that run and the blocks' own gates pass — the failure is precisely the ungated second settle.2. Stable after. Same 50ms probe against the fixed spelling:
Tests 5 passed (5). And at a 1500ms delay — beyond RTL's default 1000mswaitForbudget — still5 passed, with the file's test time rising to 3.15s. That distinguishes a real settle on the chain from a merely wider polling window.3. Not weakened — the subject ablation. With the fix in place,
withheldFromServerSort's live arm was ablated inRelatedList.tsx(return isUnmaterializedFieldType(fieldDef)toreturn false), proved on disk (anchor 1 to 0, marker 0 to 1,git diff --numstat=1 1), then run:Both blocks still fail for the reason they were written to catch.
4. Not weakened — the vacuity ablation. The sort-button row was disabled entirely (the row's render condition forced false). The block fails at its POSITIVE control, not silently at the negative one:
So the
toBeNull()read cannot pass by the row being absent. Both restores were verified withgit diff HEADempty and a blob-hash comparison againstHEAD.5. Chain-tied, not luck — a control from a harsher probe. With the schema FETCH ITSELF deferred 50ms inside the component (a stronger perturbation than delaying the mock), this file stays green while four sibling files in the same package go red. Details and the census are filed as #7075.
Checks run — all at
df2db0de0, clean treepnpm exec vitest run packages/plugin-detail/Test Files 118 passed (118),Tests 1089 passed (1089)pnpm run type-checkinpackages/plugin-detail(after building the dependency closure)tsc -p tsconfig.test.json --listFilesconfirms the edited test file is IN the program, so this reading covers the editpnpm run lintinpackages/plugin-detail(plain form)891 problems (0 errors, 891 warnings)— all pre-existing; the edited file contributes nonenode scripts/check-changeset-presence.mjspnpm changeset:checkmajordeclaredpnpm check:control-bytespnpm check:vi-mock-specifiers,pnpm check:vi-mock-inheritExit codes were captured before any pipe, and each row quotes the gate's own verdict line.
Scope
Beyond the card's
:153, the sibling block in the same file at:123is fixed in the same commit: measurement 1 above shows it failing under the same probe, for the same reason, with the same one-line remedy. It is the same defect class in the same claimed file — declared here rather than landed quietly.Out of scope, filed rather than fixed: #7075 — four other
plugin-detailtest files, 17 tests, measured to ride the same ungated settle.Generated by Claude Code