From 0a232e27a06d0f5768b126434f7cb27e9445e5e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 21:52:19 +0000 Subject: [PATCH 1/3] Start fix for #1089 (vendor silent slice) Assisted-by: Claude Code:claude-opus-5-5 From df12411e308d5dd0e18682c3a794f1053bb3d0a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:02:26 +0000 Subject: [PATCH 2/3] Vendor silent staging tests join vendor suite coverage_fix_vendor_silent_mute_exit.rs was its own test binary with its own runner and a raw events[0] envelope read. It now lives in the vendor suite as vendor/vendor_silent_staging_errors.rs, next to its apply twin apply/apply_silent_staging_errors.rs, so CI links one fewer test binary. The tests spawn the CLI through npm_fixture::run_cli (hermetic env), read the --json envelope through common::envelope (find_event for the failed/apply_failed event, plus its purl and partialFailure status), and the --silent checks require the error line to name the patch purl rather than matching a free-form word. The module doc now names the envelope code vendor really emits (apply_failed, not no_local_source). The Windows shard timings move the old binary's seconds onto vendor. Refs #1089. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/vendor/main.rs | 3 +- .../vendor_silent_staging_errors.rs} | 167 ++++++++---------- scripts/ci-test-durations.json | 3 +- 3 files changed, 79 insertions(+), 94 deletions(-) rename crates/socket-patch-cli/tests/{coverage_fix_vendor_silent_mute_exit.rs => vendor/vendor_silent_staging_errors.rs} (56%) diff --git a/crates/socket-patch-cli/tests/vendor/main.rs b/crates/socket-patch-cli/tests/vendor/main.rs index d7c4d834c..e7f58d516 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` staging errors 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_staging_errors; #[path = "../prebuilt_common/mod.rs"] mod prebuilt_common; diff --git a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs b/crates/socket-patch-cli/tests/vendor/vendor_silent_staging_errors.rs similarity index 56% rename from crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs rename to crates/socket-patch-cli/tests/vendor/vendor_silent_staging_errors.rs index 5d5312c25..29827df74 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_vendor_silent_mute_exit.rs +++ b/crates/socket-patch-cli/tests/vendor/vendor_silent_staging_errors.rs @@ -17,28 +17,15 @@ //! (`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}; +//! machine channel: one `failed` event for the patch with +//! `errorCode = apply_failed`. 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(), - ) -} +use serde_json::json; + +use crate::common::envelope::{find_event, json_string, parse_json_envelope}; +use crate::npm_fixture::{patch_record, run_cli, PURL, REG_RESOLVED}; /// Non-error stderr lines: drop the unconditional core API-token warning /// (both its lead line and its "Got: ... Continuing anyway" continuation) @@ -55,34 +42,37 @@ fn stderr_chatter(stderr: &str) -> Vec { .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. +/// 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): 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" + 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" + } } - }}"#, - ) - .unwrap(); + }), + ); + write_json( + ".socket/manifest.json", + json!({ "patches": { PURL: patch_record(&"a".repeat(64), &"b".repeat(64)) } }), + ); } /// A guaranteed-unreachable local endpoint: bind an ephemeral port, then @@ -96,6 +86,29 @@ fn dead_endpoint() -> String { format!("http://127.0.0.1:{port}") } +/// `vendor --api-url ...` with a syntactically valid token, +/// so every patch-content fetch fails at connect. +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_cli( + root, + &[ + "vendor", + 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 shared offline error diagnostics (errors only, /// never nothing) — the mem stager routes through the same @@ -105,7 +118,7 @@ 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"]); + let (code, stdout, stderr) = run_cli(tmp.path(), &["vendor", "--silent", "--offline"], &[]); assert_eq!(code, 1, "offline + no local source must fail: {stderr}"); assert!( stdout.trim().is_empty(), @@ -113,9 +126,11 @@ fn vendor_silent_offline_missing_source_keeps_error_output() { ); 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:?}" + chatter + .iter() + .any(|l| l.contains(PURL) && l.contains("--offline")), + "--silent must keep the offline no-source error naming the patch \ + (errors only, never nothing); stderr was: {stderr:?}" ); } @@ -128,22 +143,7 @@ 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", - ], - ); + 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(), @@ -151,43 +151,28 @@ fn vendor_silent_online_fetch_failure_keeps_error_output() { ); 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:?}" + chatter + .iter() + .any(|l| l.contains(PURL) && l.contains("request")), + "--silent must keep the fetch-failure error naming the patch \ + (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. +/// 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 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", - ], - ); + let (code, stdout, stderr) = run_vendor_dead_endpoint(tmp.path(), "--json"); 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 v = parse_json_envelope(&stdout); + assert_eq!(json_string(&v, "status"), Some("partialFailure"), "{v}"); + let failed = find_event(&v, "failed", Some("apply_failed")); + assert_eq!(failed["purl"], PURL, "{v}"); let chatter = stderr_chatter(&stderr); assert!( chatter.is_empty(), diff --git a/scripts/ci-test-durations.json b/scripts/ci-test-durations.json index dcab1e773..91d4179ed 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": 18.92, "vendor_crash_safety_e2e": 0.67, "vendor_eject": 0.35, "vendor_eject_bun_lockb": 2.2, From 69c08f00427a880020f375c324f4f2723048927d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 22:07:16 +0000 Subject: [PATCH 3/3] Pin vendor's real --silent error line Review of the move found the vendor silent tests described a staging layer vendor no longer uses and matched the free-form word "request", which comes as much from the HTTP client's text as from ours. The file is now vendor/vendor_silent_errors.rs. Its docs describe what it really pins: the per-patch "Error:" line vendor prints for a patch it can't source, offline and against a dead patch service. Both --silent tests now require an "Error:" line naming the patch purl. The offline run spawns the hermetic binary directly again, so no fixture patch service or SOCKET_VENDOR_URL changes the branch under test. dead_endpoint() moves into npm_fixture.rs instead of being copied a third time into the vendor binary. The apply twin's docs no longer claim vendor emits no_local_source, and the vendor shard timing counts only the moved tests' runtime, not the old binary's link time. Refs #1089. Assisted-by: Claude Code:claude-opus-5-5 --- .../apply/apply_silent_staging_errors.rs | 3 +- crates/socket-patch-cli/tests/vendor/main.rs | 4 +- .../tests/vendor/npm_fixture.rs | 15 ++- .../vendor/vendor_gem_lockfile_only_e2e.rs | 12 +- .../vendor/vendor_rerun_no_network_e2e.rs | 12 +- ...ging_errors.rs => vendor_silent_errors.rs} | 112 +++++++++--------- scripts/ci-test-durations.json | 2 +- 7 files changed, 75 insertions(+), 85 deletions(-) rename crates/socket-patch-cli/tests/vendor/{vendor_silent_staging_errors.rs => vendor_silent_errors.rs} (54%) 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/vendor/main.rs b/crates/socket-patch-cli/tests/vendor/main.rs index e7f58d516..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, the `--silent` staging errors 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,7 +21,7 @@ mod vendor_failure_paths; mod vendor_gem_lockfile_only_e2e; mod vendor_render; mod vendor_rerun_no_network_e2e; -mod vendor_silent_staging_errors; +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_staging_errors.rs b/crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs similarity index 54% rename from crates/socket-patch-cli/tests/vendor/vendor_silent_staging_errors.rs rename to crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs index 29827df74..e15a6187d 100644 --- a/crates/socket-patch-cli/tests/vendor/vendor_silent_staging_errors.rs +++ b/crates/socket-patch-cli/tests/vendor/vendor_silent_errors.rs @@ -1,22 +1,16 @@ -//! `vendor --silent` sources-unavailable error-output contract tests. +//! `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. The in-memory staging -//! layer (`fetch_stage::stage_vendor_sources_in_memory`) muted its online -//! fetch-failure diagnostics under `--silent`: +//! 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. //! -//! - `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`. +//! Apply's staging-layer twin is `apply/apply_silent_staging_errors.rs`. //! -//! 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 +//! Under `--json` the error lines stay off stderr — the envelope is the //! machine channel: one `failed` event for the patch with //! `errorCode = apply_failed`. @@ -24,8 +18,9 @@ use std::path::Path; use serde_json::json; -use crate::common::envelope::{find_event, json_string, parse_json_envelope}; -use crate::npm_fixture::{patch_record, run_cli, PURL, REG_RESOLVED}; +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) @@ -42,9 +37,16 @@ fn stderr_chatter(stderr: &str) -> Vec { .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): the mem stager must fetch, or fail. +/// 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); @@ -75,26 +77,34 @@ fn write_sourceless_manifest(root: &Path) { ); } -/// 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-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 --api-url ...` with a syntactically valid token, -/// so every patch-content fetch fails at connect. +/// `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_cli( + run_vendor( root, &[ - "vendor", mode, "--api-url", &url, @@ -105,39 +115,31 @@ fn run_vendor_dead_endpoint(root: &Path, mode: &str) -> (i32, String, String) { "--org", "test-org", ], - &[], ) } /// `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. +/// 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_cli(tmp.path(), &["vendor", "--silent", "--offline"], &[]); + 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(PURL) && l.contains("--offline")), - "--silent must keep the offline no-source error naming the patch \ - (errors only, never nothing); stderr was: {stderr:?}" + 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-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. +/// 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(); @@ -149,19 +151,16 @@ fn vendor_silent_online_fetch_failure_keeps_error_output() { 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(PURL) && l.contains("request")), - "--silent must keep the fetch-failure error naming the patch \ - (errors only, never nothing); stderr was: {stderr:?}" + 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 staging diagnostics must NOT leak to stderr, and the envelope -/// carries a `failed` / `apply_failed` event for the patch. +/// 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(); @@ -170,13 +169,12 @@ fn vendor_json_online_fetch_failure_keeps_stderr_clean() { 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); - assert_eq!(json_string(&v, "status"), Some("partialFailure"), "{v}"); 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 suppresses the staging diagnostics on stderr (the \ - envelope is the machine channel); stderr was: {stderr:?}" + "--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 91d4179ed..ec9728018 100644 --- a/scripts/ci-test-durations.json +++ b/scripts/ci-test-durations.json @@ -235,7 +235,7 @@ "update": 16.45, "upstream_restore_golden": 8.1, "uv_hosted": 0.01, - "vendor": 18.92, + "vendor": 12.8, "vendor_crash_safety_e2e": 0.67, "vendor_eject": 0.35, "vendor_eject_bun_lockb": 2.2,