diff --git a/crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs b/crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs index 467d22830..f5ebf98ce 100644 --- a/crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs +++ b/crates/socket-patch-cli/tests/apply/apply_silent_staging_errors.rs @@ -18,7 +18,8 @@ //! Under `--json` both diagnostics stay off stderr — the envelope is the //! machine channel, and `apply_invariants.rs` pins its exact shape for //! this path (partialFailure, empty events, NO top-level error record — -//! deliberately distinct from vendor's `no_local_source` hard error). +//! unlike vendor, which records a `failed` / `apply_failed` event for the +//! patch; see `vendor/vendor_silent_errors.rs`). use std::path::Path; diff --git a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs b/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs deleted file mode 100644 index 5d5312c25..000000000 --- a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! `vendor --silent` sources-unavailable error-output contract tests. -//! -//! CLI_CONTRACT.md defines `--silent` as "Errors only" — never "nothing": -//! an exit-1 run with zero output is undiagnosable. The in-memory staging -//! layer (`fetch_stage::stage_vendor_sources_in_memory`) muted its online -//! fetch-failure diagnostics under `--silent`: -//! -//! - `vendor --silent` whose per-patch content fetches all fail (dead -//! endpoint) exited 1 with zero bytes on stdout+stderr (the "could not -//! fetch patch content" summary and the per-file "no blob content -//! served" line were gated on `!quiet`, i.e. muted by `--silent`), -//! because the caller (`vendor.rs`) only marks the envelope — which -//! prints exclusively under `--json`. -//! -//! Same class fixed for the DISK stager's arms in -//! `apply/apply_silent_staging_errors.rs`; the offline arm is shared -//! (`report_offline_missing`) and is pinned here for vendor too. -//! -//! Under `--json` the diagnostics stay off stderr — the envelope is the -//! machine channel (`error.code = no_local_source`, as `in_process_vendor.rs` -//! pins for the offline flavor). - -#[path = "common/mod.rs"] -mod common; -use common::{binary, hermetic_command}; - -use std::path::Path; - -/// Run `socket-patch vendor` in `cwd` with a scrubbed SOCKET_* environment -/// so ambient developer/CI configuration (tokens, silent toggles) can't -/// change the branch under test. -fn run_vendor(cwd: &Path, args: &[&str]) -> (i32, String, String) { - let mut cmd = hermetic_command(&binary()); - cmd.arg("vendor").args(args).current_dir(cwd); - let out = cmd.output().expect("run socket-patch vendor"); - ( - out.status.code().unwrap_or(-1), - String::from_utf8_lossy(&out.stdout).to_string(), - String::from_utf8_lossy(&out.stderr).to_string(), - ) -} - -/// Non-error stderr lines: drop the unconditional core API-token warning -/// (both its lead line and its "Got: ... Continuing anyway" continuation) -/// and blank lines, keep everything else. -fn stderr_chatter(stderr: &str) -> Vec { - stderr - .lines() - .filter(|l| { - !l.contains("SOCKET_API_TOKEN") - && !l.contains("Continuing anyway") - && !l.trim().is_empty() - }) - .map(|l| l.to_string()) - .collect() -} - -/// Valid manifest with one npm patch entry and NO blob/diff/package -/// artifact anywhere under `.socket/` (and no committed vendor artifact -/// to harvest) — the mem stager must fetch, or fail. -fn write_sourceless_manifest(root: &Path) { - std::fs::write( - root.join("package.json"), - r#"{"name":"fixture","version":"1.0.0"}"#, - ) - .unwrap(); - std::fs::write(root.join("package-lock.json"), r#"{"lockfileVersion":3,"packages":{"":{},"node_modules/left-pad":{"version":"1.3.0","resolved":"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz","integrity":"sha512-original"}}}"#).unwrap(); - let socket = root.join(".socket"); - std::fs::create_dir_all(&socket).unwrap(); - std::fs::write( - socket.join("manifest.json"), - r#"{ "patches": { - "pkg:npm/left-pad@1.3.0": { - "uuid": "11111111-1111-4111-8111-111111111111", - "exportedAt": "2026-01-01T00:00:00Z", - "files": { "index.js": { - "beforeHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "afterHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - }}, - "vulnerabilities": {}, "description": "x", - "license": "MIT", "tier": "free" - } - }}"#, - ) - .unwrap(); -} - -/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then -/// release it, so every request fails fast with connection-refused. -fn dead_endpoint() -> String { - let port = std::net::TcpListener::bind("127.0.0.1:0") - .unwrap() - .local_addr() - .unwrap() - .port(); - format!("http://127.0.0.1:{port}") -} - -/// `vendor --silent --offline` with a manifest patch that has no local -/// source must keep the shared offline error diagnostics (errors only, -/// never nothing) — the mem stager routes through the same -/// `report_offline_missing` as apply's disk stager. -#[test] -fn vendor_silent_offline_missing_source_keeps_error_output() { - let tmp = tempfile::tempdir().unwrap(); - write_sourceless_manifest(tmp.path()); - - let (code, stdout, stderr) = run_vendor(tmp.path(), &["--silent", "--offline"]); - assert_eq!(code, 1, "offline + no local source must fail: {stderr}"); - assert!( - stdout.trim().is_empty(), - "silent human mode writes errors to stderr, not stdout: {stdout}" - ); - let chatter = stderr_chatter(&stderr); - assert!( - chatter.iter().any(|l| l.contains("offline")), - "--silent must keep the offline no-source error (errors only, \ - never nothing); stderr was: {stderr:?}" - ); -} - -/// Online `vendor --silent` where every patch-content fetch fails (dead -/// endpoint) must keep the fetch-failure error output — the caller only -/// marks the envelope, which never prints in human mode, so a muted -/// stager means exit 1 with zero output. -#[test] -fn vendor_silent_online_fetch_failure_keeps_error_output() { - let tmp = tempfile::tempdir().unwrap(); - write_sourceless_manifest(tmp.path()); - - let url = dead_endpoint(); - let token = format!("sktsec_{}_api", "x".repeat(44)); - let (code, stdout, stderr) = run_vendor( - tmp.path(), - &[ - "--silent", - "--api-url", - &url, - "--proxy-url", - &url, - "--api-token", - &token, - "--org", - "test-org", - ], - ); - assert_eq!(code, 1, "failed fetches + no source must fail: {stderr}"); - assert!( - stdout.trim().is_empty(), - "silent human mode writes errors to stderr, not stdout: {stdout}" - ); - let chatter = stderr_chatter(&stderr); - assert!( - chatter.iter().any(|l| l.contains("request")), - "--silent must keep the fetch-failure error (errors only, \ - never nothing); stderr was: {stderr:?}" - ); -} - -/// Overshoot guard: under `--json` the envelope is the machine channel — -/// the staging diagnostics must NOT leak to stderr, and the envelope -/// carries vendor's pinned `no_local_source` hard error. -#[test] -fn vendor_json_online_fetch_failure_keeps_stderr_clean() { - let tmp = tempfile::tempdir().unwrap(); - write_sourceless_manifest(tmp.path()); - - let url = dead_endpoint(); - let token = format!("sktsec_{}_api", "x".repeat(44)); - let (code, stdout, stderr) = run_vendor( - tmp.path(), - &[ - "--json", - "--api-url", - &url, - "--proxy-url", - &url, - "--api-token", - &token, - "--org", - "test-org", - ], - ); - assert_eq!(code, 1, "failed fetches + no source must fail: {stderr}"); - let v: serde_json::Value = - serde_json::from_str(&stdout).expect("vendor --json must emit valid JSON"); - assert_eq!( - v["events"][0]["errorCode"], "apply_failed", - "the pinned envelope error for the unavailable bail, got {v}" - ); - let chatter = stderr_chatter(&stderr); - assert!( - chatter.is_empty(), - "--json suppresses the staging diagnostics on stderr (the \ - envelope is the machine channel); stderr was: {stderr:?}" - ); -} diff --git a/crates/socket-patch-cli/tests/vendor/main.rs b/crates/socket-patch-cli/tests/vendor/main.rs index d7c4d834c..9e3c81089 100644 --- a/crates/socket-patch-cli/tests/vendor/main.rs +++ b/crates/socket-patch-cli/tests/vendor/main.rs @@ -1,4 +1,4 @@ -//! `vendor`: lockfile-only gems, pristine fetch order, no-network re-runs, bun, the hosted redirects, the failure paths and the human render. +//! `vendor`: lockfile-only gems, pristine fetch order, no-network re-runs, bun, the hosted redirects, the failure paths, the `--silent` error output and the human render. //! //! One test binary per command: each module was its own binary. @@ -21,6 +21,7 @@ mod vendor_failure_paths; mod vendor_gem_lockfile_only_e2e; mod vendor_render; mod vendor_rerun_no_network_e2e; +mod vendor_silent_errors; #[path = "../prebuilt_common/mod.rs"] mod prebuilt_common; diff --git a/crates/socket-patch-cli/tests/vendor/npm_fixture.rs b/crates/socket-patch-cli/tests/vendor/npm_fixture.rs index 9b7924e47..c0c6ba9c1 100644 --- a/crates/socket-patch-cli/tests/vendor/npm_fixture.rs +++ b/crates/socket-patch-cli/tests/vendor/npm_fixture.rs @@ -1,5 +1,5 @@ -//! The offline npm fixture and runners `vendor_failure_paths.rs` and -//! `vendor_render.rs` share: a self-contained npm project with a staged +//! The offline npm fixture and runners `vendor_failure_paths.rs`, +//! `vendor_render.rs` and `vendor_silent_errors.rs` share: a self-contained npm project with a staged //! patch blob, driven either in-process (`vendor_run`) or through the built //! binary with a scrubbed child environment (`run_cli` / `vendor_cli`). No //! test mutates this process's environment. @@ -222,6 +222,17 @@ pub(crate) fn vendor_cli(cwd: &Path, extra: &[&str]) -> (i32, Value) { (code, env) } +/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then +/// release it, so every request fails fast with connection-refused. +pub(crate) fn dead_endpoint() -> String { + let port = std::net::TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port(); + format!("http://127.0.0.1:{port}") +} + /// A synthetic ledger with one entry of `eco` for [`PURL`], written via the /// real `save_state` serializer (what a tampered-but-parseable state.json /// deserializes into). diff --git a/crates/socket-patch-cli/tests/vendor/vendor_gem_lockfile_only_e2e.rs b/crates/socket-patch-cli/tests/vendor/vendor_gem_lockfile_only_e2e.rs index b7bf802a3..0592ec3b3 100644 --- a/crates/socket-patch-cli/tests/vendor/vendor_gem_lockfile_only_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor/vendor_gem_lockfile_only_e2e.rs @@ -1,6 +1,7 @@ //! Service vendoring of portable gems without a local install or registry fetch. use crate::common::{binary, git_sha256, hermetic}; +use crate::npm_fixture::dead_endpoint; use std::path::Path; @@ -161,17 +162,6 @@ fn run_vendor(root: &Path, source: &str, api_url: &str) -> (i32, serde_json::Val (out.status.code().unwrap_or(-1), v, stderr) } -/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then -/// release it, so every request fails fast with connection-refused. -fn dead_endpoint() -> String { - let port = std::net::TcpListener::bind("127.0.0.1:0") - .unwrap() - .local_addr() - .unwrap() - .port(); - format!("http://127.0.0.1:{port}") -} - fn failed_event(v: &serde_json::Value) -> &serde_json::Value { v["events"] .as_array() diff --git a/crates/socket-patch-cli/tests/vendor/vendor_rerun_no_network_e2e.rs b/crates/socket-patch-cli/tests/vendor/vendor_rerun_no_network_e2e.rs index be3e8aac8..e2f9cb7f4 100644 --- a/crates/socket-patch-cli/tests/vendor/vendor_rerun_no_network_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor/vendor_rerun_no_network_e2e.rs @@ -20,22 +20,12 @@ //! guaranteed-dead local endpoint, and patch staging reads `.socket/blobs`. use crate::common::{binary, git_sha256, hermetic}; +use crate::npm_fixture::dead_endpoint; use std::path::Path; use sha2::{Digest, Sha256}; -/// A guaranteed-unreachable local endpoint: bind an ephemeral port, then -/// release it, so every request fails fast with connection-refused. -fn dead_endpoint() -> String { - let port = std::net::TcpListener::bind("127.0.0.1:0") - .unwrap() - .local_addr() - .unwrap() - .port(); - format!("http://127.0.0.1:{port}") -} - /// `.socket/manifest.json` with one patch and its after-blob. fn write_manifest(root: &Path, purl: &str, uuid: &str, file: &str, before: &[u8], after: &[u8]) { let socket = root.join(".socket"); diff --git a/crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs b/crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs new file mode 100644 index 000000000..e15a6187d --- /dev/null +++ b/crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs @@ -0,0 +1,180 @@ +//! `vendor --silent` error-output contract tests. +//! +//! CLI_CONTRACT.md defines `--silent` as "Errors only" — never "nothing": +//! an exit-1 run with zero output is undiagnosable. These pin that a +//! patch vendor can't source still prints its per-patch `Error:` line +//! under `--silent`, both offline (the service source needs the network) +//! and online when every patch-service request fails (dead endpoint). +//! Vendor's caller marks the envelope too, but the envelope prints only +//! under `--json`, so a muted error line means exit 1 with zero output. +//! +//! Apply's staging-layer twin is `apply/apply_silent_staging_errors.rs`. +//! +//! Under `--json` the error lines stay off stderr — the envelope is the +//! machine channel: one `failed` event for the patch with +//! `errorCode = apply_failed`. + +use std::path::Path; + +use serde_json::json; + +use crate::common::envelope::{find_event, parse_json_envelope}; +use crate::common::hermetic; +use crate::npm_fixture::{dead_endpoint, patch_record, PURL, REG_RESOLVED}; + +/// Non-error stderr lines: drop the unconditional core API-token warning +/// (both its lead line and its "Got: ... Continuing anyway" continuation) +/// and blank lines, keep everything else. +fn stderr_chatter(stderr: &str) -> Vec { + stderr + .lines() + .filter(|l| { + !l.contains("SOCKET_API_TOKEN") + && !l.contains("Continuing anyway") + && !l.trim().is_empty() + }) + .map(|l| l.to_string()) + .collect() +} + +/// Whether some stderr line is the per-patch error for [`PURL`]. +fn has_patch_error_line(chatter: &[String]) -> bool { + chatter + .iter() + .any(|l| l.starts_with("Error:") && l.contains(PURL)) +} + +/// Valid npm project and manifest with one patch entry and NO +/// blob/diff/package artifact anywhere under `.socket/` (and no committed +/// vendor artifact to harvest): vendor must fetch the patch, or fail. +fn write_sourceless_manifest(root: &Path) { + let write_json = |rel: &str, value: serde_json::Value| { + let path = root.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, serde_json::to_vec_pretty(&value).unwrap()).unwrap(); + }; + write_json( + "package.json", + json!({ "name": "fixture", "version": "1.0.0" }), + ); + write_json( + "package-lock.json", + json!({ + "lockfileVersion": 3, + "packages": { + "": {}, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": REG_RESOLVED, + "integrity": "sha512-original" + } + } + }), + ); + write_json( + ".socket/manifest.json", + json!({ "patches": { PURL: patch_record(&"a".repeat(64), &"b".repeat(64)) } }), + ); +} + +/// `socket-patch vendor ` in `cwd` through the hermetic binary +/// command (every ambient `SOCKET_*` var scrubbed), with no fixture patch +/// service, so no configured vendor URL changes the branch under test. +fn run_vendor(cwd: &Path, args: &[&str]) -> (i32, String, String) { + let out = hermetic::binary_command() + .arg("vendor") + .args(args) + .current_dir(cwd) + .output() + .expect("run socket-patch vendor"); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +/// `vendor ` with the patch API and proxy on a dead endpoint, so +/// every patch-service request fails at connect. The token has the real +/// `sktsec_<44 chars>_api` shape: a placeholder such as the siblings' +/// `sktsec_placeholder_value_for_tests_api` draws the token-shape warning +/// on stderr, which the `--json` test requires to stay clean. +fn run_vendor_dead_endpoint(root: &Path, mode: &str) -> (i32, String, String) { + let url = dead_endpoint(); + let token = format!("sktsec_{}_api", "x".repeat(44)); + run_vendor( + root, + &[ + mode, + "--api-url", + &url, + "--proxy-url", + &url, + "--api-token", + &token, + "--org", + "test-org", + ], + ) +} + +/// `vendor --silent --offline` with a manifest patch that has no local +/// source must keep the per-patch error line (errors only, never nothing). +#[test] +fn vendor_silent_offline_missing_source_keeps_error_output() { + let tmp = tempfile::tempdir().unwrap(); + write_sourceless_manifest(tmp.path()); + + let (code, stdout, stderr) = run_vendor(tmp.path(), &["--silent", "--offline"]); + assert_eq!(code, 1, "offline + no local source must fail: {stderr}"); + assert!( + stdout.trim().is_empty(), + "silent human mode writes errors to stderr, not stdout: {stdout}" + ); + assert!( + has_patch_error_line(&stderr_chatter(&stderr)), + "--silent must keep the per-patch error line (errors only, never \ + nothing); stderr was: {stderr:?}" + ); +} + +/// Online `vendor --silent` where every patch-service request fails (dead +/// endpoint) must keep the per-patch error line. +#[test] +fn vendor_silent_online_fetch_failure_keeps_error_output() { + let tmp = tempfile::tempdir().unwrap(); + write_sourceless_manifest(tmp.path()); + + let (code, stdout, stderr) = run_vendor_dead_endpoint(tmp.path(), "--silent"); + assert_eq!(code, 1, "failed fetches + no source must fail: {stderr}"); + assert!( + stdout.trim().is_empty(), + "silent human mode writes errors to stderr, not stdout: {stdout}" + ); + assert!( + has_patch_error_line(&stderr_chatter(&stderr)), + "--silent must keep the per-patch error line (errors only, never \ + nothing); stderr was: {stderr:?}" + ); +} + +/// Overshoot guard: under `--json` the envelope is the machine channel — +/// the error lines must NOT leak to stderr, and the envelope carries a +/// `failed` / `apply_failed` event for the patch. +#[test] +fn vendor_json_online_fetch_failure_keeps_stderr_clean() { + let tmp = tempfile::tempdir().unwrap(); + write_sourceless_manifest(tmp.path()); + + let (code, stdout, stderr) = run_vendor_dead_endpoint(tmp.path(), "--json"); + assert_eq!(code, 1, "failed fetches + no source must fail: {stderr}"); + let v = parse_json_envelope(&stdout); + let failed = find_event(&v, "failed", Some("apply_failed")); + assert_eq!(failed["purl"], PURL, "{v}"); + let chatter = stderr_chatter(&stderr); + assert!( + chatter.is_empty(), + "--json keeps the error lines off stderr (the envelope is the \ + machine channel); stderr was: {stderr:?}" + ); +} diff --git a/scripts/ci-test-durations.json b/scripts/ci-test-durations.json index dcab1e773..ec9728018 100644 --- a/scripts/ci-test-durations.json +++ b/scripts/ci-test-durations.json @@ -38,7 +38,6 @@ "contract_gradle_codes": 0.93, "coverage_fix_rollback_ecosystem_scoped_hosted": 0.09, "coverage_fix_scan_hosted_dryrun_vendored": 1.61, - "coverage_fix_vendor_silent_mute_exit": 7.4, "covgap_api_blob_fetcher": 4.04, "covgap_commands_get": 1.81, "covgap_commands_rollback": 1.86, @@ -236,7 +235,7 @@ "update": 16.45, "upstream_restore_golden": 8.1, "uv_hosted": 0.01, - "vendor": 11.52, + "vendor": 12.8, "vendor_crash_safety_e2e": 0.67, "vendor_eject": 0.35, "vendor_eject_bun_lockb": 2.2,