Skip to content

Fix vendored re-runs failing on unserved upgrades (#1235) - #1707

Open
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
agent/fix-vendor-unserved-superseding
Open

Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
agent/fix-vendor-unserved-superseding

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #1235

Summary

Vendored re-runs outside the npm family (Composer, Cargo, Go, gem, PyPI, Maven, NuGet) failed with exit 1 from the moment a superseding patch was published until the service built its artifact, or forever when it was build_failed / not_found. The older vendored patch was untouched and still in force the whole time. They now behave like npm (#954): the older patch is kept and reported as a skipped vendor_prebuilt_pending / vendor_prebuilt_unavailable event naming both patches, with exit 0.

Root cause

ServicePolicy::unserved (crates/socket-patch-core/src/vendor/service_fetch.rs) carried the "not served (yet)" code only under ServicePolicy::Failure, which only the npm-family backends use. Every other backend settles its service artifact under ServicePolicy::Refused, where pending and unavailable both became the generic vendor_prebuilt_required refusal. keep_older_vendored_patch in the CLI only looked for the unserved marker on a failed Done, so it never fired outside npm.

Fix

  • ServicePolicy::unserved now gives the Refused outcome the unserved code itself. All non-npm backends (and service_archive_copy, which Maven and NuGet use) go through settle, so this is the one change point.
  • keep_older_vendored_patch recognises the unserved code on a Refused outcome as well as on a failed Done, with the same vendor_entry_live check. When there is no live older vendoring, the refusal is reported as vendor_prebuilt_required exactly as before, so first-vendor failures keep their code and message.
  • The two other consumers of raw backend outcomes keep their old behaviour. takeover_dry_refusal (dry-run takeover preview) and redownload::detail (repair restores) both map an unserved refusal back to vendor_prebuilt_required, because neither has an older patch to keep.
  • vendor::is_unserved and vendor::VENDOR_PREBUILT_REQUIRED are new in core and shared by the CLI and redownload.
  • CLI_CONTRACT.md: dropped the "npm family" scoping from the scan --mode vendored paragraph and the vendor_prebuilt_* row, and described the per-backend artifact noun in the detail.

Tests

New CLI regression tests in crates/socket-patch-cli/tests/in_process_vendor.rs (gem, through scan --mode vendored against a mock API):

Test Before fix After fix
scan_vendor_gem_keeps_the_older_patch_while_the_superseding_one_is_unserved (pending_build, build_failed, not_found) FAILED: exit 1, failed vendor_prebuilt_required "prebuilt .gem is still building" ok: exit 0, skipped with the unserved code naming both uuids; Gemfile, lock, ledger and artifact byte-identical
scan_vendor_gem_unserved_upgrade_fails_when_the_old_patch_is_unwired ok ok: exit 1, vendor_prebuilt_required, no unserved code leaks
scan_vendor_gem_unserved_first_vendor_still_fails ok ok: exit 1, vendor_prebuilt_required

In the per-backend core tests, service_pending_* / service_unavailable_* for cargo, golang, composer, gem, pypi, nuget and service_copy_* (the Maven and NuGet path) now assert that the refusal carries vendor_prebuilt_pending / vendor_prebuilt_unavailable. That proves each backend hands the CLI the marker. The existing npm #954 tests stay green.

Local runs:

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --all-features -- -D warnings: clean
  • cargo test --workspace --all-features --no-fail-fast: all vendor tests pass (core lib 6725 passed; in_process_vendor all passed). 12 unrelated tests fail only in this container: 10 read-only/unremovable-file and symlinked-parent tests that cannot fail as uid 0 (in_process_redirect ×3, repair ×2, core copy_tree/vlt_heal/pypi_poetry/pypi_requirements; the two pypi ones fail identically on unmodified main here), and 2 e2e_bun_lockb tests that need network access to the public patch API and a different bun. CI runs these as non-root.

Per-issue checklist:

Review notes (/code-review high)

Fixed: the contract row now describes the per-backend artifact noun (.gem, crate, module zip, …) rather than npm-only wording, and the fallback outcome is no longer cloned up front.

Not changed, with reasons:

  • Raw unserved refusals count as benign in refusal_is_benign. Only two production paths classify backend outcomes. The vendor loop goes through keep_older_vendored_patch first, and takeover_dry_refusal maps unserved codes to vendor_prebuilt_required before the benign check. A separate marker on VendorOutcome::Refused would touch hundreds of match sites for no behaviour gain.
  • The dry-run takeover preview can disagree with the wet run when a purl has both a live hosted pin and a live older vendored entry. That state is already inconsistent, and before this PR both runs failed.
  • discover_wiring runs once per unserved package. This is unchanged from the npm path, and it only runs for packages whose superseding patch is unserved.
  • The end-to-end CLI test covers only gem. The keep logic doesn't depend on the backend, and each backend's refusal code is pinned in its own core test.

https://claude.ai/code/session_01WSzRRL9zy5mWcTuc6sxJQw


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
A gem vendored at patch A fails every `scan --mode vendored` re-run
with exit 1 once the API offers a superseding patch B whose prebuilt
.gem is still building or unavailable, although nothing was touched
and A stays in force (#1235). The npm family already keeps A and
reports a skip (#954).

Add regression tests: the kept case for pending_build, build_failed
and not_found, the unwired older patch that must still fail, and a
first vendor that must still fail with vendor_prebuilt_required.

Assisted-by: Claude Code:claude-opus-5-5
Vendored re-runs outside the npm family (Composer, Cargo, Go, gem,
PyPI, Maven, NuGet) failed with exit 1 while a superseding patch had
no prebuilt artifact yet, or at all. Only npm backends told the
vendor loop that the patch was merely not served; every other backend
reported a generic vendor_prebuilt_required refusal (#1235).

The shared service policy now gives every backend's refusal the
vendor_prebuilt_pending / vendor_prebuilt_unavailable code, and the
vendor loop keeps a live older vendored patch for those refusals as
it does for npm. With nothing to keep, the refusal is still reported
as vendor_prebuilt_required, so first-vendor failures, takeover dry
runs and repair messages are unchanged.

Fixes #1235

Assisted-by: Claude Code:claude-opus-5-5
The contract row for vendor_prebuilt_pending / _unavailable quoted the
npm wording only; other backends name their own artifact (.gem, crate,
module zip, wheel, ...). Also build the not-kept fallback only when
the older patch is not kept.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 24, 2026 05:08
@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 507fbf7. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 24, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review by the burn-down agent.

  • Head: 507fbf738ed9dd03e4ef1d9a8b88dff70bf06333
  • CI: 38/38 non-skipped checks green (50 skipped), no main-wide failures
  • Bugbot: reviewed 507fbf7, no findings; no open review threads
  • Mergeable, no CHANGELOG.md changes
  • Reviewer focus: ServicePolicy::unserved in crates/socket-patch-core/src/vendor/service_fetch.rs (now returns the unserved code under Refused) and keep_older_vendored_patch in crates/socket-patch-cli/src/commands/vendor.rs; check that the dry-run takeover and redownload paths still map unserved refusals back to vendor_prebuilt_required

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review by the burn-down agent.

  • Head: 507fbf738ed9dd03e4ef1d9a8b88dff70bf06333
  • CI: 38/38 non-skipped checks green, no main-wide failures
  • Bugbot: reviewed this head, no findings; no unresolved review threads
  • Mergeable, no CHANGELOG.md changes

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. Vendored re-runs for Composer, Cargo, Go, gem, PyPI, Maven and NuGet failed with exit 1 whenever a newer patch for an already-vendored package wasn't built yet (or never would be). The older vendored patch was still in force the whole time. Now they behave like npm (#954): the old patch is kept and reported as a skipped vendor_prebuilt_pending / vendor_prebuilt_unavailable event, exit 0. Fixes #1235.

Risk: low–medium. The core change is one line: non-npm backends now return the unserved code instead of the generic vendor_prebuilt_required. Every caller that sees raw backend outcomes maps it back when there is no older patch to keep, so first-vendor failures and repair restores report exactly what they did before. I checked all the consumers: the vendor loop, the dry-run takeover preview and redownload::detail.

Look here

Verified

  • Read the full diff (12 files).
  • Grepped every VendorOutcome::Refused consumer and dispatch_vendor_one / redownload caller: none treats the new codes wrongly. The sbt gate's SKIP_CODES are unaffected.
  • The tests would fail without the fix: the core per-backend asserts flip from required to pending/unavailable, and the PR's table shows the gem CLI test failing before the fix.
  • CHANGELOG.md is untouched.
  • CI: ci-ok and clippy green on 507fbf73. Cursor Bugbot passed with no open threads.

Changes I made: none.

Open questions: none blocking. The PR description lists the review findings deliberately left as they are, with reasons.

Auto-merge is armed, so approving sends this straight to the merge queue.


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

Vendored re-runs outside the npm family exit 1 while a superseding patch is still building, instead of keeping the older vendored patch

2 participants