From 2b86b4d4b155299a5ade4b549d4faac39c8eb26a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:52:48 +0000 Subject: [PATCH 1/3] Start gem variant test slice for #1089 Assisted-by: Claude Code:claude-opus-5-5 From 21128b4e8ee562ab871652b7a77d2a89f812656c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 19:08:32 +0000 Subject: [PATCH 2/3] Check gem variant apply tests by JSON code The gem variant mismatch tests matched English sentences on human stderr, so a wording change could fail a test whose behaviour was still right. They now check the --json envelope (exit code, status, event actions and errorCodes) and the bytes on disk. The human wording checks move to apply_render.rs, which reuses the policy file's fixtures. The wrong-platform fixture is now a function so both files can share it. Refs #1089 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/apply/apply_render.rs | 85 ++++++++- .../apply/cli_gem_variant_mismatch_policy.rs | 178 +++++++++--------- 2 files changed, 177 insertions(+), 86 deletions(-) diff --git a/crates/socket-patch-cli/tests/apply/apply_render.rs b/crates/socket-patch-cli/tests/apply/apply_render.rs index 74d126129..a095f2991 100644 --- a/crates/socket-patch-cli/tests/apply/apply_render.rs +++ b/crates/socket-patch-cli/tests/apply/apply_render.rs @@ -7,7 +7,8 @@ //! `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, the +//! yarn-PnP refusal, the gem fallback-home skip warning, the gem +//! variant mismatch warning and no-matching-variant error, the //! `--dry-run --vex` skip line, and the staging layer's offline //! no-local-source report and download result line. @@ -401,6 +402,88 @@ mod gem_fallback_home_human { } } +// ═══════════ gem variant mismatch policy ═══════════ + +/// The human legs of `cli_gem_variant_mismatch_policy.rs`, which asserts +/// the same cases through `--json` codes. Run with that file's runner +/// (`BUNDLE_*` scrubbed) and fixtures. +mod gem_variant_mismatch_human { + use crate::cli_gem_variant_mismatch_policy::{ + multi_variant_fixture, qualified_wrong_platform_fixture, run_apply, singleton_fixture, + with_marker, LINUX_PRISTINE, SINGLETON_PURL, + }; + + const OVERWRITE_LINE: &str = "did not match the patch's expected original content"; + + /// The default-policy overwrite prints the npm-family mismatch + /// warning: the package, and what happened to the file. + #[test] + fn singleton_overwrite_prints_mismatch_warning() { + let tmp = tempfile::tempdir().unwrap(); + singleton_fixture(tmp.path()); + let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + assert_eq!(code, 0, "stderr={stderr}"); + assert!(stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); + assert!(stderr.contains(SINGLETON_PURL), "stderr={stderr}"); + assert!( + stderr.contains("applied the full verified patched content"), + "stderr={stderr}" + ); + } + + /// `--strict` prints the per-package failure with the hash refusal, + /// not the overwrite warning. + #[test] + fn singleton_strict_prints_hash_refusal() { + let tmp = tempfile::tempdir().unwrap(); + singleton_fixture(tmp.path()); + let (code, _stdout, stderr) = run_apply(tmp.path(), &["--strict"]); + assert_eq!(code, 1, "stderr={stderr}"); + assert!( + stderr.contains(&format!("Failed to patch {SINGLETON_PURL}")), + "stderr={stderr}" + ); + assert!( + stderr.contains("File hash does not match expected value"), + "stderr={stderr}" + ); + assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); + } + + /// A base with no matching variant (a wrong-platform qualified + /// singleton, or a multi-variant base whose variants all mismatch) + /// prints the no-matching-variant error, never the overwrite warning. + #[test] + fn no_matching_variant_prints_variant_error() { + let wrong_platform = tempfile::tempdir().unwrap(); + qualified_wrong_platform_fixture(wrong_platform.path()); + let none_matching = tempfile::tempdir().unwrap(); + multi_variant_fixture( + none_matching.path(), + &with_marker(LINUX_PRISTINE, b"# local tweak\n"), + ); + for root in [wrong_platform.path(), none_matching.path()] { + let (code, _stdout, stderr) = run_apply(root, &[]); + assert_eq!(code, 1, "stderr={stderr}"); + assert!( + stderr.contains("no matching variant found"), + "stderr={stderr}" + ); + assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); + } + } + + /// A skipped sibling variant prints no overwrite warning. + #[test] + fn mismatched_sibling_prints_no_overwrite_warning() { + let tmp = tempfile::tempdir().unwrap(); + multi_variant_fixture(tmp.path(), LINUX_PRISTINE); + let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + assert_eq!(code, 0, "stderr={stderr}"); + assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); + } +} + // ═══════════ dry-run --vex ═══════════ /// `apply --dry-run --vex ` announces that VEX generation is skipped. diff --git a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs index d47c732b3..cecb74fdc 100644 --- a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs +++ b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs @@ -25,17 +25,29 @@ //! (a sibling mismatch means "different distribution", not "locally //! modified"). When NO variant matches, the base still fails with //! "no matching variant found" and the file stays untouched. +//! +//! Behavior is asserted through the `--json` envelope (exit code, +//! `status`, event actions and `errorCode`s) and the bytes on disk. The +//! human wording of each case lives in `apply_render.rs` (#1089), which +//! reuses this file's fixtures. In `--json` a no-matching-variant miss +//! shows as exit 1 with `status: partialFailure` and no `applied` or +//! `failed` event for the base. +use crate::common::envelope::{ + assert_envelope_invariants, event_codes, event_purls, has_event, parse_json_envelope, +}; use crate::common::hermetic::{self, Extra}; use crate::common::{binary, git_sha256, hermetic_command}; use std::path::{Path, PathBuf}; -const SINGLETON_PURL: &str = "pkg:gem/rack@3.1.0"; +pub(crate) const SINGLETON_PURL: &str = "pkg:gem/rack@3.1.0"; const UUID_SINGLETON: &str = "31313131-3131-4131-8131-313131313131"; +/// The per-file warning code of a default-policy overwrite. +const OVERWRITTEN: &str = "content_mismatch_overwritten"; const PRISTINE: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n"; -const LOCAL: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n# local tweak\n"; +pub(crate) const LOCAL: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n# local tweak\n"; const PATCH_MARKER: &[u8] = b"\n# SOCKET-SINGLETON-PATCH\n"; const MULTI_NAME: &str = "nokogiri"; @@ -43,12 +55,12 @@ const MULTI_VERSION: &str = "1.16.5"; const UUID_LINUX: &str = "41414141-4141-4141-8141-414141414141"; const UUID_DARWIN: &str = "42424242-4242-4242-8242-424242424242"; -const LINUX_PRISTINE: &[u8] = b"module Nokogiri\n VERSION = '1.16.5'\nend\n"; +pub(crate) const LINUX_PRISTINE: &[u8] = b"module Nokogiri\n VERSION = '1.16.5'\nend\n"; const LINUX_MARKER: &[u8] = b"\n# SOCKET-LINUX-PATCH\n"; -const DARWIN_BEFORE: &[u8] = b"# nokogiri.rb from the arm64-darwin gem\n"; +pub(crate) const DARWIN_BEFORE: &[u8] = b"# nokogiri.rb from the arm64-darwin gem\n"; const DARWIN_MARKER: &[u8] = b"\n# DARWIN-MARKER\n"; -fn with_marker(base: &[u8], marker: &[u8]) -> Vec { +pub(crate) fn with_marker(base: &[u8], marker: &[u8]) -> Vec { let mut v = base.to_vec(); v.extend_from_slice(marker); v @@ -57,7 +69,7 @@ fn with_marker(base: &[u8], marker: &[u8]) -> Vec { /// Run `socket-patch apply --offline --ecosystems gem ` in `cwd` /// with the hermetic `SOCKET_*` environment (`hermetic_command`) and the /// ambient `BUNDLE_*` scrubbed, so only the argv decides behavior. -fn run_apply(cwd: &Path, extra: &[&str]) -> (i32, String, String) { +pub(crate) fn run_apply(cwd: &Path, extra: &[&str]) -> (i32, String, String) { let mut cmd = hermetic_command(&binary()); cmd.arg("apply") .args(["--offline", "--ecosystems", "gem"]) @@ -121,7 +133,7 @@ fn write_socket_dir(cwd: &Path, patches: serde_json::Value, blobs: &[(&str, &[u8 /// the afterHash blob is staged so the offline apply has the full /// patched bytes available. Returns the on-disk file path and the exact /// patched bytes a correct warn-overwrite must produce. -fn singleton_fixture(cwd: &Path) -> (PathBuf, Vec) { +pub(crate) fn singleton_fixture(cwd: &Path) -> (PathBuf, Vec) { let patched = with_marker(PRISTINE, PATCH_MARKER); let file = install_gem(cwd, "rack-3.1.0", "lib/rack.rb", LOCAL); write_socket_dir( @@ -151,50 +163,29 @@ fn singleton_mismatch_default_warns_and_applies() { let tmp = tempfile::tempdir().expect("tempdir"); let (file, patched) = singleton_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); assert_eq!( code, 0, - "default mismatch on a singleton variant is a warning, not an error; stderr={stderr}" + "default mismatch on a singleton variant is a warning, not an error; stdout={stdout}" ); assert_eq!( std::fs::read(&file).expect("read patched file"), patched, "the file must carry exactly the verified patched bytes" ); - assert!( - stderr.contains("did not match the patch's expected original content"), - "the overwrite must be surfaced as the npm-family mismatch warning; stderr={stderr}" - ); - assert!( - stderr.contains(SINGLETON_PURL), - "the warning must name the package; stderr={stderr}" - ); - assert!( - stderr.contains("applied the full verified patched content"), - "the warning must say what happened to the file; stderr={stderr}" - ); - - // JSON envelope twin: `applied` event for the purl plus the per-file - // warning event — the same shape apply_network pins for npm. - let tmp = tempfile::tempdir().expect("tempdir"); - let (file, patched) = singleton_fixture(tmp.path()); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); - assert_eq!(code, 0, "json run must also succeed; stdout={stdout}"); - assert_eq!(std::fs::read(&file).expect("read patched file"), patched); - let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON envelope"); + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); assert_eq!(v["status"], "success", "{v:#}"); - let events = v["events"].as_array().expect("events array"); - assert!( - events - .iter() - .any(|e| e["action"] == "applied" && e["purl"] == SINGLETON_PURL), - "singleton must be reported applied: {events:?}" + assert_eq!( + event_purls(&v, "applied"), + [SINGLETON_PURL], + "singleton must be reported applied: {v:#}" ); + // The overwrite rides as the npm-family per-file warning event, the + // same shape apply_network pins for npm, naming the package. assert!( - events - .iter() - .any(|e| e["errorCode"] == "content_mismatch_overwritten"), - "the overwrite must ride as a warning event: {events:?}" + has_event(&v, "skipped", OVERWRITTEN, SINGLETON_PURL), + "the overwrite must ride as a warning event: {v:#}" ); } @@ -205,28 +196,28 @@ fn singleton_mismatch_strict_refuses() { let tmp = tempfile::tempdir().expect("tempdir"); let (file, _patched) = singleton_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &["--strict"]); + let (code, stdout, _stderr) = run_apply(tmp.path(), &["--strict", "--json"]); assert_eq!( code, 1, - "--strict must refuse the mismatch; stderr={stderr}" + "--strict must refuse the mismatch; stdout={stdout}" ); assert_eq!( std::fs::read(&file).expect("read file"), LOCAL, "--strict must not modify the file" ); + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); + assert_eq!(v["status"], "partialFailure", "{v:#}"); + // The package fails on the per-file hash refusal (`apply_failed`), + // not the no-matching-variant miss, which reports no `failed` event. assert!( - stderr.contains(&format!("Failed to patch {SINGLETON_PURL}")), - "--strict must fail the package with a per-package error; stderr={stderr}" + has_event(&v, "failed", "apply_failed", SINGLETON_PURL), + "--strict must fail the package on the hash mismatch: {v:#}" ); assert!( - stderr.contains("File hash does not match expected value"), - "--strict must name the actual refusal (the hash mismatch), not a \ - generic no-matching-variant miss; stderr={stderr}" - ); - assert!( - !stderr.contains("did not match the patch's expected original content"), - "--strict must not claim an overwrite happened; stderr={stderr}" + !event_codes(&v).iter().any(|c| c == OVERWRITTEN), + "--strict must not claim an overwrite happened: {v:#}" ); } @@ -250,30 +241,21 @@ fn multi_purl(platform: &str) -> String { format!("pkg:gem/{MULTI_NAME}@{MULTI_VERSION}?platform={platform}") } -/// A QUALIFIED singleton keeps the fail-closed gate: a lone -/// `?platform=x86_64-linux` record names one specific distribution, and -/// the representative-hash check is the ONLY thing resolving whether that -/// distribution is the one on disk (the crawler drops the gem dir's -/// platform suffix). With an arm64-darwin gem installed, the default -/// apply must NOT overwrite it with the linux variant's bytes — the -/// Bundler plugin auto-applies with `--silent`, where the warn half of -/// warn-and-apply is invisible, and a later rollback would restore the -/// LINUX before-bytes onto the darwin install. It fails exactly like a -/// multi-variant base with no matching variant. -#[test] -fn qualified_singleton_wrong_platform_fails_closed() { - let tmp = tempfile::tempdir().expect("tempdir"); +/// Qualified-singleton fixture: a lone `?platform=x86_64-linux` record +/// with the arm64-darwin gem installed, whose bytes match neither of the +/// record's hashes. Returns the on-disk file path. +pub(crate) fn qualified_wrong_platform_fixture(cwd: &Path) -> PathBuf { let linux_patched = with_marker(LINUX_PRISTINE, LINUX_MARKER); // On disk: the arm64-darwin gem, whose bytes match NEITHER of the // linux record's hashes. let file = install_gem( - tmp.path(), + cwd, &format!("{MULTI_NAME}-{MULTI_VERSION}-arm64-darwin"), &format!("lib/{MULTI_NAME}.rb"), DARWIN_BEFORE, ); write_socket_dir( - tmp.path(), + cwd, serde_json::json!({ multi_purl("x86_64-linux"): patch_record( UUID_LINUX, @@ -288,20 +270,37 @@ fn qualified_singleton_wrong_platform_fails_closed() { // wrong-platform path under test is never taken. assert_ne!(git_sha256(DARWIN_BEFORE), git_sha256(LINUX_PRISTINE)); assert_ne!(git_sha256(DARWIN_BEFORE), git_sha256(&linux_patched)); + file +} + +/// A QUALIFIED singleton keeps the fail-closed gate: a lone +/// `?platform=x86_64-linux` record names one specific distribution, and +/// the representative-hash check is the ONLY thing resolving whether that +/// distribution is the one on disk (the crawler drops the gem dir's +/// platform suffix). With an arm64-darwin gem installed, the default +/// apply must NOT overwrite it with the linux variant's bytes — the +/// Bundler plugin auto-applies with `--silent`, where the warn half of +/// warn-and-apply is invisible, and a later rollback would restore the +/// LINUX before-bytes onto the darwin install. It fails exactly like a +/// multi-variant base with no matching variant. +#[test] +fn qualified_singleton_wrong_platform_fails_closed() { + let tmp = tempfile::tempdir().expect("tempdir"); + let file = qualified_wrong_platform_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); assert_eq!( code, 1, - "a qualified singleton whose distribution is not on disk must fail closed; stderr={stderr}" - ); - assert!( - stderr.contains("no matching variant found"), - "the failure must stay the no-matching-variant error; stderr={stderr}" + "a qualified singleton whose distribution is not on disk must fail closed; stdout={stdout}" ); + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); + assert_eq!(v["status"], "partialFailure", "{v:#}"); + assert!(event_purls(&v, "applied").is_empty(), "{v:#}"); assert!( - !stderr.contains("did not match the patch's expected original content"), + !event_codes(&v).iter().any(|c| c == OVERWRITTEN), "a wrong-distribution record must never be surfaced as a \ - local-modification overwrite; stderr={stderr}" + local-modification overwrite: {v:#}" ); assert_eq!( std::fs::read(&file).expect("read file"), @@ -314,7 +313,7 @@ fn qualified_singleton_wrong_platform_fails_closed() { /// beforeHash matches the on-disk bytes unless `local_bytes` overrides /// them); the arm64-darwin sibling describes a DIFFERENT distribution /// whose beforeHash can never match. Both afterHash blobs are staged. -fn multi_variant_fixture(cwd: &Path, on_disk: &[u8]) -> (PathBuf, Vec, Vec) { +pub(crate) fn multi_variant_fixture(cwd: &Path, on_disk: &[u8]) -> (PathBuf, Vec, Vec) { let linux_patched = with_marker(LINUX_PRISTINE, LINUX_MARKER); let darwin_patched = with_marker(DARWIN_BEFORE, DARWIN_MARKER); let file = install_gem( @@ -357,10 +356,10 @@ fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { let tmp = tempfile::tempdir().expect("tempdir"); let (file, linux_patched, _darwin_patched) = multi_variant_fixture(tmp.path(), LINUX_PRISTINE); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); assert_eq!( code, 0, - "the installed variant must apply cleanly; stderr={stderr}" + "the installed variant must apply cleanly; stdout={stdout}" ); let after = std::fs::read(&file).expect("read patched file"); assert_eq!( @@ -373,9 +372,17 @@ fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { .any(|w| w == DARWIN_MARKER), "the sibling distribution's bytes must never be written" ); + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); + assert_eq!(v["status"], "success", "{v:#}"); + assert_eq!( + event_purls(&v, "applied"), + [multi_purl("x86_64-linux")], + "{v:#}" + ); assert!( - !stderr.contains("did not match the patch's expected original content"), - "a sibling-variant mismatch is a skip, not an overwrite warning; stderr={stderr}" + !event_codes(&v).iter().any(|c| c == OVERWRITTEN), + "a sibling-variant mismatch is a skip, not an overwrite warning: {v:#}" ); } @@ -391,15 +398,16 @@ fn multi_variant_none_matching_still_fails_closed() { assert_ne!(git_sha256(&local), git_sha256(LINUX_PRISTINE)); assert_ne!(git_sha256(&local), git_sha256(DARWIN_BEFORE)); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); + let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); assert_eq!( code, 1, - "a multi-variant base with no matching variant must keep failing; stderr={stderr}" - ); - assert!( - stderr.contains("no matching variant found"), - "the failure must stay the no-matching-variant error; stderr={stderr}" + "a multi-variant base with no matching variant must keep failing; stdout={stdout}" ); + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); + assert_eq!(v["status"], "partialFailure", "{v:#}"); + assert!(event_purls(&v, "applied").is_empty(), "{v:#}"); + assert!(!event_codes(&v).iter().any(|c| c == OVERWRITTEN), "{v:#}"); let after = std::fs::read(&file).expect("read file"); assert_eq!( after, local, From a64116466a22b0e1a6bf40b3d50124850d0266e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 19:12:48 +0000 Subject: [PATCH 3/3] Share gem variant fixtures, tighten asserts Move the gem variant fixtures into apply_fixture.rs, the shared fixture module for the apply suite, instead of having apply_render.rs import a sibling test file. They now reuse its gem installer and manifest writers. The JSON tests also check that a no-matching-variant miss reports no failed event, that the --strict refusal names the mismatched file, and the --force envelope. The render test checks the purl on the warning line itself, and the two no-matching-variant cases are now separate tests. Refs #1089 Assisted-by: Claude Code:claude-opus-5-5 --- .../tests/apply/apply_fixture.rs | 142 +++++++++- .../tests/apply/apply_render.rs | 146 +++++----- .../apply/cli_gem_variant_mismatch_policy.rs | 268 ++++-------------- 3 files changed, 265 insertions(+), 291 deletions(-) diff --git a/crates/socket-patch-cli/tests/apply/apply_fixture.rs b/crates/socket-patch-cli/tests/apply/apply_fixture.rs index 27719dac2..68c09fb76 100644 --- a/crates/socket-patch-cli/tests/apply/apply_fixture.rs +++ b/crates/socket-patch-cli/tests/apply/apply_fixture.rs @@ -9,7 +9,8 @@ use std::path::{Path, PathBuf}; use serde_json::{json, Value}; -use crate::common::{git_sha256, run_with_env}; +use crate::common::hermetic::{self, Extra}; +use crate::common::{binary, git_sha256, hermetic_command, run_with_env}; pub const GO_PURL: &str = "pkg:golang/example.com/mod@v1.0.0"; pub const GO_COPY_DIR: &str = ".socket/go-patches/example.com/mod@v1.0.0"; @@ -93,8 +94,10 @@ pub fn write_root_package_json(root: &Path) { } /// Synthesize an installed gem under the cwd's vendor/bundle tree (the -/// layout the ruby crawler scans in local mode — same shape as -/// `cli_gem_variant_mismatch_policy.rs`). Returns the patchable file path. +/// layout the ruby crawler scans in local mode; a real `gem install` +/// produces exactly this `gems/-[-]` leaf, +/// verified against RubyGems on ruby 3.4). Returns the patchable file +/// path. pub fn install_gem(root: &Path, leaf: &str, file_rel: &str, contents: &[u8]) -> PathBuf { let gem_dir = root .join("vendor") @@ -225,3 +228,136 @@ pub fn write_sourceless_npm_patches(root: &Path, n: u32) { .collect(); write_manifest(root, Value::Object(patches)); } + +// ═══════════ gem release-variant mismatch fixtures ═══════════ +// +// Shared by `cli_gem_variant_mismatch_policy.rs` (the `--json` +// contracts) and `apply_render.rs` (the human output). + +/// The rack singleton's on-disk bytes after a local edit: they match +/// neither the record's beforeHash nor its afterHash. +pub const GEM_LOCAL: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n# local tweak\n"; +pub const NOKOGIRI_LINUX_PRISTINE: &[u8] = b"module Nokogiri\n VERSION = '1.16.5'\nend\n"; +pub const NOKOGIRI_DARWIN_BEFORE: &[u8] = b"# nokogiri.rb from the arm64-darwin gem\n"; +pub const NOKOGIRI_LINUX_MARKER: &[u8] = b"\n# SOCKET-LINUX-PATCH\n"; +pub const NOKOGIRI_DARWIN_MARKER: &[u8] = b"\n# DARWIN-MARKER\n"; + +/// Run `socket-patch apply --offline --ecosystems gem ` in `cwd` +/// with the hermetic `SOCKET_*` environment (`hermetic_command`) and the +/// ambient `BUNDLE_*` scrubbed, so only the argv decides behavior. +pub fn run_gem_apply(cwd: &Path, extra: &[&str]) -> (i32, String, String) { + let mut cmd = hermetic_command(&binary()); + cmd.arg("apply") + .args(["--offline", "--ecosystems", "gem"]) + .args(extra) + .current_dir(cwd); + hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); + let out = cmd.output().expect("run socket-patch apply"); + ( + out.status.code().unwrap_or(-1), + String::from_utf8_lossy(&out.stdout).to_string(), + String::from_utf8_lossy(&out.stderr).to_string(), + ) +} + +/// `pkg:gem/nokogiri@1.16.5?platform=`. +pub fn nokogiri_purl(platform: &str) -> String { + format!("pkg:gem/nokogiri@1.16.5?platform={platform}") +} + +/// Singleton fixture: one bare-PURL gem record (`GEM_BASE_PURL`) whose +/// only file was locally modified on disk (`GEM_LOCAL`); the afterHash +/// blob is staged so the offline apply has the full patched bytes. +/// Returns the on-disk file path and the exact patched bytes a correct +/// warn-overwrite must produce. +pub fn gem_singleton_mismatch_fixture(root: &Path) -> (PathBuf, Vec) { + let patched = [GEM_PRISTINE, b"\n# SOCKET-SINGLETON-PATCH\n"].concat(); + let file = install_gem(root, "rack-3.1.0", "lib/rack.rb", GEM_LOCAL); + write_manifest( + root, + json!({ + GEM_BASE_PURL: patch_record( + "31313131-3131-4131-8131-313131313131", + one_file("lib/rack.rb", GEM_PRISTINE, &patched), + ) + }), + ); + stage_blob(root, &git_sha256(&patched), &patched); + // Fixture sanity: the on-disk bytes must match neither hash, or the + // mismatch path under test is never taken. + assert_ne!(git_sha256(GEM_LOCAL), git_sha256(GEM_PRISTINE)); + assert_ne!(git_sha256(GEM_LOCAL), git_sha256(&patched)); + (file, patched) +} + +/// The x86_64-linux nokogiri record (`lib/nokogiri.rb`, pristine → +/// pristine + `NOKOGIRI_LINUX_MARKER`), blob staged. Returns the +/// patched bytes. +fn write_nokogiri_linux_record( + root: &Path, + patches: &mut serde_json::Map, +) -> Vec { + let patched = [NOKOGIRI_LINUX_PRISTINE, NOKOGIRI_LINUX_MARKER].concat(); + patches.insert( + nokogiri_purl("x86_64-linux"), + patch_record( + "41414141-4141-4141-8141-414141414141", + one_file("lib/nokogiri.rb", NOKOGIRI_LINUX_PRISTINE, &patched), + ), + ); + stage_blob(root, &git_sha256(&patched), &patched); + patched +} + +/// Qualified-singleton fixture: a lone `?platform=x86_64-linux` record +/// with the arm64-darwin gem installed, whose bytes match neither of the +/// record's hashes. Returns the on-disk file path. +pub fn gem_wrong_platform_fixture(root: &Path) -> PathBuf { + let file = install_gem( + root, + "nokogiri-1.16.5-arm64-darwin", + "lib/nokogiri.rb", + NOKOGIRI_DARWIN_BEFORE, + ); + let mut patches = serde_json::Map::new(); + let linux_patched = write_nokogiri_linux_record(root, &mut patches); + write_manifest(root, Value::Object(patches)); + // Fixture sanity: the darwin bytes must match neither hash, or the + // wrong-platform path under test is never taken. + assert_ne!( + git_sha256(NOKOGIRI_DARWIN_BEFORE), + git_sha256(NOKOGIRI_LINUX_PRISTINE) + ); + assert_ne!( + git_sha256(NOKOGIRI_DARWIN_BEFORE), + git_sha256(&linux_patched) + ); + file +} + +/// Multi-variant fixture: the x86_64-linux gem is installed with +/// `on_disk` bytes; the manifest has its record plus an arm64-darwin +/// sibling describing a DIFFERENT distribution whose beforeHash can never +/// match. Both afterHash blobs are staged. Returns the on-disk file path +/// and the linux and darwin patched bytes. +pub fn gem_multi_variant_fixture(root: &Path, on_disk: &[u8]) -> (PathBuf, Vec, Vec) { + let file = install_gem( + root, + "nokogiri-1.16.5-x86_64-linux", + "lib/nokogiri.rb", + on_disk, + ); + let mut patches = serde_json::Map::new(); + let linux_patched = write_nokogiri_linux_record(root, &mut patches); + let darwin_patched = [NOKOGIRI_DARWIN_BEFORE, NOKOGIRI_DARWIN_MARKER].concat(); + patches.insert( + nokogiri_purl("arm64-darwin"), + patch_record( + "42424242-4242-4242-8242-424242424242", + one_file("lib/nokogiri.rb", NOKOGIRI_DARWIN_BEFORE, &darwin_patched), + ), + ); + stage_blob(root, &git_sha256(&darwin_patched), &darwin_patched); + write_manifest(root, Value::Object(patches)); + (file, linux_patched, darwin_patched) +} diff --git a/crates/socket-patch-cli/tests/apply/apply_render.rs b/crates/socket-patch-cli/tests/apply/apply_render.rs index a095f2991..84c54948d 100644 --- a/crates/socket-patch-cli/tests/apply/apply_render.rs +++ b/crates/socket-patch-cli/tests/apply/apply_render.rs @@ -403,85 +403,85 @@ mod gem_fallback_home_human { } // ═══════════ gem variant mismatch policy ═══════════ +// +// The human legs of `cli_gem_variant_mismatch_policy.rs`, which asserts +// the same cases through `--json` codes. -/// The human legs of `cli_gem_variant_mismatch_policy.rs`, which asserts -/// the same cases through `--json` codes. Run with that file's runner -/// (`BUNDLE_*` scrubbed) and fixtures. -mod gem_variant_mismatch_human { - use crate::cli_gem_variant_mismatch_policy::{ - multi_variant_fixture, qualified_wrong_platform_fixture, run_apply, singleton_fixture, - with_marker, LINUX_PRISTINE, SINGLETON_PURL, - }; +const GEM_OVERWRITE_LINE: &str = "did not match the patch's expected original content"; - const OVERWRITE_LINE: &str = "did not match the patch's expected original content"; +/// The default-policy overwrite prints the npm-family mismatch warning, +/// naming the package and what happened to the file. +#[test] +fn gem_singleton_overwrite_prints_mismatch_warning() { + let tmp = tempfile::tempdir().unwrap(); + gem_singleton_mismatch_fixture(tmp.path()); + let (code, _stdout, stderr) = run_gem_apply(tmp.path(), &[]); + assert_eq!(code, 0, "stderr={stderr}"); + let warning = stderr + .lines() + .find(|l| l.contains(GEM_OVERWRITE_LINE)) + .unwrap_or_else(|| panic!("no mismatch warning; stderr={stderr}")); + assert!(warning.contains(GEM_BASE_PURL), "stderr={stderr}"); + assert!( + warning.contains("applied the full verified patched content"), + "stderr={stderr}" + ); +} - /// The default-policy overwrite prints the npm-family mismatch - /// warning: the package, and what happened to the file. - #[test] - fn singleton_overwrite_prints_mismatch_warning() { - let tmp = tempfile::tempdir().unwrap(); - singleton_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); - assert_eq!(code, 0, "stderr={stderr}"); - assert!(stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); - assert!(stderr.contains(SINGLETON_PURL), "stderr={stderr}"); - assert!( - stderr.contains("applied the full verified patched content"), - "stderr={stderr}" - ); - } +/// `--strict` prints the per-package failure with the hash refusal, not +/// the overwrite warning. +#[test] +fn gem_singleton_strict_prints_hash_refusal() { + let tmp = tempfile::tempdir().unwrap(); + gem_singleton_mismatch_fixture(tmp.path()); + let (code, _stdout, stderr) = run_gem_apply(tmp.path(), &["--strict"]); + assert_eq!(code, 1, "stderr={stderr}"); + assert!( + stderr.contains(&format!("Failed to patch {GEM_BASE_PURL}")), + "stderr={stderr}" + ); + assert!( + stderr.contains("File hash does not match expected value"), + "stderr={stderr}" + ); + assert!(!stderr.contains(GEM_OVERWRITE_LINE), "stderr={stderr}"); +} - /// `--strict` prints the per-package failure with the hash refusal, - /// not the overwrite warning. - #[test] - fn singleton_strict_prints_hash_refusal() { - let tmp = tempfile::tempdir().unwrap(); - singleton_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &["--strict"]); - assert_eq!(code, 1, "stderr={stderr}"); - assert!( - stderr.contains(&format!("Failed to patch {SINGLETON_PURL}")), - "stderr={stderr}" - ); - assert!( - stderr.contains("File hash does not match expected value"), - "stderr={stderr}" - ); - assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); - } +/// A base with no matching variant prints the no-matching-variant error, +/// never the overwrite warning. +fn assert_no_matching_variant_error(root: &std::path::Path) { + let (code, _stdout, stderr) = run_gem_apply(root, &[]); + assert_eq!(code, 1, "stderr={stderr}"); + assert!( + stderr.contains("no matching variant found"), + "stderr={stderr}" + ); + assert!(!stderr.contains(GEM_OVERWRITE_LINE), "stderr={stderr}"); +} - /// A base with no matching variant (a wrong-platform qualified - /// singleton, or a multi-variant base whose variants all mismatch) - /// prints the no-matching-variant error, never the overwrite warning. - #[test] - fn no_matching_variant_prints_variant_error() { - let wrong_platform = tempfile::tempdir().unwrap(); - qualified_wrong_platform_fixture(wrong_platform.path()); - let none_matching = tempfile::tempdir().unwrap(); - multi_variant_fixture( - none_matching.path(), - &with_marker(LINUX_PRISTINE, b"# local tweak\n"), - ); - for root in [wrong_platform.path(), none_matching.path()] { - let (code, _stdout, stderr) = run_apply(root, &[]); - assert_eq!(code, 1, "stderr={stderr}"); - assert!( - stderr.contains("no matching variant found"), - "stderr={stderr}" - ); - assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); - } - } +#[test] +fn gem_wrong_platform_prints_no_matching_variant() { + let tmp = tempfile::tempdir().unwrap(); + gem_wrong_platform_fixture(tmp.path()); + assert_no_matching_variant_error(tmp.path()); +} - /// A skipped sibling variant prints no overwrite warning. - #[test] - fn mismatched_sibling_prints_no_overwrite_warning() { - let tmp = tempfile::tempdir().unwrap(); - multi_variant_fixture(tmp.path(), LINUX_PRISTINE); - let (code, _stdout, stderr) = run_apply(tmp.path(), &[]); - assert_eq!(code, 0, "stderr={stderr}"); - assert!(!stderr.contains(OVERWRITE_LINE), "stderr={stderr}"); - } +#[test] +fn gem_multi_variant_none_matching_prints_no_matching_variant() { + let tmp = tempfile::tempdir().unwrap(); + let local = [NOKOGIRI_LINUX_PRISTINE, b"# local tweak\n"].concat(); + gem_multi_variant_fixture(tmp.path(), &local); + assert_no_matching_variant_error(tmp.path()); +} + +/// A skipped sibling variant prints no overwrite warning. +#[test] +fn gem_mismatched_sibling_prints_no_overwrite_warning() { + let tmp = tempfile::tempdir().unwrap(); + gem_multi_variant_fixture(tmp.path(), NOKOGIRI_LINUX_PRISTINE); + let (code, _stdout, stderr) = run_gem_apply(tmp.path(), &[]); + assert_eq!(code, 0, "stderr={stderr}"); + assert!(!stderr.contains(GEM_OVERWRITE_LINE), "stderr={stderr}"); } // ═══════════ dry-run --vex ═══════════ diff --git a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs index cecb74fdc..0e6570a52 100644 --- a/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs +++ b/crates/socket-patch-cli/tests/apply/cli_gem_variant_mismatch_policy.rs @@ -28,142 +28,34 @@ //! //! Behavior is asserted through the `--json` envelope (exit code, //! `status`, event actions and `errorCode`s) and the bytes on disk. The -//! human wording of each case lives in `apply_render.rs` (#1089), which -//! reuses this file's fixtures. In `--json` a no-matching-variant miss +//! human wording of each case lives in `apply_render.rs` (#1089); both +//! files share the fixtures in `apply_fixture.rs`. In `--json` a no-matching-variant miss //! shows as exit 1 with `status: partialFailure` and no `applied` or //! `failed` event for the base. +use crate::apply_fixture::{ + gem_multi_variant_fixture, gem_singleton_mismatch_fixture, gem_wrong_platform_fixture, + nokogiri_purl, run_gem_apply, GEM_BASE_PURL, GEM_LOCAL, NOKOGIRI_DARWIN_BEFORE, + NOKOGIRI_DARWIN_MARKER, NOKOGIRI_LINUX_PRISTINE, +}; use crate::common::envelope::{ - assert_envelope_invariants, event_codes, event_purls, has_event, parse_json_envelope, + assert_envelope_invariants, event_codes, event_purls, find_event, has_event, + parse_json_envelope, }; -use crate::common::hermetic::{self, Extra}; -use crate::common::{binary, git_sha256, hermetic_command}; - -use std::path::{Path, PathBuf}; +use crate::common::git_sha256; -pub(crate) const SINGLETON_PURL: &str = "pkg:gem/rack@3.1.0"; -const UUID_SINGLETON: &str = "31313131-3131-4131-8131-313131313131"; /// The per-file warning code of a default-policy overwrite. const OVERWRITTEN: &str = "content_mismatch_overwritten"; -const PRISTINE: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n"; -pub(crate) const LOCAL: &[u8] = b"module Rack\n VERSION = '3.1.0'\nend\n# local tweak\n"; -const PATCH_MARKER: &[u8] = b"\n# SOCKET-SINGLETON-PATCH\n"; - -const MULTI_NAME: &str = "nokogiri"; -const MULTI_VERSION: &str = "1.16.5"; -const UUID_LINUX: &str = "41414141-4141-4141-8141-414141414141"; -const UUID_DARWIN: &str = "42424242-4242-4242-8242-424242424242"; - -pub(crate) const LINUX_PRISTINE: &[u8] = b"module Nokogiri\n VERSION = '1.16.5'\nend\n"; -const LINUX_MARKER: &[u8] = b"\n# SOCKET-LINUX-PATCH\n"; -pub(crate) const DARWIN_BEFORE: &[u8] = b"# nokogiri.rb from the arm64-darwin gem\n"; -const DARWIN_MARKER: &[u8] = b"\n# DARWIN-MARKER\n"; - -pub(crate) fn with_marker(base: &[u8], marker: &[u8]) -> Vec { - let mut v = base.to_vec(); - v.extend_from_slice(marker); - v -} - -/// Run `socket-patch apply --offline --ecosystems gem ` in `cwd` -/// with the hermetic `SOCKET_*` environment (`hermetic_command`) and the -/// ambient `BUNDLE_*` scrubbed, so only the argv decides behavior. -pub(crate) fn run_apply(cwd: &Path, extra: &[&str]) -> (i32, String, String) { - let mut cmd = hermetic_command(&binary()); - cmd.arg("apply") - .args(["--offline", "--ecosystems", "gem"]) - .args(extra) - .current_dir(cwd); - hermetic::scrub_extra(&mut cmd, &[Extra::Bundle]); - let out = cmd.output().expect("run socket-patch apply"); - ( - out.status.code().unwrap_or(-1), - String::from_utf8_lossy(&out.stdout).to_string(), - String::from_utf8_lossy(&out.stderr).to_string(), - ) -} - -/// Synthesize an installed gem under the cwd's vendor/bundle tree (what -/// the ruby crawler scans in local mode; a real `gem install` produces -/// exactly this `gems/-[-]` leaf — verified -/// against RubyGems on ruby 3.4). Returns the patchable file path. -fn install_gem(cwd: &Path, leaf: &str, file_rel: &str, contents: &[u8]) -> PathBuf { - let gem_dir = cwd - .join("vendor") - .join("bundle") - .join("ruby") - .join("3.4.0") - .join("gems") - .join(leaf); - let file = gem_dir.join(file_rel); - std::fs::create_dir_all(file.parent().unwrap()).expect("create gem dir"); - std::fs::write(&file, contents).expect("write gem file"); - file -} - -fn patch_record(uuid: &str, file: &str, before_hash: &str, after_hash: &str) -> serde_json::Value { - serde_json::json!({ - "uuid": uuid, - "exportedAt": "2024-01-01T00:00:00Z", - "files": { file: { "beforeHash": before_hash, "afterHash": after_hash } }, - "vulnerabilities": {}, - "description": "gem variant mismatch fixture", - "license": "MIT", - "tier": "free" - }) -} - -fn write_socket_dir(cwd: &Path, patches: serde_json::Value, blobs: &[(&str, &[u8])]) { - let socket = cwd.join(".socket"); - let blobs_dir = socket.join("blobs"); - std::fs::create_dir_all(&blobs_dir).expect("create .socket/blobs"); - std::fs::write( - socket.join("manifest.json"), - serde_json::to_vec_pretty(&serde_json::json!({ "patches": patches })).unwrap(), - ) - .expect("write manifest"); - for (hash, content) in blobs { - std::fs::write(blobs_dir.join(hash), content).expect("write blob"); - } -} - -/// Singleton fixture: one bare-PURL gem record whose only file was -/// locally modified on disk (matches NEITHER beforeHash nor afterHash); -/// the afterHash blob is staged so the offline apply has the full -/// patched bytes available. Returns the on-disk file path and the exact -/// patched bytes a correct warn-overwrite must produce. -pub(crate) fn singleton_fixture(cwd: &Path) -> (PathBuf, Vec) { - let patched = with_marker(PRISTINE, PATCH_MARKER); - let file = install_gem(cwd, "rack-3.1.0", "lib/rack.rb", LOCAL); - write_socket_dir( - cwd, - serde_json::json!({ - SINGLETON_PURL: patch_record( - UUID_SINGLETON, - "lib/rack.rb", - &git_sha256(PRISTINE), - &git_sha256(&patched), - ) - }), - &[(&git_sha256(&patched), patched.as_slice())], - ); - // Fixture sanity: the on-disk bytes must match neither hash, or the - // mismatch path under test is never taken. - assert_ne!(git_sha256(LOCAL), git_sha256(PRISTINE)); - assert_ne!(git_sha256(LOCAL), git_sha256(&patched)); - (file, patched) -} - /// Default policy: the singleton's local modification is overwritten with /// the full verified patched content and surfaced as a warning — the same /// npm contract, not "no matching variant found" with the file untouched. #[test] fn singleton_mismatch_default_warns_and_applies() { let tmp = tempfile::tempdir().expect("tempdir"); - let (file, patched) = singleton_fixture(tmp.path()); + let (file, patched) = gem_singleton_mismatch_fixture(tmp.path()); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--json"]); assert_eq!( code, 0, "default mismatch on a singleton variant is a warning, not an error; stdout={stdout}" @@ -178,13 +70,13 @@ fn singleton_mismatch_default_warns_and_applies() { assert_eq!(v["status"], "success", "{v:#}"); assert_eq!( event_purls(&v, "applied"), - [SINGLETON_PURL], + [GEM_BASE_PURL], "singleton must be reported applied: {v:#}" ); // The overwrite rides as the npm-family per-file warning event, the // same shape apply_network pins for npm, naming the package. assert!( - has_event(&v, "skipped", OVERWRITTEN, SINGLETON_PURL), + has_event(&v, "skipped", OVERWRITTEN, GEM_BASE_PURL), "the overwrite must ride as a warning event: {v:#}" ); } @@ -194,16 +86,16 @@ fn singleton_mismatch_default_warns_and_applies() { #[test] fn singleton_mismatch_strict_refuses() { let tmp = tempfile::tempdir().expect("tempdir"); - let (file, _patched) = singleton_fixture(tmp.path()); + let (file, _patched) = gem_singleton_mismatch_fixture(tmp.path()); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--strict", "--json"]); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--strict", "--json"]); assert_eq!( code, 1, "--strict must refuse the mismatch; stdout={stdout}" ); assert_eq!( std::fs::read(&file).expect("read file"), - LOCAL, + GEM_LOCAL, "--strict must not modify the file" ); let v = parse_json_envelope(&stdout); @@ -211,9 +103,13 @@ fn singleton_mismatch_strict_refuses() { assert_eq!(v["status"], "partialFailure", "{v:#}"); // The package fails on the per-file hash refusal (`apply_failed`), // not the no-matching-variant miss, which reports no `failed` event. + let failed = find_event(&v, "failed", Some("apply_failed")); + assert_eq!(failed["purl"], GEM_BASE_PURL, "{v:#}"); assert!( - has_event(&v, "failed", "apply_failed", SINGLETON_PURL), - "--strict must fail the package on the hash mismatch: {v:#}" + failed["error"] + .as_str() + .is_some_and(|e| e.contains("lib/rack.rb")), + "the refusal must name the mismatched file: {v:#}" ); assert!( !event_codes(&v).iter().any(|c| c == OVERWRITTEN), @@ -226,51 +122,23 @@ fn singleton_mismatch_strict_refuses() { #[test] fn singleton_mismatch_force_still_applies() { let tmp = tempfile::tempdir().expect("tempdir"); - let (file, patched) = singleton_fixture(tmp.path()); + let (file, patched) = gem_singleton_mismatch_fixture(tmp.path()); - let (code, _stdout, stderr) = run_apply(tmp.path(), &["--force"]); - assert_eq!(code, 0, "--force must keep applying; stderr={stderr}"); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--force", "--json"]); + assert_eq!(code, 0, "--force must keep applying; stdout={stdout}"); assert_eq!( std::fs::read(&file).expect("read patched file"), patched, "--force must overwrite with the verified patched bytes" ); -} - -fn multi_purl(platform: &str) -> String { - format!("pkg:gem/{MULTI_NAME}@{MULTI_VERSION}?platform={platform}") -} - -/// Qualified-singleton fixture: a lone `?platform=x86_64-linux` record -/// with the arm64-darwin gem installed, whose bytes match neither of the -/// record's hashes. Returns the on-disk file path. -pub(crate) fn qualified_wrong_platform_fixture(cwd: &Path) -> PathBuf { - let linux_patched = with_marker(LINUX_PRISTINE, LINUX_MARKER); - // On disk: the arm64-darwin gem, whose bytes match NEITHER of the - // linux record's hashes. - let file = install_gem( - cwd, - &format!("{MULTI_NAME}-{MULTI_VERSION}-arm64-darwin"), - &format!("lib/{MULTI_NAME}.rb"), - DARWIN_BEFORE, - ); - write_socket_dir( - cwd, - serde_json::json!({ - multi_purl("x86_64-linux"): patch_record( - UUID_LINUX, - "lib/nokogiri.rb", - &git_sha256(LINUX_PRISTINE), - &git_sha256(&linux_patched), - ), - }), - &[(&git_sha256(&linux_patched), linux_patched.as_slice())], + let v = parse_json_envelope(&stdout); + assert_envelope_invariants(&v, "apply"); + assert_eq!(v["status"], "success", "{v:#}"); + assert_eq!(event_purls(&v, "applied"), [GEM_BASE_PURL], "{v:#}"); + assert!( + has_event(&v, "skipped", OVERWRITTEN, GEM_BASE_PURL), + "{v:#}" ); - // Fixture sanity: the darwin bytes must match neither hash, or the - // wrong-platform path under test is never taken. - assert_ne!(git_sha256(DARWIN_BEFORE), git_sha256(LINUX_PRISTINE)); - assert_ne!(git_sha256(DARWIN_BEFORE), git_sha256(&linux_patched)); - file } /// A QUALIFIED singleton keeps the fail-closed gate: a lone @@ -286,9 +154,9 @@ pub(crate) fn qualified_wrong_platform_fixture(cwd: &Path) -> PathBuf { #[test] fn qualified_singleton_wrong_platform_fails_closed() { let tmp = tempfile::tempdir().expect("tempdir"); - let file = qualified_wrong_platform_fixture(tmp.path()); + let file = gem_wrong_platform_fixture(tmp.path()); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--json"]); assert_eq!( code, 1, "a qualified singleton whose distribution is not on disk must fail closed; stdout={stdout}" @@ -297,6 +165,9 @@ fn qualified_singleton_wrong_platform_fails_closed() { assert_envelope_invariants(&v, "apply"); assert_eq!(v["status"], "partialFailure", "{v:#}"); assert!(event_purls(&v, "applied").is_empty(), "{v:#}"); + // No variant was attempted, so no `failed` event either: that tells + // this miss apart from a variant that was tried and refused. + assert!(event_purls(&v, "failed").is_empty(), "{v:#}"); assert!( !event_codes(&v).iter().any(|c| c == OVERWRITTEN), "a wrong-distribution record must never be surfaced as a \ @@ -304,48 +175,11 @@ fn qualified_singleton_wrong_platform_fails_closed() { ); assert_eq!( std::fs::read(&file).expect("read file"), - DARWIN_BEFORE, + NOKOGIRI_DARWIN_BEFORE, "the installed distribution's bytes must be untouched" ); } -/// Multi-variant fixture: the x86_64-linux variant is installed (its -/// beforeHash matches the on-disk bytes unless `local_bytes` overrides -/// them); the arm64-darwin sibling describes a DIFFERENT distribution -/// whose beforeHash can never match. Both afterHash blobs are staged. -pub(crate) fn multi_variant_fixture(cwd: &Path, on_disk: &[u8]) -> (PathBuf, Vec, Vec) { - let linux_patched = with_marker(LINUX_PRISTINE, LINUX_MARKER); - let darwin_patched = with_marker(DARWIN_BEFORE, DARWIN_MARKER); - let file = install_gem( - cwd, - &format!("{MULTI_NAME}-{MULTI_VERSION}-x86_64-linux"), - &format!("lib/{MULTI_NAME}.rb"), - on_disk, - ); - write_socket_dir( - cwd, - serde_json::json!({ - multi_purl("x86_64-linux"): patch_record( - UUID_LINUX, - "lib/nokogiri.rb", - &git_sha256(LINUX_PRISTINE), - &git_sha256(&linux_patched), - ), - multi_purl("arm64-darwin"): patch_record( - UUID_DARWIN, - "lib/nokogiri.rb", - &git_sha256(DARWIN_BEFORE), - &git_sha256(&darwin_patched), - ), - }), - &[ - (&git_sha256(&linux_patched), linux_patched.as_slice()), - (&git_sha256(&darwin_patched), darwin_patched.as_slice()), - ], - ); - (file, linux_patched, darwin_patched) -} - /// Over-broadening guard: a MULTI-variant base keeps the current /// behavior. The installed variant applies; the mismatched sibling means /// "different distribution" and is skipped — its bytes must never be @@ -354,9 +188,10 @@ pub(crate) fn multi_variant_fixture(cwd: &Path, on_disk: &[u8]) -> (PathBuf, Vec #[test] fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { let tmp = tempfile::tempdir().expect("tempdir"); - let (file, linux_patched, _darwin_patched) = multi_variant_fixture(tmp.path(), LINUX_PRISTINE); + let (file, linux_patched, _darwin_patched) = + gem_multi_variant_fixture(tmp.path(), NOKOGIRI_LINUX_PRISTINE); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--json"]); assert_eq!( code, 0, "the installed variant must apply cleanly; stdout={stdout}" @@ -368,8 +203,8 @@ fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { ); assert!( !after - .windows(DARWIN_MARKER.len()) - .any(|w| w == DARWIN_MARKER), + .windows(NOKOGIRI_DARWIN_MARKER.len()) + .any(|w| w == NOKOGIRI_DARWIN_MARKER), "the sibling distribution's bytes must never be written" ); let v = parse_json_envelope(&stdout); @@ -377,7 +212,7 @@ fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { assert_eq!(v["status"], "success", "{v:#}"); assert_eq!( event_purls(&v, "applied"), - [multi_purl("x86_64-linux")], + [nokogiri_purl("x86_64-linux")], "{v:#}" ); assert!( @@ -392,13 +227,13 @@ fn multi_variant_mismatched_sibling_is_skipped_not_overwritten() { /// mismatch-policy fall-through is singleton-only. #[test] fn multi_variant_none_matching_still_fails_closed() { - let local = with_marker(LINUX_PRISTINE, b"# local tweak\n"); + let local = [NOKOGIRI_LINUX_PRISTINE, b"# local tweak\n"].concat(); let tmp = tempfile::tempdir().expect("tempdir"); - let (file, linux_patched, darwin_patched) = multi_variant_fixture(tmp.path(), &local); - assert_ne!(git_sha256(&local), git_sha256(LINUX_PRISTINE)); - assert_ne!(git_sha256(&local), git_sha256(DARWIN_BEFORE)); + let (file, linux_patched, darwin_patched) = gem_multi_variant_fixture(tmp.path(), &local); + assert_ne!(git_sha256(&local), git_sha256(NOKOGIRI_LINUX_PRISTINE)); + assert_ne!(git_sha256(&local), git_sha256(NOKOGIRI_DARWIN_BEFORE)); - let (code, stdout, _stderr) = run_apply(tmp.path(), &["--json"]); + let (code, stdout, _stderr) = run_gem_apply(tmp.path(), &["--json"]); assert_eq!( code, 1, "a multi-variant base with no matching variant must keep failing; stdout={stdout}" @@ -407,6 +242,9 @@ fn multi_variant_none_matching_still_fails_closed() { assert_envelope_invariants(&v, "apply"); assert_eq!(v["status"], "partialFailure", "{v:#}"); assert!(event_purls(&v, "applied").is_empty(), "{v:#}"); + // No variant was attempted, so no `failed` event either: that tells + // this miss apart from a variant that was tried and refused. + assert!(event_purls(&v, "failed").is_empty(), "{v:#}"); assert!(!event_codes(&v).iter().any(|c| c == OVERWRITTEN), "{v:#}"); let after = std::fs::read(&file).expect("read file"); assert_eq!(