Skip to content

fix(rollback): surface missing-blob aborts in the JSON envelope - #204

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
fix/rollback-json-diagnostics
Aug 19, 2026
Merged

fix(rollback): surface missing-blob aborts in the JSON envelope#204
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
fix/rollback-json-diagnostics

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Problem

When rollback aborted because beforeHash blobs were missing (--offline) or undownloadable (online 404), the --json envelope was contentless and self-contradictory: exit 1 with {"status":"partial_failure","rolledBack":0,"failed":0,"warnings":[],"results":[]}failed: 0 on a failed run, no error field, nothing on stderr. Human mode explained the failure fine. A JSON consumer learned nothing. (Found on every apply leg of the 2026-08-18 pnpm e2e matrix; the bug is ecosystem-independent.)

Two smaller issues rode along: the "No SOCKET_API_TOKEN set" advisory printed twice per invocation (once per internal ApiClient construction), and the abort error string hand-copied the engine's first-blocking-file shape.

Fix

  • Both pre-flight abort paths now synthesize one failed RollbackResult per gated package: purl, filesVerified records with the engine's existing missing_blob status naming the missing hash, the offline-gate or per-hash download reason, and the socket-patch repair hint. Counters follow apply's per-package semantics (failed: N, exit 1). Created-by-patch sentinels (empty beforeHash) are excluded; output is purl-sorted. No new JSON keys — only when the documented results[]/failed fields are populated changed.
  • The abort error string is built by a new cannot_rollback_error() shared with the engine's emission site, so the two shapes cannot drift.
  • The rollback path threads its ApiClient through instead of constructing a second one; the token advisory prints once.

Deliberately not changed: the snake_case partial_failure status. CLI_CONTRACT.md pins rollback as a pre-v3.0 legacy envelope whose snake_case status is the documented consumer discriminator, and rates a status-string change MAJOR — the tracked fix is the unified-envelope migration.

Verification

  • New integration tests: json-mode missing-before-blob offline abort → exit 1 with blob hash + repair hint + failed:1; human-mode parity; token note printed exactly once. Unit tests for the abort-result synthesis.
  • Rollback/remove consumer suites green (rollback_invariants, cli_rollback_silent, cli_parse_rollback, remove_invariants, remove_rollback_api_overrides).
  • Manual smoke with the release binary: offline and dead-registry online aborts both render purl + hash + reason + repair hint, failed:1, exit 1.

🤖 Generated with Claude Code


Note

Low Risk
Rollback CLI/JSON diagnostics only; no changes to patch application, auth, or blob storage semantics beyond populating existing envelope fields on abort paths.

Overview
Fixes rollback --json when the run exits before the rollback loop because beforeHash blobs are missing (--offline) or still missing after a failed download. Previously that path returned exit 1 with partial_failure, failed: 0, and empty results[] while stderr was muted under --json—machine consumers got no package, hash, or remedy.

Both pre-flight abort paths now call missing_blob_abort_results, which emits one failed RollbackResult per gated package (purl-sorted, empty beforeHash sentinels skipped): filesVerified with missing_blob, targetHash, offline or per-hash download reasons, and socket-patch repair hints; failed matches apply’s per-package semantics.

cannot_rollback_error in core centralizes the Cannot rollback: {file} - {why} string so synthesized aborts match mid-run engine failures. rollback_patches_inner accepts the telemetry ApiClient from run() so blob fetch does not build a second client (dedupes the No SOCKET_API_TOKEN advisory); the remove delegation still builds on demand when None.

Tests lock the new envelope shape, human-mode parity, and client-notice count.

Reviewed by Cursor Bugbot for commit 502fef0. Configure here.

When rollback aborted because beforeHash blobs were missing (offline) or
undownloadable (online 404), the --json envelope was contentless and
self-contradictory: exit 1 with failed:0, empty results/warnings, and no
diagnostic on any stream — while human mode explained the failure fine.
Confirmed on every apply leg of the 2026-08-18 pnpm matrix.

- Both pre-flight abort paths now synthesize one failed RollbackResult per
  gated package: purl, the missing blob hash, the offline-gate or per-hash
  download reason, and the `socket-patch repair` hint; counters follow
  apply's per-package semantics (failed:N, exit 1).
- The abort error string is built by cannot_rollback_error(), shared with
  the engine's first-blocking-file emission so the two shapes cannot
  drift.
- The "No SOCKET_API_TOKEN set" advisory printed twice per invocation
  (one per internal ApiClient construction); the client is now threaded
  through, printing once.

The envelope's snake_case partial_failure status is deliberately kept:
CLI_CONTRACT.md pins rollback as a pre-v3.0 legacy shape whose snake_case
status is the documented consumer discriminator, and a status-string
change is rated MAJOR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 502fef0. Configure here.

Comment thread crates/socket-patch-cli/src/commands/rollback.rs
…talled entries as markers

The before-blob gate ran before the installed-package match, so a manifest
entry whose package was never applied and is not on disk hard-failed the
run ('Cannot rollback: ... Before blob not found', path:"") when its blob
was missing — even though there was nothing to roll back. Found on the
2026-08-18 pnpm legacy matrix; ecosystem-independent.

- The gate now covers only installed rollback targets whose files the
  engine would genuinely read the blob for (already-original/deleted/
  drifted files no longer gate or trigger downloads).
- Not-installed in-scope entries surface as additive marker records in
  results[]: {"purl", "path": null, "skipped": "package_not_installed"} —
  never failed, never counted in rolledBack/failed, never flip the exit
  code. Not-installed-only runs keep the pinned exit-0 "succeeds quietly"
  contract: rollback's job is to make the tree unpatched, and a
  not-installed package already satisfies that end state (deliberately
  asymmetric with apply's exit-1-on-unmatched; rationale documented on
  RollbackOutcome and in the tests).
- remove's delegation keeps per-package semantics; fixtures in the
  remove/silent suites now install the fake package so the blob gate
  still fires for a genuinely installed patched package, preserving each
  test's documented intent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o-op suite

global_packages_e2e's no-op helper asserted results[] empty; the marker
records from the previous commit now legitimately appear there. Assert
the marker shape (path null, skipped=package_not_installed, no
success/error keys) instead of emptiness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sts keep their teeth

remove_network's fixtures were manifest-only; under the corrected gate
(blobs planned only for installed rollback targets) there was nothing to
roll back, so the online test observed no fetch and the offline test
succeeded. Installing the package at the patched bytes makes the nested
rollback genuinely need the beforeHash blob, preserving both tests'
documented intent (online fetches exactly that blob; --offline fails
closed without touching the network).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ollback was skipped not-installed

Review follow-up: a package that is installed but missed by the crawler
(layout gaps have precedent) reads as a benign not-installed skip; the
remove delegation then dropped the manifest entry AND swept its
beforeHash blobs — permanently destroying revert data while patched
bytes remained on disk, with no machine-visible signal.

remove now records an additive Skipped/rollback_not_installed envelope
event (naming the purl, details.beforeBlobsRetained, plus a stderr
warning) for each dropped entry whose nested rollback was skipped as
not-installed, and pins those entries' beforeHash blobs into the cleanup
sweep's keep set so a later repair / rollback --one-off can still
restore. Standalone rollback's not-installed-exit-0 contract and
--skip-rollback semantics are unchanged (both pinned).

RED-verified: pre-fix, the new test observed the before-blob swept
(blobsRemoved:2, no event).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Aug 19, 2026
…ouch trustLockfile, takeover reconciliation, revert guards (#213)

* feat(pnpm): full pnpm 7-12 vendor+hosted support — legacy lock grammars, zero-touch trustLockfile, takeover reconciliation, revert guards

Built and verified against real corepack-pinned pnpm 7.33.5 / 8.15.9 /
9.15.9 / 10.34.5 / 11.22.0 / 12.0.0-rc.7 (plus legacy 1-6 probes) in the
2026-08-18 e2e campaign.

HOSTED, pnpm 7/8: the v5.4/v6.0 refusal is replaced by a real rewrite —
every instance key of a dep is spliced (v5 /name/ver and _peer-suffixed,
v6 (peer)-parenthesized; each owns its resolution), one ledger edit per
instance; a post-splice residual detector refuses the dep set-wide if any
instance shape the splice regex cannot claim remains (no partial
rewrites). Frozen installs from empty stores land patched bytes on both
majors; tamper fails ERR_PNPM_TARBALL_INTEGRITY.

HOSTED, pnpm 11/12 zero-touch: rewriting a v9 root lock now auto-writes
trustLockfile: true into pnpm-workspace.yaml (create with scaffold or
byte-preserving append; ledger-recorded as redirect_pnpm_workspace_trust;
--no-trust-lockfile-config opt-out; only ErrorKind::NotFound creates —
an unreadable existing file falls back to guidance, never overwrite;
re-scans heal a missing config on already-redirected locks). pnpm 11.22
and 12-rc frozen installs succeed with no flags and no CI changes; 9/10
ignore the key (verified); the sha512 pin still fails closed under trust.
Warnings name the actual spliced host (userinfo stripped) and both
per-major error codes, and pre-empt pnpm 12's own rebuild-the-lock advice
that silently unpatches.

VENDOR, pnpm 7/8: new pnpm-legacy backend (flavor-stamped so older
binaries fail closed) — package.json pnpm.overrides + legacy lock surgery
emitting exactly what those majors serialize (byte-stable under pnpm's
own re-lock). pnpm <= 8 absolutizes file: specifiers, so frozen installs
are path-bound: surfaced as vendor_pnpm_legacy_absolute_specifier, with
plain `pnpm install --offline` as the moved-checkout path (marker bytes
verified). Windows-shaped canonical paths are normalized (verbatim prefix
stripped, forward slashes).

CONVERSIONS + SAFETY: vendoring over a hosted-redirected npm-family purl
now reconciles the redirect ledger (artifact-uuid-anchored matching —
version-exact, v5 underscore keys claimed; degraded ledgers keep edits
fail-closed), firing vendor_supersedes_redirect exactly once; vendor
--revert byte-restores the hosted lock from the wiring originals. All six
npm-family vendor backends refuse to delete an artifact the live lock
still references when a repair-reconstructed entry has no wiring
(vendor_wiring_unknown_revert_blocked — the revert-brick fix); repair
stamps detected flavors and preserves corrupt artifacts when no rebuild
source exists. Legacy-era diagnostics: shrinkwrap.yaml projects get
pnpm-flavored no-lockfile guidance and join the lockfile-only supplement;
vendored lock entries get redirect_pnpm_entry_vendored instead of
entry-not-found.

Tests: e2e_redirect_pnpm_build.rs (new hosted capstone: pnpm 7-11 real
corepack legs incl. the zero-touch pnpm 11 proof, tamper negative,
hermetic v5/v6 legs), e2e_vendor_pnpm_build.rs ladder (@9/@10/@11 + real
pnpm 7/8 lifecycle legs), takeover/reconciliation/guard/heal unit + e2e
suites — all RED-verified where behavior changed. CLI_CONTRACT.md and
docs/ecosystems.md updated.

Stacked on #203 (lock-inventory legacy grammars); trivially overlapping
test hunks with #204/#208 carry identical content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pnpm-legacy): moved-checkout recovery needs --no-frozen-lockfile (pnpm defaults frozen on under CI)

CI caught what local runs could not: pnpm turns --frozen-lockfile ON when
CI=true, and the pnpm <= 8 moved-checkout recovery works precisely by
re-resolving the path-bound absolute specifier — frozen semantics skip
that re-resolution (pnpm 8: ERR_PNPM_OUTDATED_LOCKFILE; pnpm 7:
stale-path install). The lifecycle legs' recovery step now passes
--no-frozen-lockfile explicitly, and the vendor_pnpm_legacy_absolute_
specifier remedy (warning text, module doc, CLI_CONTRACT.md,
docs/ecosystems.md) recommends `pnpm install --offline
--no-frozen-lockfile` so real CI users get working advice.

Verified: the full capstone (10 legs incl. real pnpm 7/8 lifecycles)
passes under CI=true locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(pnpm-legacy): lock oracle uses the real path normalizer (Windows byte-exactness)

Windows CI proved the production normalizer right and the test oracle
wrong: the hermetic splice legs built their expected absolute specifier
with raw canonicalize().display() — the \\?\C:\ verbatim form the
normalizer exists to strip. normalize_canonical_root is now pub and the
oracle consumes it at both assertion sites, so the expected string is
built by the same transformation the backend writes and cannot drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(pnpm-legacy): in-file oracles also use the shared root normalizer

Windows CI surfaced the same oracle-drift bug in the module's own unit
tests: the fixture helper handed raw canonicalize().display() (verbatim
\\?\C:\ form) to the ROOT_TOKEN substitution, the no-leak contains probe,
and the moved-checkout fixture builder. All three now go through a
canon_root_str() helper built on normalize_canonical_root, so every
oracle spells the root exactly as the splice writes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 1e2b1e9 into main Aug 19, 2026
398 of 404 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/rollback-json-diagnostics branch August 19, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants