test(plugin-designer): the field designer's save body is pinned to the object body, never the envelope (#4546) - #4556
Merged
Conversation
…ct body, never the envelope (#4546) Part of #4546. MetadataFieldsPage reads an object through MetadataClient.get() and writes it straight back with `client.save('object', name, {...state.raw, fields})`, so whatever shape get() returns is the shape that lands in the database. Before #4271 / PR #4545 that was the response ENVELOPE, which made a routine field edit persist the envelope over the object body — the highest-severity consumer in #4545's census, and silent corruption rather than an empty render. #4545 repaired the contract at the producer, so this page was healed without being edited and nothing pinned the save path's wire shape. This adds that pin: five cases driving the page's real read/merge/save chain through a REAL MetadataClient over a fetch double answering the real server envelope. Nothing mocks get() or save(); the only double besides the transport is the presentational FieldDesigner leaf, recording its props. No product change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…ntmatter) The changeset-presence gate demands a file for any change under a released package's src/; an empty frontmatter is its explicit exemption for a test-only change, which this is. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash) — landing verified per the merge-queue discipline. Generated by Claude Code Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4546
Test-only. No product change —
MetadataFieldsPage.tsxis untouched.What is being pinned
MetadataFieldsPagereads an object throughMetadataClient.get()and writes it straight back:so whatever shape
get()hands back is the shape that lands in the database. Before #4271 / PR #4545 that was the response ENVELOPE —{ type, name, item, …ADR-0010 protection carriers }— which made a routine field edit persist the envelope OVER the object body. That was the highest-severity consumer in #4545's census: silent data corruption on a save, not merely an empty render.#4545 repaired the contract at the producer, so this page was healed without being edited — and that is exactly why nothing pinned the save path's wire shape. This adds that pin.
Shape of the pin
One new file,
packages/plugin-designer/src/MetadataFieldsPage.saveEnvelope.test.tsx, written the way #4545's suite is:MetadataClient, constructed in the test and handed to the page as itsclientprop. Nothing mocksget(),save(), or anything else on it;{ fields }body the repo's older doubles invented from the docblock. A double written against documentation instead of the wire is what let this defect hide inside 3,628 green tests;The only double besides the transport is
FieldDesigner, the presentational leaf, recording its props. The unit under test is the page's read/merge/save chain, which runs for real; the grid-and-drawer UI that produces the edit is not where the corruption lives. Same shape as #4545'sPermissionAdvancedFacetsstub, and it keeps the file in the lightdomproject.The enumerated envelope keys
Measured off
MetadataLayeredinpackages/data-objectstack/src/metadata-client.ts(the ADR-0010 protection-envelope block) and #4545'sA2fixture, not guessed:item,lock,lockReason,lockSource,lockDocsUrl,provenance,packageId,packageVersion,editable,deletable,resettable,_diagnosticsTwo keys are handled apart from that list, deliberately:
nameis not in it. The envelope'snameand the body'snamecarry the same string, which is precisely whynamecan never detect this corruption.typeis checked separately. It is the envelope's discriminator, and an object document has no top-leveltypeof its own — but a view does ({ name, type: 'grid', … }), so the general rule asserted here is the identity TRIPLE (typestring +namestring + anitemslot — the same presence testMetadataClient.isMetaItemEnvelopeuses), nottypealone.Enumeration is the readable half; the load-bearing half is
S1, which deep-equals the whole non-fieldsremainder of the body against the object document, so any stray envelope key fails without having been named in advance.The five cases
/api/v1/meta/object/showcase_projectfieldsremainder deep-equals the object document (name,label,description,indexes,hooks), the edit landed, and the per-fieldhelpTextthe designer knows nothing about survived the mergetypediscriminator, and does not itself satisfy the envelope identity tripleDiscrimination proof
Ruling 2 asks the pin to be demonstrably capable of catching the #4271 corruption. The split was predicted in writing before running (1 green / 4 red), and it came out exactly as predicted.
Method:
git checkout c0f9a4bd5 -- packages/data-objectstack/src/metadata-client.ts—c0f9a4bd5is479cc7b46^, the commit immediately before PR #4545 landed the unwrap. Working-tree only, nevergit stash, then@object-ui/data-objectstackrebuilt so no stale artifact could be blamed. (Vitest resolves@object-ui/data-objectstackthrough the root config's source alias, so the revert is what the run reads either way; the rebuild removes the question.)S1's failure is the #4271 corruption captured verbatim — the whole real object demoted into
item, the protection carriers spread at the top level, andfieldswritten alongside them:S2 and S4 name the leaking carrier one at a time (
expected { key: 'item', present: true } to deeply equal { key: 'item', present: false }), and S3 reportsexpected [] to deeply equal [ 'name', 'stage', 'amount' ]— the read half.S0 stayed green in both directions, deliberately. It is the guard that proves S1–S4's red is about the payload and not about a save that silently never fired: the envelope defect moves the body only, never the route, the verb, or whether a save happens.
One authoring correction, reported rather than papered over: S4 first went red on a setup wait (
waitForon the reloaded field list) instead of on its own second-PUT assertion, because under the broken client the reload also yields an empty designer. That made the case red for the right reason at the wrong line — it would have hidden the shape S4 exists to pin. The wait was replaced with a plain commit flush, and the re-run puts S4's red on its own assertion, as shown above.Restore:
git checkout HEAD -- packages/data-objectstack/src/metadata-client.ts, verified byte-identical by sha256 (68bb36d59717a1f936e1d1d26c134959a03241742c58d6446ec46d4df27608b3before and after),data-objectstackrebuilt, andgit status --porcelainconfirmed empty of anydata-objectstackentry before the first commit. The branch diff is two files: the test and the changeset.Premise
Verified, and it holds: on current
origin/main(fa2125400) the save path sends the object body. Ruling 3's inversion condition did not fire — nothing envelope-shaped is on the wire today, which is what #4545 promised and what this now pins.Verification
@object-ui/plugin-designer^...+ itself)plugin-designersuitetsc --noEmitandtsconfig.test.json)origin/mainbaselinecheck:control-bytescheck-changeset-presencecheck-changeset-fixed/check-changeset-no-majorcheck:phantom-depsChangeset
Ruling 4 said none was expected and to use the presence gate's own verdict. The gate demanded a file (
1 source file(s) of 1 released package(s) changed, and this change adds no changeset), so.changeset/field-designer-save-body-pin-4546.mdwas added with an empty frontmatter — the gate's explicit exemption, releasing nothing, never major. objectui has noskip-changesetlabel escape hatch on that workflow; the file is the only way to answer it.Surface
packages/plugin-designer/src/MetadataFieldsPage.saveEnvelope.test.tsxand the changeset — nothing else.MetadataFieldsPage.tsx,data-objectstack,app-shell,plugin-grid,plugin-gantt, the CelPredicateField family andcontent/docs/releases/are all untouched.Generated by Claude Code