Block a revise that would replace a different package's contents - #5245
Block a revise that would replace a different package's contents#5245QuiltSimon wants to merge 2 commits into
Conversation
…describe useParams gated only on the manifest being ready, but the manifest is keyed on `src` while the name comes from `dst`. Renaming the destination to a different existing package left the gate satisfied, and entries ship as a complete replacement list, so that package's latest revision was overwritten. Thread `src` into useParams and compare it against `dst`. A genuinely new name and the plain revise of the opened package are both still permitted.
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## master #5245 +/- ##
===========================================
- Coverage 55.20% 34.95% -20.26%
===========================================
Files 872 741 -131
Lines 36766 23811 -12955
Branches 6429 6433 +4
===========================================
- Hits 20298 8322 -11976
+ Misses 14724 13745 -979
Partials 1744 1744
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| if ( | ||
| src && | ||
| name.status._tag === 'exists' && | ||
| (dst.bucket !== src.bucket || dst.name !== src.name) | ||
| ) { | ||
| return Invalid(new ERRORS.DestinationManifestMismatch()) |
There was a problem hiding this comment.
Async mismatch remains publishable
When a revise destination's existence query is loading, or “load and revise it” updates src while the prior manifest remains ready, this exists-only guard is skipped and useParams accepts the old source entries for the new destination, allowing that existing package's contents to be replaced.
…-gate # Conflicts: # catalog/CHANGELOG.md
Found reviewing the 26.8.0 candidate. Pre-existing, and adjacent to #5165's fix rather than caused by it.
useParamsgates publishing withmanifest._tag !== 'ready' && name.status._tag !== 'new'. Butmanifestis keyed onsrc, whileparams.namecomes fromdst. So: open the revise dialog onalice/p— its manifest loadsready— then edit the Name field tobob/q, a different existing package.name.statusbecomesexists,manifestis stillreadyforalice/p, and the gate passes.Entries are sent as a complete replacement list, so this publishes
alice/p's entry list overbob/q's latest revision. Same class of silent data loss #5165 closed, one condition short.The fix
useParamsnow takessrc(threaded fromState.tsx, which already owns it) and blocks when a loaded manifest does not describe the destination:Design notes:
srcto describe a mismatch against, andSourceManifestNotLoadedis the truthful report. A test pins the ordering.src &&because plain package creation passes nosrc; gating onname.status === 'exists'alone would have broken creating into an existing name. A test pins this too.srcexplicitly rather than treatingexistsas implying a mismatch. Both are true today, but the comparison states the invariant honestly instead of relying on that coincidence.New
DestinationManifestMismatcherror besideSourceManifestNotLoaded, matching its shape. The message points at "load and revise it" — the exact link textInputs/Name.tsxalready renders for theexistsstate, which callssetSrcand is the real recovery path.Tests
Six new cases in
params.spec.ts. Two are the blocking cases; four guard the permissive side (src===dst revise, dst 'new', manifest-not-ready still reported asSourceManifestNotLoaded, no-src plain creation) so a later widening of the gate fails.TZ=UTC npx vitest run app/containers/Bucket/PackageDialog app/containers/Bucket/errors.tsx— 125 passed (119 on master)npx tsc --noEmit -p .— cleannpx oxfmt --check— cleanRegression-verified: restoring master's
params.tsfails exactly the two new blocking cases and nothing else.Known caveat
The new error's message is currently unreachable UI.
Create.tsx:129returns null whenmanifest._tag !== 'error', and the manifest isreadyin this scenario — so the push button is correctly disabled andInputs/Name.tsxstill offers the recovery link, but this specific copy never renders. WiringStatusMessageto surface it is a separate conceptual unit; flagging rather than folding it in.The Copy dialog cannot reach this gate at all (
disableRestore: truemapsexiststonew-revision), so Copy behavior is unchanged.🤖 Generated with Claude Code
Greptile Summary
This PR threads the package source into publish-parameter validation and adds a destination-manifest mismatch error intended to prevent one package's loaded entries from replacing another package. It also adds regression tests and documents the fix.
Confidence Score: 4/5
This PR should not merge until publishing remains disabled during destination-existence and source-manifest transitions, which currently leave the destructive replacement path reachable.
The new guard blocks the settled mismatch case but skips
loadingandnew-revision; retained ready manifest data can therefore still be submitted to a different existing destination.Files Needing Attention: catalog/app/containers/Bucket/PackageDialog/State/params.ts, catalog/app/containers/Bucket/PackageDialog/State/params.spec.ts
Important Files Changed
exists-only condition leaves destructive asynchronous transition paths reachable.srcinto parameter validation, althoughsrcand the asynchronously loaded manifest are not an atomic identity pair.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Manifest for package A is ready] --> B[Destination changes to existing package B] B --> C{Asynchronous state} C -->|Existence query pending| D[name status: loading] C -->|Load and revise clicked| E[src becomes B; status: new-revision] D --> F[exists-only mismatch guard skipped] E --> G[Old A query data retained while B fetches] G --> F F --> H[useParams returns Ok] H --> I[A entries submitted to destination B]Reviews (1): Last reviewed commit: "catalog: block a push whose destination ..." | Re-trigger Greptile