Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/socket-patch-cli/tests/apply/apply_fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,22 @@ pub fn write_gem_patch(root: &Path, uuid: &str, marker: &[u8]) -> PathBuf {
stage_blob(root, &git_sha256(&patched), &patched);
file
}

/// A root `package.json`, an empty `node_modules/` and a manifest of `n`
/// npm patches (`pkg:npm/miss-<i>@1.0.0`) with no blob, diff or package
/// artifact anywhere under `.socket/`, so staging has no usable source.
pub fn write_sourceless_npm_patches(root: &Path, n: u32) {
write_root_package_json(root);
std::fs::create_dir_all(root.join("node_modules")).expect("create node_modules");
let patches: serde_json::Map<String, Value> = (0..n)
.map(|i| {
let uuid = format!("{i:08}-0000-4000-8000-{i:012}");
let body = format!("sourceless {i}\n");
(
format!("pkg:npm/miss-{i}@1.0.0"),
patch_record(&uuid, one_file("package/index.js", b"", body.as_bytes())),
)
})
.collect();
write_manifest(root, Value::Object(patches));
}
101 changes: 99 additions & 2 deletions crates/socket-patch-cli/tests/apply/apply_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
//! `Would remove` announcement, "No patches to apply.", the
//! no-matching-package error block, the dry-run summary counts,
//! `--verbose` per-file labels, the pnpm/bun/vlt layout notes, the
//! yarn-PnP refusal, the gem fallback-home skip warning and the
//! `--dry-run --vex` skip line.
//! yarn-PnP refusal, the gem fallback-home skip warning, the
//! `--dry-run --vex` skip line, and the staging layer's offline
//! no-local-source report and download result line.

use serde_json::json;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::apply_fixture::*;
use crate::common::git_sha256;

// ═══════════ go drift and reconcile reports ═══════════

Expand Down Expand Up @@ -429,3 +433,96 @@ fn dry_run_with_vex_announces_the_skip() {
"stdout={stdout}"
);
}

// ═══════════ staging: offline report and download result ═══════════

/// The offline no-local-source report: the header carries the TOTAL count,
/// at most five purls are listed, the rest is "... and N more", and the
/// `repair` remedy closes it.
#[test]
fn offline_missing_report_caps_purls_and_names_repair() {
let tmp = tempfile::tempdir().unwrap();
write_sourceless_npm_patches(tmp.path(), 7);

let (code, _stdout, stderr) = run_apply(tmp.path(), &["--offline"], &[]);
assert_eq!(code, 1, "stderr={stderr}");
assert!(
stderr.contains("Error: 7 patches have no local source and --offline is set:"),
"stderr={stderr}"
);
// Five distinct fixture purls are listed (in no fixed order).
let fixture: Vec<String> = (0..7)
.map(|i| format!(" - pkg:npm/miss-{i}@1.0.0"))
.collect();
let listed: std::collections::BTreeSet<&str> =
stderr.lines().filter(|l| l.starts_with(" - ")).collect();
assert_eq!(
listed.len(),
5,
"the list is capped at five; stderr={stderr}"
);
assert!(
listed.iter().all(|l| fixture.iter().any(|f| f == l)),
"only the fixture purls are listed; stderr={stderr}"
);
assert!(stderr.contains(" ... and 2 more"), "stderr={stderr}");
assert!(
stderr.contains("Run `socket-patch repair` to download missing artifacts."),
"stderr={stderr}"
);
}

/// A human-mode online apply with a cold cache: the download progress is
/// transient (nothing permanent on a non-terminal stderr) and only the
/// "Downloaded 1 blob" result line stays, on stderr, never stdout. The
/// fetch itself is pinned through `--json` in `apply_network.rs`.
#[tokio::test]
async fn online_staging_prints_result_line_not_progress() {
const ORG: &str = "test-org";
let before = b"before\n";
let after = b"after\n";
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!(
"/v0/orgs/{ORG}/patches/blob/{}",
git_sha256(after)
)))
.respond_with(ResponseTemplate::new(200).set_body_bytes(after.to_vec()))
.mount(&mock)
.await;

let tmp = tempfile::tempdir().unwrap();
write_root_package_json(tmp.path());
let index = install_npm_pkg(tmp.path(), "fallback-test", "1.0.0", Some(before));
write_manifest(
tmp.path(),
json!({
"pkg:npm/fallback-test@1.0.0": patch_record(
"11111111-1111-4111-8111-111111111111",
one_file("package/index.js", before, after),
)
}),
);

let uri = mock.uri();
let (code, stdout, stderr) = run_apply(
tmp.path(),
&[],
&[
("SOCKET_API_URL", uri.as_str()),
("SOCKET_API_TOKEN", "fake-token-for-test"),
("SOCKET_ORG_SLUG", ORG),
],
);
assert_eq!(code, 0, "stdout={stdout}\nstderr={stderr}");
assert_eq!(std::fs::read(&index).unwrap(), after);
assert!(stderr.contains("Downloaded 1 blob"), "stderr={stderr}");
assert!(
!stderr.contains("Downloading missing patch artifacts"),
"progress is transient; stderr={stderr}"
);
assert!(
!stdout.contains("Downloading") && !stdout.contains("Downloaded"),
"stdout={stdout}"
);
}
49 changes: 26 additions & 23 deletions crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@
//! unlike vendor, which records a `failed` / `apply_failed` event for the
//! patch; see `vendor/vendor_silent_errors.rs`).

use std::path::Path;

use serde_json::json;

use crate::apply_fixture::{patch_record, run_apply, write_manifest};
use crate::common::envelope::{json_string, parse_json_envelope};
use crate::apply_fixture::{run_apply, write_sourceless_npm_patches};
use crate::common::envelope::{codes_in, json_string, parse_json_envelope};

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

/// Valid manifest with one npm patch entry and NO blob/diff/package
/// artifact anywhere under `.socket/`: staging has no usable source.
fn write_sourceless_manifest(root: &Path) {
write_manifest(
root,
json!({
"pkg:npm/left-pad@1.3.0": patch_record(
"11111111-1111-4111-8111-111111111111",
json!({ "index.js": { "beforeHash": "a".repeat(64), "afterHash": "b".repeat(64) } }),
)
}),
);
}

/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then
/// release it, so every request fails fast with connection-refused.
fn dead_endpoint() -> String {
Expand All @@ -75,7 +57,7 @@ fn dead_endpoint() -> String {
#[test]
fn apply_silent_offline_missing_source_keeps_error_output() {
let tmp = tempfile::tempdir().unwrap();
write_sourceless_manifest(tmp.path());
write_sourceless_npm_patches(tmp.path(), 1);

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

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

let (code, stdout, stderr) = run_apply(tmp.path(), &["--json", "--offline"], &[]);
assert_eq!(code, 1, "offline + no local source must fail: {stderr}");
Expand All @@ -152,6 +135,26 @@ fn apply_json_offline_missing_source_keeps_stderr_clean() {
Some("partialFailure"),
"the pinned envelope shape for the offline bail, got {v}"
);
// The run warning is the envelope's only explanation of the bail.
let warnings = codes_in(&v["warnings"]);
assert!(
warnings.iter().any(|c| c == "offline_missing_sources")
&& !warnings.iter().any(|c| c == "sources_download_failed"),
"the offline bail reports offline_missing_sources, got {v}"
);
assert_eq!(v["summary"]["applied"], 0, "nothing is applied, got {v}");
assert_eq!(
std::fs::read(tmp.path().join(".socket/manifest.json")).unwrap(),
manifest_before,
"the bail must leave the manifest untouched"
);
assert_eq!(
std::fs::read_dir(tmp.path().join(".socket/blobs"))
.unwrap()
.count(),
0,
"the bail must not stage anything into .socket/blobs"
);
let chatter = stderr_chatter(&stderr);
assert!(
chatter.is_empty(),
Expand Down
Loading
Loading