Skip to content

Fix gem takeover un-hosting a grouped gem (#775) - #776

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-gem-takeover-declaration-preflight
Open

Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
mainfrom
agent/fix-gem-takeover-declaration-preflight

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #775

Summary

When a hosted gem is declared inside a group … do block, scan --mode vendored / get --mode vendored no longer un-host it and then refuse it. The takeover now asks the gem backend's Gemfile declaration gate before it restores the hosted pin:

  • Wet run: failed gemfile_declaration_not_editable (exit 1 / partial_failure). The hosted Gemfile / Gemfile.lock are byte-untouched, so the gem stays hosted-patched.
  • Dry run: the preview reports the gem as would_refuse with errorCode: gemfile_declaration_not_editable (exit 0, the same posture as the Bun/vlt would_refuse rows). It no longer says would_vendor.

socket-patch vendor (eject) already rolled back correctly and is unchanged.

Root cause

The hosted→vendored takeover (vendor_records_reusing in commands/vendor.rs) runs restore_upstream, which writes the upstream Gemfile + Gemfile.lock, before the gem vendored backend runs. The only gem gate before the restore was gem_manifest_refusal (a gems.rb twin or BUNDLE_GEMFILE). The declaration gate (plan_gemfile_edit / refuse_append_of_direct_dependency) only ran inside the backend, after the restore had been written. The dry-run preview is a ledger classification with no engine refusals, so it could not see the problem either.

Changes

  • socket-patch-core/src/vendor/gem.rs: new gem_vendor_target_preflight, the gem twin of yarn_berry_vendor_target_preflight. It runs a dry-run restore_upstream of the pin and evaluates the declaration gate on the restored Gemfile / Gemfile.lock text. That way socket-patch's own hosted source … do block is never mistaken for the user's declaration. It never writes.
  • socket-patch-cli/src/commands/vendor.rs: gem_takeover_refusal (manifest gate, then the declaration preflight) is used by the takeover loop before the restore. gem_takeover_preview_refusals does the same for the dry-run preview.
  • scan/vendor_flow.rs, scan/mod.rs, get.rs: preview_vendor_json takes the resolved takeover refusals and renders them as would_refuse rows (JSON and the human [would-refuse] lines).
  • CLI_CONTRACT.md: new "Gem preflight before the takeover" paragraph under "Takeover reconciliation".

No wrapper changes are needed: npm/, pypi/ and gem/ only dispatch to the binary.

Test evidence

Issue Test Red without fix → green with fix
#775 (wet get/scan --mode vendored) e2e_redirect_gem_build::gem_hosted_group_block_pin_survives_a_refused_vendored_takeover (real Bundler 4.0.17) Red: vendor_takeover_reverted_redirect emitted, Gemfile/lock un-hosted. Green: failed gemfile_declaration_not_editable, files byte-identical.
#775 (dry run) same test, dry_run=true legs for get and scan Red: "action": "would_vendor". Green: would_refuse + errorCode.
preflight logic vendor::gem::tests::takeover_preflight_* (3 tests: group block refused and nothing written, top-level hosted gem passes, a refused restore is left to the takeover) new
preview rendering scan::vendor_flow::preview_tests::preview_marks_a_refused_takeover_would_refuse new

Red was shown by stubbing gem_vendor_target_preflight to return None. Both the dry legs and the wet leg failed exactly as reported in #775.

The e2e respells the mock upstream as https://rubygems.org/ in the hosted pair and serves it through SOCKET_RUBYGEMS_URL. The takeover restore only re-derives a CHECKSUMS sha256 for a rubygems.org remote, which is the shape of the report.

Commands run locally:

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored: 12 + 6 passed.
  • cargo test --workspace --all-features: all pass except 16 write-failure tests across 4 binaries. Those depend on chmod making files unwritable, which does not hold when running as root (uid 0) in this sandbox. They do not touch the changed code, and CI runs them non-root.
  • Formatting: main is not cargo fmt-clean under the pinned toolchain, and CI has no fmt check. Only the hunks this PR changes are rustfmt-formatted, so the diff stays reviewable.

Follow-ups (not in this PR)

🤖 Generated with Claude Code

https://claude.ai/code/session_0145M3bGaAVYdPzNXXD75tqe


Note

Medium Risk
Changes hosted→vendored takeover ordering for gems and dry-run preview semantics; wrong preflight could block valid takeovers or still allow bad restores, but scope is gem takeover paths with new tests.

Overview
Fixes #775 by running gem vendored-mode checks before a hosted→vendored takeover restores upstream lockfiles, mirroring the existing Bun preflight.

Wet runs (vendor, scan/get --mode vendored): for hosted pkg:gem/ pins, the CLI now runs manifest refusal plus a new gem_vendor_target_preflight (dry-run restore_upstream, then the Gemfile declaration gate on the restored text). If vendored mode cannot wire the gem—e.g. it lives inside a group block—the takeover fails with gemfile_declaration_not_editable and leaves hosted Gemfile / Gemfile.lock unchanged instead of un-hosting first.

Dry runs: gem_takeover_preview_refusals feeds those refusals into preview_vendor_json, which emits would_refuse rows (not would_vendor) for scan/get vendored previews.

Docs (CLI_CONTRACT.md) and e2e/unit tests cover the group-block scenario.

Reviewed by Cursor Bugbot for commit ec8f5f1. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Vendored mode cannot edit a gem declared inside a `group` block (or
any other declaration its line grammar refuses), but the hosted ->
vendored takeover only finds that out after it has restored the
hosted pin. gem_vendor_target_preflight runs the backend's Gemfile
declaration gate on the Gemfile and Gemfile.lock text the restore
would leave, without writing anything, so the takeover can refuse
first (#775).

Assisted-by: Claude Code:claude-opus-5-5
`scan --mode vendored` and `get --mode vendored` over a hosted gem
declared inside a `group ... do` block restored its upstream entry and
only then hit `gemfile_declaration_not_editable`, so the gem ended up
neither hosted nor vendored and the next frozen install loaded the
unpatched gem. The dry run promised `would_vendor`.

The takeover now asks the gem declaration preflight before the
restore: the wet run fails `gemfile_declaration_not_editable` with the
hosted Gemfile and Gemfile.lock untouched, and the dry-run preview
reports the gem as `would_refuse` with the same code (#775).

Fixes #775

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 4, 2026 12:15
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@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.

✅ Bugbot reviewed your changes and found no new issues!

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

Reviewed by Cursor Bugbot for commit ec8f5f1. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at ec8f5f1.

  • CI: 491/491 check runs finished (485 success, 6 skipped), 0 failing.
  • Bugbot: reviewed ec8f5f1, no inline findings.
  • Reviewer focus: gem_vendor_target_preflight in vendor/gem.rs (a dry-run restore_upstream and the declaration gate run before the takeover writes anything) and the new would_refuse preview rows.

Generated by Claude Code

This branch has not been deployed

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

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants