Skip to content

Commit f420739

Browse files
CI: scan suite covgap files get real homes (#1089) (#1583)
* CI: give the scan suite's covgap files real homes The scan test binary still carried four coverage-gap files, two of which tested other commands: - covgap_ecosystem_dispatch.rs held the deno apply and rollback dispatch checks as adapted copies of ecosystem_dispatch_e2e.rs's oracles. They now live in that file and use its shared oracles; RollbackFixture takes scope flags so the deno case can pass --global-prefix. - covgap_commands_fetch_stage.rs drove apply's staging output. Its cold-cache fetch duplicated apply_network.rs, so what stays is one render check per message family in apply_render.rs (the offline no-local-source report, the download result line) plus a new --json check in apply_failure_paths.rs that the offline bail reports the offline_missing_sources warning code, which no CLI test asserted before. - coverage_fix_scan_discovery_corrupt_ledger.rs and covgap_commands_scan_vendor_flow.rs already asserted through --json; they are renamed scan_prune_corrupt_vendor_ledger.rs and scan_vendor_flow.rs and parse through common::envelope. Test-only; no production change and no new test binary. Refs #1089 Assisted-by: Claude Code:claude-opus-5-5 * CI: fold review fixes into the apply staging tests - The offline --json bail already had a test in apply_silent_staging_errors.rs, so the new copy is dropped and that test now also checks the offline_missing_sources warning code, that nothing is applied, and that the manifest and .socket/blobs are left as they were. - That file's private sourceless-manifest writer is replaced by the shared write_sourceless_npm_patches fixture. - The offline report lists purls in no fixed order, so the render check counts five distinct fixture purls instead of a prefix match. - scan_vendor_flow.rs parses every envelope through common::envelope, and the deno apply runner takes env as (String, String) like the rollback runner. Refs #1089 Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 717b4a4 commit f420739

9 files changed

Lines changed: 250 additions & 597 deletions

‎crates/socket-patch-cli/tests/apply/apply_fixture.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,22 @@ pub fn write_gem_patch(root: &Path, uuid: &str, marker: &[u8]) -> PathBuf {
206206
stage_blob(root, &git_sha256(&patched), &patched);
207207
file
208208
}
209+
210+
/// A root `package.json`, an empty `node_modules/` and a manifest of `n`
211+
/// npm patches (`pkg:npm/miss-<i>@1.0.0`) with no blob, diff or package
212+
/// artifact anywhere under `.socket/`, so staging has no usable source.
213+
pub fn write_sourceless_npm_patches(root: &Path, n: u32) {
214+
write_root_package_json(root);
215+
std::fs::create_dir_all(root.join("node_modules")).expect("create node_modules");
216+
let patches: serde_json::Map<String, Value> = (0..n)
217+
.map(|i| {
218+
let uuid = format!("{i:08}-0000-4000-8000-{i:012}");
219+
let body = format!("sourceless {i}\n");
220+
(
221+
format!("pkg:npm/miss-{i}@1.0.0"),
222+
patch_record(&uuid, one_file("package/index.js", b"", body.as_bytes())),
223+
)
224+
})
225+
.collect();
226+
write_manifest(root, Value::Object(patches));
227+
}

‎crates/socket-patch-cli/tests/apply/apply_render.rs‎

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
//! `Would remove` announcement, "No patches to apply.", the
88
//! no-matching-package error block, the dry-run summary counts,
99
//! `--verbose` per-file labels, the pnpm/bun/vlt layout notes, the
10-
//! yarn-PnP refusal, the gem fallback-home skip warning and the
11-
//! `--dry-run --vex` skip line.
10+
//! yarn-PnP refusal, the gem fallback-home skip warning, the
11+
//! `--dry-run --vex` skip line, and the staging layer's offline
12+
//! no-local-source report and download result line.
1213
1314
use serde_json::json;
15+
use wiremock::matchers::{method, path};
16+
use wiremock::{Mock, MockServer, ResponseTemplate};
1417

1518
use crate::apply_fixture::*;
19+
use crate::common::git_sha256;
1620

1721
// ═══════════ go drift and reconcile reports ═══════════
1822

@@ -429,3 +433,96 @@ fn dry_run_with_vex_announces_the_skip() {
429433
"stdout={stdout}"
430434
);
431435
}
436+
437+
// ═══════════ staging: offline report and download result ═══════════
438+
439+
/// The offline no-local-source report: the header carries the TOTAL count,
440+
/// at most five purls are listed, the rest is "... and N more", and the
441+
/// `repair` remedy closes it.
442+
#[test]
443+
fn offline_missing_report_caps_purls_and_names_repair() {
444+
let tmp = tempfile::tempdir().unwrap();
445+
write_sourceless_npm_patches(tmp.path(), 7);
446+
447+
let (code, _stdout, stderr) = run_apply(tmp.path(), &["--offline"], &[]);
448+
assert_eq!(code, 1, "stderr={stderr}");
449+
assert!(
450+
stderr.contains("Error: 7 patches have no local source and --offline is set:"),
451+
"stderr={stderr}"
452+
);
453+
// Five distinct fixture purls are listed (in no fixed order).
454+
let fixture: Vec<String> = (0..7)
455+
.map(|i| format!(" - pkg:npm/miss-{i}@1.0.0"))
456+
.collect();
457+
let listed: std::collections::BTreeSet<&str> =
458+
stderr.lines().filter(|l| l.starts_with(" - ")).collect();
459+
assert_eq!(
460+
listed.len(),
461+
5,
462+
"the list is capped at five; stderr={stderr}"
463+
);
464+
assert!(
465+
listed.iter().all(|l| fixture.iter().any(|f| f == l)),
466+
"only the fixture purls are listed; stderr={stderr}"
467+
);
468+
assert!(stderr.contains(" ... and 2 more"), "stderr={stderr}");
469+
assert!(
470+
stderr.contains("Run `socket-patch repair` to download missing artifacts."),
471+
"stderr={stderr}"
472+
);
473+
}
474+
475+
/// A human-mode online apply with a cold cache: the download progress is
476+
/// transient (nothing permanent on a non-terminal stderr) and only the
477+
/// "Downloaded 1 blob" result line stays, on stderr, never stdout. The
478+
/// fetch itself is pinned through `--json` in `apply_network.rs`.
479+
#[tokio::test]
480+
async fn online_staging_prints_result_line_not_progress() {
481+
const ORG: &str = "test-org";
482+
let before = b"before\n";
483+
let after = b"after\n";
484+
let mock = MockServer::start().await;
485+
Mock::given(method("GET"))
486+
.and(path(format!(
487+
"/v0/orgs/{ORG}/patches/blob/{}",
488+
git_sha256(after)
489+
)))
490+
.respond_with(ResponseTemplate::new(200).set_body_bytes(after.to_vec()))
491+
.mount(&mock)
492+
.await;
493+
494+
let tmp = tempfile::tempdir().unwrap();
495+
write_root_package_json(tmp.path());
496+
let index = install_npm_pkg(tmp.path(), "fallback-test", "1.0.0", Some(before));
497+
write_manifest(
498+
tmp.path(),
499+
json!({
500+
"pkg:npm/fallback-test@1.0.0": patch_record(
501+
"11111111-1111-4111-8111-111111111111",
502+
one_file("package/index.js", before, after),
503+
)
504+
}),
505+
);
506+
507+
let uri = mock.uri();
508+
let (code, stdout, stderr) = run_apply(
509+
tmp.path(),
510+
&[],
511+
&[
512+
("SOCKET_API_URL", uri.as_str()),
513+
("SOCKET_API_TOKEN", "fake-token-for-test"),
514+
("SOCKET_ORG_SLUG", ORG),
515+
],
516+
);
517+
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
518+
assert_eq!(std::fs::read(&index).unwrap(), after);
519+
assert!(stderr.contains("Downloaded 1 blob"), "stderr={stderr}");
520+
assert!(
521+
!stderr.contains("Downloading missing patch artifacts"),
522+
"progress is transient; stderr={stderr}"
523+
);
524+
assert!(
525+
!stdout.contains("Downloading") && !stdout.contains("Downloaded"),
526+
"stdout={stdout}"
527+
);
528+
}

‎crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs‎

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@
2121
//! unlike vendor, which records a `failed` / `apply_failed` event for the
2222
//! patch; see `vendor/vendor_silent_errors.rs`).
2323
24-
use std::path::Path;
25-
26-
use serde_json::json;
27-
28-
use crate::apply_fixture::{patch_record, run_apply, write_manifest};
29-
use crate::common::envelope::{json_string, parse_json_envelope};
24+
use crate::apply_fixture::{run_apply, write_sourceless_npm_patches};
25+
use crate::common::envelope::{codes_in, json_string, parse_json_envelope};
3026

3127
/// Non-error stderr lines: drop the unconditional core API-token warning
3228
/// (both its lead line and its "Got: ... Continuing anyway" continuation)
@@ -43,20 +39,6 @@ fn stderr_chatter(stderr: &str) -> Vec<String> {
4339
.collect()
4440
}
4541

46-
/// Valid manifest with one npm patch entry and NO blob/diff/package
47-
/// artifact anywhere under `.socket/`: staging has no usable source.
48-
fn write_sourceless_manifest(root: &Path) {
49-
write_manifest(
50-
root,
51-
json!({
52-
"pkg:npm/left-pad@1.3.0": patch_record(
53-
"11111111-1111-4111-8111-111111111111",
54-
json!({ "index.js": { "beforeHash": "a".repeat(64), "afterHash": "b".repeat(64) } }),
55-
)
56-
}),
57-
);
58-
}
59-
6042
/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then
6143
/// release it, so every request fails fast with connection-refused.
6244
fn dead_endpoint() -> String {
@@ -75,7 +57,7 @@ fn dead_endpoint() -> String {
7557
#[test]
7658
fn apply_silent_offline_missing_source_keeps_error_output() {
7759
let tmp = tempfile::tempdir().unwrap();
78-
write_sourceless_manifest(tmp.path());
60+
write_sourceless_npm_patches(tmp.path(), 1);
7961

8062
let (code, stdout, stderr) = run_apply(tmp.path(), &["--silent", "--offline"], &[]);
8163
assert_eq!(code, 1, "offline + no local source must fail: {stderr}");
@@ -96,7 +78,7 @@ fn apply_silent_offline_missing_source_keeps_error_output() {
9678
#[test]
9779
fn apply_silent_online_download_failure_keeps_error_output() {
9880
let tmp = tempfile::tempdir().unwrap();
99-
write_sourceless_manifest(tmp.path());
81+
write_sourceless_npm_patches(tmp.path(), 1);
10082

10183
let url = dead_endpoint();
10284
let token = format!("sktsec_{}_api", "x".repeat(44));
@@ -142,7 +124,8 @@ fn apply_silent_online_download_failure_keeps_error_output() {
142124
#[test]
143125
fn apply_json_offline_missing_source_keeps_stderr_clean() {
144126
let tmp = tempfile::tempdir().unwrap();
145-
write_sourceless_manifest(tmp.path());
127+
write_sourceless_npm_patches(tmp.path(), 1);
128+
let manifest_before = std::fs::read(tmp.path().join(".socket/manifest.json")).unwrap();
146129

147130
let (code, stdout, stderr) = run_apply(tmp.path(), &["--json", "--offline"], &[]);
148131
assert_eq!(code, 1, "offline + no local source must fail: {stderr}");
@@ -152,6 +135,26 @@ fn apply_json_offline_missing_source_keeps_stderr_clean() {
152135
Some("partialFailure"),
153136
"the pinned envelope shape for the offline bail, got {v}"
154137
);
138+
// The run warning is the envelope's only explanation of the bail.
139+
let warnings = codes_in(&v["warnings"]);
140+
assert!(
141+
warnings.iter().any(|c| c == "offline_missing_sources")
142+
&& !warnings.iter().any(|c| c == "sources_download_failed"),
143+
"the offline bail reports offline_missing_sources, got {v}"
144+
);
145+
assert_eq!(v["summary"]["applied"], 0, "nothing is applied, got {v}");
146+
assert_eq!(
147+
std::fs::read(tmp.path().join(".socket/manifest.json")).unwrap(),
148+
manifest_before,
149+
"the bail must leave the manifest untouched"
150+
);
151+
assert_eq!(
152+
std::fs::read_dir(tmp.path().join(".socket/blobs"))
153+
.unwrap()
154+
.count(),
155+
0,
156+
"the bail must not stage anything into .socket/blobs"
157+
);
155158
let chatter = stderr_chatter(&stderr);
156159
assert!(
157160
chatter.is_empty(),

0 commit comments

Comments
 (0)