Skip to content

Commit 7a62fbd

Browse files
Sweep hosted_unwind, lock_cli, list, commands/mod (+ rollback dead branch) (#1864)
* Start sweep of six CLI command files Sweep hosted_bundle, hosted_unwind, list, lock_cli, commands/mod and pypi_reinstall per the coverage notes. Assisted-by: Claude Code:claude-opus-5-5 * Drop the never-set hosted unsupported list HostedLegOutcome::unsupported was never written, so rollback's hosted_unsupported events and its exit-code conjunct were dead. run_hosted_leg now merges its two post-flush blocks and skips the bun patch-key read on a dry run, which never used it. hosted-bundle moves the bundle's option fields instead of cloning them, and its module doc lists policySha256. Assisted-by: Claude Code:claude-opus-5-5 * Tighten list and commands/mod helpers list uses ledgers::Store directly, builds uuid-only records and run warnings through their helpers, and reads the vendor ledger leniently inline (vendor_state_lenient had one caller). The can't-fail determinism test is gone. user_patches is declared with the other modules, and the mode tests compare the whole usage error, so a swapped agent/hosted variant fails them. Assisted-by: Claude Code:claude-opus-5-5 * Inline lock_cli's single-caller helpers emit, held_hint and held_message fold into their only callers, and the lifecycle essay that core apply_lock already documents is gone. The three held_message tests become one on lock_failure, and the held_hint test duplicated format_lock_error's. Output is unchanged. Assisted-by: Claude Code:claude-opus-5-5 * Print acquire_or_emit's error via format_lock_error The human branch now calls the formatter the self-enveloping sites use, so the two cannot drift. The budget test is renamed after the function it exercises. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6fa01d8 commit 7a62fbd

6 files changed

Lines changed: 161 additions & 301 deletions

File tree

‎crates/socket-patch-cli/src/commands/hosted_bundle.rs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! Stdin: `{"files": {path: text}, "binaryFiles"?: {path: base64},
1111
//! "presentOnly"?: [path], "symlinks"?: [path], "projectRoots"?: [dir],
1212
//! "pipenvMajor"?: n, "batchSize"?: n, "noSocketYml"?: bool,
13-
//! "minSeverity"?: severity, "policyPaths"?: [path],
13+
//! "minSeverity"?: severity, "policyPaths"?: [path], "policySha256"?: hex,
1414
//! "maxNewPatches"?: n | "none", "maxNewPatchesCap"?: n,
1515
//! "inFlightPatches"?: [purl]}`. Stdout: the engine result
1616
//! (`HostedScanResult`, binary contents base64), or
@@ -122,7 +122,7 @@ pub async fn run(args: HostedBundleArgs) -> i32 {
122122
print_error("invalid_bundle", &format!("cannot read stdin: {e}"));
123123
return 2;
124124
}
125-
let bundle: Bundle = match serde_json::from_str(&raw) {
125+
let mut bundle: Bundle = match serde_json::from_str(&raw) {
126126
Ok(bundle) => bundle,
127127
Err(e) => {
128128
print_error("invalid_bundle", &e.to_string());
@@ -137,14 +137,14 @@ pub async fn run(args: HostedBundleArgs) -> i32 {
137137
pipenv_major: bundle.pipenv_major,
138138
trust_lockfile_config: Some(!common.no_trust_lockfile_config),
139139
npm_allow_remote_config: Some(!common.no_npm_allow_remote_config),
140-
project_roots: bundle.project_roots.clone(),
140+
project_roots: bundle.project_roots.take(),
141141
no_socket_yml: bundle.no_socket_yml,
142-
min_severity: bundle.min_severity.clone(),
143-
policy_paths: bundle.policy_paths.clone(),
144-
policy_sha256: bundle.policy_sha256.clone(),
142+
min_severity: bundle.min_severity.take(),
143+
policy_paths: bundle.policy_paths.take(),
144+
policy_sha256: bundle.policy_sha256.take(),
145145
max_new_patches: bundle.max_new_patches,
146146
max_new_patches_cap: bundle.max_new_patches_cap,
147-
in_flight_patches: bundle.in_flight_patches.clone(),
147+
in_flight_patches: bundle.in_flight_patches.take(),
148148
..HostedScanOptions::default()
149149
};
150150
let input = match build_input(bundle, options) {

‎crates/socket-patch-cli/src/commands/hosted_unwind.rs‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! The hosted unwind leg: restore in-scope hosted pins to their default
22
//! upstream registry entries. A helper module, not a command: `rollback`,
3-
//! `remove` and `vendor` (a pre-v5 vendored-over-hosted revert) all run it
4-
//! without importing each other.
3+
//! `remove`, scan's withdrawn-patch restore and `vendor` (a pre-v5
4+
//! vendored-over-hosted revert) all run it without importing each other.
55
66
use socket_patch_core::patch::redirect::upstream::HostedPin;
77

@@ -12,8 +12,6 @@ use crate::args::GlobalArgs;
1212
pub(crate) struct HostedLegOutcome {
1313
pub(crate) reverted: Vec<String>,
1414
pub(crate) failed: Vec<(String, String)>,
15-
/// Scoped targets whose ecosystem has no per-purl hosted revert.
16-
pub(crate) unsupported: Vec<String>,
1715
pub(crate) warnings: Vec<(String, String)>,
1816
pub(crate) edited_files: std::collections::BTreeSet<String>,
1917
}
@@ -42,7 +40,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
4240
use socket_patch_core::patch::redirect::upstream::{
4341
restore_upstream, PinStatus, RestoreOptions,
4442
};
45-
// The pins are the manifest's project's (#745): restore them there.
43+
// The pins are the manifest's project's: restore them there.
4644
let rooted = common.at_project_root();
4745
let common: &GlobalArgs = &rooted;
4846

@@ -76,7 +74,11 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
7674
// rebuilt registry record is not byte-exact for every lock.
7775
bun_lockb: false,
7876
};
79-
let live_before = socket_patch_core::vendor::bun_lock::live_user_patch_keys(&common.cwd).await;
77+
let live_before = if common.dry_run {
78+
Default::default()
79+
} else {
80+
socket_patch_core::vendor::bun_lock::live_user_patch_keys(&common.cwd).await
81+
};
8082
let outcome = restore_upstream(&common.cwd, pins, &opts).await;
8183
for pin in &outcome.pins {
8284
match &pin.status {
@@ -115,7 +117,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
115117
out.edited_files
116118
.extend(outcome.reverted_files.iter().cloned());
117119
// Bun's hoisted linker keeps the patched copies of the pins it no
118-
// longer pins (#764): say so, with the install that does reinstall.
120+
// longer pins: say so, with the install that does reinstall.
119121
// A dry run says it too, so the preview's reinstall note names
120122
// `bun install --force` like the real run's.
121123
if outcome.flush_error.is_none() {
@@ -133,7 +135,7 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
133135
));
134136
}
135137
// A `bun patch` keyed by a hosted URL the restore took out of the
136-
// lock stops applying (#1019).
138+
// lock stops applying.
137139
if !common.dry_run {
138140
let live_after = bun_lock::live_user_patch_keys(&common.cwd).await;
139141
for key in live_before.difference(&live_after) {
@@ -143,11 +145,9 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
143145
));
144146
}
145147
}
146-
}
147-
// pip, PDM, uv and Pipenv keep a same-version install through a plain
148-
// install or sync (#477, #1953): name the reinstall that restores the
149-
// upstream bytes.
150-
if outcome.flush_error.is_none() {
148+
// pip, PDM, uv and Pipenv keep a same-version install through a
149+
// plain install or sync: name the reinstall that restores the
150+
// upstream bytes.
151151
use crate::commands::pypi_reinstall::{advisory, Tool, Unwound, HOSTED_CODE};
152152
let unwound: Vec<Unwound> = outcome
153153
.restored()

‎crates/socket-patch-cli/src/commands/list.rs‎

Lines changed: 38 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use clap::Args;
2+
use socket_patch_core::ledgers::Store;
23
use socket_patch_core::manifest::schema::{PatchManifest, PatchRecord};
34
use socket_patch_core::patch::redirect::upstream::HostedPin;
45
use socket_patch_core::patch::redirect::RedirectState;
@@ -16,20 +17,12 @@ pub struct ListArgs {
1617
pub common: GlobalArgs,
1718
}
1819

19-
// Where a listed record lives (see `socket_patch_core::ledgers`): the
20-
// manifest (agent mode); the hosted redirect ledger, where `scan --mode
21-
// hosted` recorded its patches; or the vendor ledger, where vendored mode
22-
// keeps every `scan`/`get --mode vendored` patch as a `detached` entry's
23-
// embedded record (a standalone `vendor` entry's fallback copy lists too
24-
// once no manifest entry covers it — the checkout `vex` attests from it).
25-
use socket_patch_core::ledgers::Store as Source;
26-
2720
/// The display order of one purl's copies: manifest, hosted, vendored.
28-
fn display_rank(source: Source) -> u8 {
21+
fn display_rank(source: Store) -> u8 {
2922
match source {
30-
Source::Manifest => 0,
31-
Source::Hosted => 1,
32-
Source::Vendored => 2,
23+
Store::Manifest => 0,
24+
Store::Hosted => 1,
25+
Store::Vendored => 2,
3326
}
3427
}
3528

@@ -38,18 +31,18 @@ fn display_rank(source: Source) -> u8 {
3831
/// `HOSTED_MODE_LABEL`'s docs) — or `None` for a manifest entry or a hosted
3932
/// pin (which has no ledger; see [`ListEntry::lockfiles`]). Shared by the
4033
/// JSON `details` and the human `Mode:` line.
41-
fn ledger_label(source: Source) -> Option<(&'static str, &'static str)> {
34+
fn ledger_label(source: Store) -> Option<(&'static str, &'static str)> {
4235
match source {
43-
Source::Manifest | Source::Hosted => None,
44-
Source::Vendored => Some((crate::commands::VENDORED_MODE_LABEL, VENDOR_STATE_REL)),
36+
Store::Manifest | Store::Hosted => None,
37+
Store::Vendored => Some((crate::commands::VENDORED_MODE_LABEL, VENDOR_STATE_REL)),
4538
}
4639
}
4740

4841
/// One listable patch record with its provenance.
4942
struct ListEntry<'a> {
5043
purl: &'a str,
5144
record: &'a PatchRecord,
52-
source: Source,
45+
source: Store,
5346
/// The lockfiles wiring a hosted pin (empty for the other sources).
5447
lockfiles: &'a [String],
5548
}
@@ -78,15 +71,7 @@ impl HostedListing {
7871
.find(|(k, r)| PurlKey::same(k, &pin.purl) && r.uuid == pin.uuid)
7972
.map(|(_, r)| r.clone())
8073
})
81-
.unwrap_or_else(|| PatchRecord {
82-
uuid: pin.uuid.clone(),
83-
exported_at: String::new(),
84-
files: Default::default(),
85-
vulnerabilities: Default::default(),
86-
description: String::new(),
87-
license: String::new(),
88-
tier: String::new(),
89-
});
74+
.unwrap_or_else(|| socket_patch_core::ledgers::uuid_only_record(&pin.uuid));
9075
HostedListing {
9176
purl: pin.purl.clone(),
9277
record,
@@ -129,7 +114,7 @@ fn combined_entries<'a>(
129114
entries.extend(hosted.iter().map(|h| ListEntry {
130115
purl: &h.purl,
131116
record: &h.record,
132-
source: Source::Hosted,
117+
source: Store::Hosted,
133118
lockfiles: &h.lockfiles,
134119
}));
135120
entries.sort_by(|a, b| {
@@ -195,7 +180,7 @@ fn build_list_envelope(entries: &[ListEntry<'_>]) -> Envelope {
195180
details["mode"] = serde_json::json!(mode);
196181
details["ledger"] = serde_json::json!(ledger);
197182
}
198-
if entry.source == Source::Hosted {
183+
if entry.source == Store::Hosted {
199184
details["mode"] = serde_json::json!(crate::commands::HOSTED_MODE_LABEL);
200185
details["lockfiles"] = serde_json::json!(entry.lockfiles);
201186
}
@@ -264,7 +249,7 @@ fn format_entry(entry: &ListEntry<'_>, color: bool) -> String {
264249
// Same labeling rule as the JSON details.
265250
lines.push(format!(" Mode: {mode} (recorded in {ledger})"));
266251
}
267-
if entry.source == Source::Hosted {
252+
if entry.source == Store::Hosted {
268253
lines.push(format!(
269254
" Mode: {} (wired in {})",
270255
crate::commands::HOSTED_MODE_LABEL,
@@ -334,11 +319,6 @@ pub async fn run(args: ListArgs) -> i32 {
334319
apply_env_toggles(&args.common);
335320
let manifest_path = args.common.resolved_manifest_path();
336321

337-
// `read_manifest` is the single source of truth for the three error
338-
// states: `Ok(None)` (file absent), `Err(InvalidData)` (present but
339-
// unparseable), and any other `Err` (genuine I/O failure). No stat
340-
// pre-check: it would report any stat failure as `manifest_not_found`
341-
// and open a TOCTOU window.
342322
// One load of the three stores, all from the SAME project as the
343323
// manifest (see below); each keeps list's own posture.
344324
let ctx = crate::commands::context::ProjectContext::new(&args.common);
@@ -370,10 +350,10 @@ pub async fn run(args: ListArgs) -> i32 {
370350
Ok(state) => state.as_ref(),
371351
Err(corrupt) => {
372352
if args.common.json {
373-
warnings.push(RunWarning {
374-
code: "redirect_ledger_corrupt".to_string(),
375-
detail: corrupt.to_string(),
376-
});
353+
warnings.push(RunWarning::new(
354+
"redirect_ledger_corrupt",
355+
corrupt.to_string(),
356+
));
377357
} else if !args.common.silent {
378358
eprintln!("Warning: {corrupt}");
379359
}
@@ -387,10 +367,7 @@ pub async fn run(args: ListArgs) -> i32 {
387367
let contested = inventory.contested_refusal();
388368
if let Some(detail) = &contested {
389369
if args.common.json {
390-
warnings.push(RunWarning {
391-
code: "hosted_wiring_contested".to_string(),
392-
detail: detail.clone(),
393-
});
370+
warnings.push(RunWarning::new("hosted_wiring_contested", detail.clone()));
394371
} else if !args.common.silent {
395372
eprintln!("Warning: {}", crate::ui::sentence_case(detail));
396373
}
@@ -426,15 +403,24 @@ pub async fn run(args: ListArgs) -> i32 {
426403
)
427404
};
428405
if args.common.json {
429-
warnings.push(RunWarning {
430-
code: "hosted_resolution_orphaned".to_string(),
431-
detail,
432-
});
406+
warnings.push(RunWarning::new("hosted_resolution_orphaned", detail));
433407
} else if !args.common.silent {
434408
eprintln!("Warning: {}", crate::ui::sentence_case(&detail));
435409
}
436410
}
437-
let vendor_state = crate::commands::vendor_state_lenient(&loaded.vendor, args.common.silent);
411+
// A broken vendor ledger degrades to nothing-to-list here (every path
412+
// that writes or attests from it fails closed instead), but says so.
413+
let vendor_state = match &loaded.vendor {
414+
Ok(state) => Some(state),
415+
Err(e) => {
416+
if !args.common.silent {
417+
eprintln!(
418+
"Warning: unreadable vendor ledger ({e}); its vendored patches are not listed"
419+
);
420+
}
421+
None
422+
}
423+
};
438424

439425
// `combined_entries` folds only real records in (a record-less legacy
440426
// vendor entry asserts no patch), so entry emptiness is the whole exit
@@ -799,17 +785,17 @@ mod tests {
799785
assert_eq!(v["events"][0]["details"]["mode"], "hosted");
800786
}
801787

802-
/// A vendor-ledger entry: `detached` with the embedded record when
803-
/// `record` is given (the manifest-free vendored posture), a legacy
804-
/// manifest-tracked entry (no record of its own) otherwise. Built from
805-
/// the on-disk JSON shape so the fixture follows the ledger schema.
806788
fn as_state(entries: &HashMap<String, VendorEntry>) -> VendorState {
807789
VendorState {
808790
entries: entries.clone(),
809791
..VendorState::new()
810792
}
811793
}
812794

795+
/// A vendor-ledger entry: `detached` with the embedded record when
796+
/// `record` is given (the manifest-free vendored posture), a legacy
797+
/// manifest-tracked entry (no record of its own) otherwise. Built from
798+
/// the on-disk JSON shape so the fixture follows the ledger schema.
813799
fn vendor_entry(purl: &str, record: Option<PatchRecord>) -> VendorEntry {
814800
serde_json::from_value(serde_json::json!({
815801
"ecosystem": "npm",
@@ -877,7 +863,7 @@ mod tests {
877863
("pkg:npm/minimist@1.2.2", "vendored"),
878864
("pkg:npm/zzz-vendored@1.0.0", "vendored"),
879865
],
880-
"purl-sorted, manifest < hosted < vendored on a tie, record-less entries skipped: {v}"
866+
"purl-sorted, manifest < hosted < vendored on a tie, record-less entries skipped: {v}"
881867
);
882868
let events = v["events"].as_array().unwrap();
883869
assert_eq!(
@@ -982,15 +968,6 @@ mod tests {
982968
);
983969
}
984970

985-
#[test]
986-
fn ordering_is_deterministic_across_builds() {
987-
// Two independent builds of the same manifest must be byte-identical.
988-
let manifest = multi_entry_manifest();
989-
let a = manifest_envelope(&manifest).to_pretty_json();
990-
let b = manifest_envelope(&manifest).to_pretty_json();
991-
assert_eq!(a, b);
992-
}
993-
994971
fn record_with(description: &str, license: &str, severity: &str, summary: &str) -> PatchRecord {
995972
let mut rec = sample_manifest().patches["pkg:npm/minimist@1.2.2"].clone();
996973
rec.description = description.to_string();
@@ -1005,7 +982,7 @@ mod tests {
1005982
ListEntry {
1006983
purl,
1007984
record,
1008-
source: Source::Manifest,
985+
source: Store::Manifest,
1009986
lockfiles: &[],
1010987
}
1011988
}

0 commit comments

Comments
 (0)