Repository navigation
Vendored scan / get --dry-run on an already-vendored npm project says it "Would download and vendor N patches", while its own JSON and the wet run say there is nothing to do #1850
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
Run scan --mode vendored --dry-run (or get <uuid> --mode vendored --dry-run) on a project whose patches are already vendored and in sync. The human output lists every patch under "Patches to vendor:" and ends with [dry-run] Would download and vendor N patches. No changes made. The same preview in --json correctly reports each patch as skipped / already_vendored, and the wet run prints [skip] … (already vendored) and All N packages are already vendored; nothing to do.
The other modes already get this right on the same project:
- agent:
[skip] pkg:npm/left-pad@1.3.0 (already recorded: …)andAll selected patches are already recorded in the manifest … - hosted:
1 package is already on hosted patches; nothing to rewrite.
The preview already classifies the rows (PreviewVerdict::AlreadyVendored). Only the human renderer ignores that verdict: the headline subtracts would-refuse rows but not already-vendored ones, and no [skip] line is printed.
Impact
- A user or CI step that previews before vendoring sees work that the wet run won't do, and can't tell from the human output that the project is already in sync.
- This contradicts the stated intent of the headline (
scan/render.rs: "so the headline never promises to vendor what the wet run refuses"), and the human and JSON outputs of the same dry run disagree. - Nothing is written, so this is a reporting bug only.
Repro (Linux, npm 10.9.4; same on 8.19.4 and 12.2.1)
The patch API is a local mock (--api-url / --patch-server-url) serving a free patch for left-pad@1.3.0. Any free npm patch shows the same behaviour.
mkdir dr && cd dr
echo '{"name":"dr","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
npm install
socket-patch scan --mode vendored --yes # Vendored 1 package.
socket-patch scan --mode vendored --dry-run
# Patches to vendor:
# pkg:npm/left-pad@1.3.0 [FREE] high ...
# [dry-run] Would download and vendor 1 patch. No changes made.
socket-patch scan --mode vendored --dry-run --json | jq '[.events[].errorCode], .summary.skipped'
# ["already_vendored"] 1
socket-patch get <uuid> --mode vendored --dry-run
# [dry-run] Would download and vendor 1 patch. No changes made.
socket-patch scan --mode vendored --yes
# [skip] pkg:npm/left-pad@1.3.0 (already vendored)
# 1 package is already vendored; nothing to do.A hosted→vendored eject of two packages (left-pad, is-number) followed by scan --mode vendored --dry-run gives Would download and vendor 2 patches, and the wet re-run gives All 2 packages are already vendored; nothing to do.
Expected vs actual
- Expected: the human dry-run matches its own JSON and the wet run, as agent and hosted dry runs already do. Already-vendored rows should print as
[skip] <purl> (already vendored)and be left out of the "would download and vendor" count, e.g.[dry-run] Would download and vendor 0 of 1 patch (1 already vendored), or the wet run's "already vendored; nothing to do" wording. CLI_CONTRACT.md describes--dry-runas a non-mutating preview of the run. - Actual: the human headline counts
selected.len()minus refusals only, so in-sync patches show as work to do.
OS × version
| OS | npm | scan --mode vendored --dry-run (human) |
same, --json |
get <uuid> --mode vendored --dry-run |
wet re-run |
|---|---|---|---|---|---|
| Linux | 8.19.4 | wrong ("Would … vendor 1 patch") | already_vendored (right) |
not run | nothing to do |
| Linux | 10.9.4 | wrong (2/2 runs, 1- and 2-package projects) | already_vendored |
wrong | nothing to do |
| Linux | 12.2.1 | wrong | already_vendored |
not run | nothing to do |
| macOS / Windows | — | not probed (probe branches are blocked for this routine); the code path is OS-independent |
Not a regression: published v5.0.1 prints the same output. Tested on main c07af61. The renderer is shared by every vendored ecosystem, so other package managers are probably affected too. Only npm was tested.
Suspect code
crates/socket-patch-cli/src/commands/scan/mod.rs:3410:render::Plan::Vendor(selected.len()), and:3442-3443pass onlyrefused_count()todry_run_line.crates/socket-patch-cli/src/commands/scan/render.rs:259(dry_run_line) knows only refusals.crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:157(print_refusals) printsWouldRefuserows but dropsAlreadyVendoredrows.record_into(:118) handles them for JSON.crates/socket-patch-cli/src/commands/get.rs:2303:format_dry_run("download and vendor", selected.len()).crates/socket-patch-cli/src/commands/scan/mod.rs:3368lists in-sync patches under "Patches to vendor:".
Activity
mikolalysenko commentedon Oct 29, 2026
[agent] Triaged: priority:p1 (npm). This is a reporting-only bug in the vendored dry-run human renderer (
scan/render.rsdry_run_line,vendor_flow.rsprint_refusals,get.rsformat_dry_run). Those paths are shared by every vendored ecosystem. No open PR covers it, and it doesn't duplicate an existing issue.Generated by Claude Code
mikolalysenko commentedon Oct 29, 2026
[agent] Claiming this issue (shared root cause: vendored dry-run human renderer ignores the already-vendored verdict). Branch: agent/fix-vendored-dry-run-already-vendored. Claim-ID: 2026-10-29T21:20:55Z-86f891
Generated by Claude Code
mikolalysenko commentedon Oct 29, 2026
[agent] Draft PR: #1856
Generated by Claude Code
Skip in-sync patches in vendored dry runs
Print the vendored preview through one method