Skip to content

Commit 42853e3

Browse files
committed
Warn on partial detail failures with no records
When some detail queries failed and the rest returned no records, the human scan printed nothing about the failures (the warnings were gated on a non-empty result, a case that used to end in the removed fetch error) while --json still reported patch_details_failed. Warn whenever at least one query succeeded. Vendored mode's human --prune GC on early exits is left to #1127 (PR #1338), which fixes it in finish_human for every early exit. Assisted-by: Claude Code:claude-opus-5-5
1 parent 5af7a1e commit 42853e3

2 files changed

Lines changed: 75 additions & 24 deletions

File tree

‎crates/socket-patch-cli/src/commands/scan/mod.rs‎

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -668,8 +668,9 @@ fn open_paragraph(opened: &mut bool) {
668668
/// arm treats an empty merged set as a fetch failure). The two output
669669
/// knobs are human-only: `show_progress` shows the status-line counter on
670670
/// stderr, `warn` prints a warning per failed package once the loop is
671-
/// done — only when some query succeeded (when every one failed, the
672-
/// caller's error line carries the cause instead, so nothing repeats).
671+
/// done — only when some query succeeded, even with no records (when
672+
/// every one failed, the caller's error line carries the cause instead,
673+
/// so nothing repeats).
673674
async fn fetch_patch_details(
674675
api_client: &socket_patch_core::api::client::ApiClient,
675676
packages: &[BatchPackagePatches],
@@ -711,7 +712,8 @@ async fn fetch_patch_details(
711712
}
712713
}
713714
status.finish();
714-
if warn && !results.is_empty() {
715+
// Not when every query failed: the caller's error line names it.
716+
if warn && failures.len() < packages.len() {
715717
for (purl, e) in &failures {
716718
eprintln!("Warning: could not fetch details for {purl}: {e}");
717719
}
@@ -3219,19 +3221,6 @@ async fn run_scan(
32193221
}
32203222
}
32213223
print_rollout_human(&stage, true, silent);
3222-
// `finish_human` previews the agent / report-only GC; vendored
3223-
// runs its own GC, so its dry run previews it here, as the
3224-
// `--json` arm reports it in `gc`.
3225-
if prune && vendor {
3226-
gc::run_human_gc(
3227-
&args.common,
3228-
&manifest_path,
3229-
&socket_dir,
3230-
&scanned_purls,
3231-
&vendored_purls,
3232-
)
3233-
.await;
3234-
}
32353224
return finish_human(0).await;
32363225
}
32373226

‎crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs‎

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2852,18 +2852,15 @@ async fn scan_empty_detail_results_exit_alike_in_human_and_json() {
28522852
}
28532853
}
28542854

2855-
/// #1062: a human `--dry-run --prune` previews the GC (once) in every
2856-
/// mode the `--json` arm previews it (`gc` block); vendored skipped it.
2855+
/// #1062: a human `--dry-run --prune` previews the GC (once) where the
2856+
/// `--json` arm previews it (`gc` block).
28572857
#[tokio::test]
28582858
async fn scan_dry_run_prune_previews_gc_in_human_and_json() {
28592859
let purl = "pkg:npm/minimist@1.2.2";
28602860
let stale = "pkg:npm/left-pad@1.3.0";
2861-
// Agent, vendored, and report-only (no mode; `--prune` below).
2862-
for mode in [
2863-
&["--mode", "agent"][..],
2864-
&["--mode", "vendored"][..],
2865-
&[][..],
2866-
] {
2861+
// Agent and report-only (no mode; `--prune` below). Vendored mode's
2862+
// human GC on early exits is #1127.
2863+
for mode in [&["--mode", "agent"][..], &[][..]] {
28672864
let mock = MockServer::start().await;
28682865
mount_batch_one(&mock, purl, UUID, "free", &[], false).await;
28692866
mount_by_package(&mock, purl, UUID, serde_json::json!({})).await;
@@ -2908,3 +2905,68 @@ async fn scan_dry_run_prune_previews_gc_in_human_and_json() {
29082905
}
29092906
}
29102907
}
2908+
2909+
/// #1062: when some detail queries fail and the rest succeed with no
2910+
/// records, the human run still warns per failed package (the `--json`
2911+
/// arm adds a `patch_details_failed` warning each) and exits like it.
2912+
#[tokio::test]
2913+
async fn scan_partial_failure_with_empty_results_still_warns() {
2914+
let empty = "pkg:npm/minimist@1.2.2";
2915+
let bad = "pkg:npm/lodash@4.17.20";
2916+
let mock = MockServer::start().await;
2917+
let patch = |purl: &str| {
2918+
serde_json::json!({
2919+
"uuid": UUID, "purl": purl, "tier": "free", "cveIds": [],
2920+
"ghsaIds": [], "severity": "high", "title": "t"
2921+
})
2922+
};
2923+
Mock::given(method("POST"))
2924+
.and(path(format!("/v0/orgs/{ORG_SLUG}/patches/batch")))
2925+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
2926+
"packages": [
2927+
{"purl": empty, "patches": [patch(empty)]},
2928+
{"purl": bad, "patches": [patch(bad)]},
2929+
],
2930+
"canAccessPaidPatches": false,
2931+
})))
2932+
.mount(&mock)
2933+
.await;
2934+
mount_by_package_empty(&mock, empty).await;
2935+
Mock::given(method("GET"))
2936+
.and(path(format!(
2937+
"/v0/orgs/{ORG_SLUG}/patches/by-package/{}",
2938+
encode_purl(bad)
2939+
)))
2940+
.respond_with(ResponseTemplate::new(500))
2941+
.mount(&mock)
2942+
.await;
2943+
2944+
let mut codes = Vec::new();
2945+
for json in [false, true] {
2946+
let tmp = tempfile::tempdir().unwrap();
2947+
write_root_package_json(tmp.path());
2948+
write_npm_package(tmp.path(), "minimist", "1.2.2", b"x\n");
2949+
write_npm_package(tmp.path(), "lodash", "4.17.20", b"x\n");
2950+
let mut extra = vec!["--mode", "agent", "--dry-run"];
2951+
if json {
2952+
extra.push("--json");
2953+
}
2954+
let (code, stdout, stderr) = run_scan_human(tmp.path(), &mock.uri(), &extra);
2955+
if json {
2956+
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON");
2957+
assert!(
2958+
v["warnings"]
2959+
.as_array()
2960+
.is_some_and(|w| w.iter().any(|w| w["code"] == "patch_details_failed")),
2961+
"{v}"
2962+
);
2963+
} else {
2964+
assert!(
2965+
stderr.contains(&format!("Warning: could not fetch details for {bad}")),
2966+
"the failed package is named; stderr={stderr}"
2967+
);
2968+
}
2969+
codes.push(code);
2970+
}
2971+
assert_eq!(codes[0], codes[1], "human and --json exit alike");
2972+
}

0 commit comments

Comments
 (0)