use clap::Args;
use socket_patch_core::api::blob_fetcher::{
fetch_missing_blobs, fetch_missing_sources, format_fetch_result, get_missing_archives,
get_missing_blobs, DownloadMode,
};
use socket_patch_core::api::client::get_api_client_with_overrides;
use socket_patch_core::crawlers::{
detect_npm_pkg_manager, CrawlerOptions, Ecosystem, NpmPkgManager,
};
use socket_patch_core::manifest::operations::read_manifest;
use socket_patch_core::patch::apply::{
apply_package_patch, verify_file_patch, ApplyResult, PatchSources, VerifyStatus,
};
use crate::commands::lock_cli::{acquire_or_emit, lock_broken_event};
use socket_patch_core::utils::purl::strip_purl_qualifiers;
use socket_patch_core::utils::telemetry::{track_patch_applied, track_patch_apply_failed};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use tempfile::TempDir;
use crate::args::{apply_env_toggles, GlobalArgs};
use crate::json_envelope::{
AppliedVia, Command, Envelope, EnvelopeError, PatchAction, PatchEvent, PatchEventFile, Status,
};
async fn overlay_dir(src: &Path, dst: &Path) {
let mut entries = match tokio::fs::read_dir(src).await {
Ok(e) => e,
Err(_) => return,
};
while let Ok(Some(entry)) = entries.next_entry().await {
let file_type = match entry.file_type().await {
Ok(t) => t,
Err(_) => continue,
};
if !file_type.is_file() {
continue;
}
let from = entry.path();
let to = dst.join(entry.file_name());
if tokio::fs::metadata(&to).await.is_ok() {
continue;
}
if tokio::fs::hard_link(&from, &to).await.is_err() {
let _ = tokio::fs::copy(&from, &to).await;
}
}
}
use crate::ecosystem_dispatch::{find_packages_for_purls, partition_purls};
#[derive(Args)]
pub struct ApplyArgs {
#[command(flatten)]
pub common: GlobalArgs,
#[arg(short = 'f', long, env = "SOCKET_FORCE", default_value_t = false)]
pub force: bool,
}
fn all_files_already_patched(result: &ApplyResult) -> bool {
!result.files_verified.is_empty()
&& result
.files_verified
.iter()
.all(|f| f.status == VerifyStatus::AlreadyPatched)
}
fn variant_matches_installed(first_file_status: Option<&VerifyStatus>) -> bool {
match first_file_status {
None => true,
Some(status) => {
*status == VerifyStatus::Ready || *status == VerifyStatus::AlreadyPatched
}
}
}
pub(crate) fn result_to_event(result: &ApplyResult, dry_run: bool) -> PatchEvent {
let purl = result.package_key.clone();
if !result.success {
return PatchEvent::new(PatchAction::Failed, purl).with_error(
"apply_failed",
result
.error
.clone()
.unwrap_or_else(|| "unknown error".to_string()),
);
}
if all_files_already_patched(result) {
return PatchEvent::new(PatchAction::Skipped, purl)
.with_reason("already_patched", "All files already match afterHash");
}
if dry_run {
let files = result
.files_verified
.iter()
.filter(|f| {
f.status == VerifyStatus::Ready || f.status == VerifyStatus::AlreadyPatched
})
.map(|f| PatchEventFile {
path: f.file.clone(),
verified: true,
applied_via: None,
})
.collect();
return PatchEvent::new(PatchAction::Verified, purl).with_files(files);
}
let files = result
.files_patched
.iter()
.map(|f| PatchEventFile {
path: f.clone(),
verified: true,
applied_via: result
.applied_via
.get(f)
.copied()
.map(AppliedVia::from_core),
})
.collect();
PatchEvent::new(PatchAction::Applied, purl).with_files(files)
}
pub async fn run(args: ApplyArgs) -> i32 {
apply_env_toggles(&args.common);
let (telemetry_client, _) =
get_api_client_with_overrides(args.common.api_client_overrides()).await;
let api_token = telemetry_client.api_token().cloned();
let org_slug = telemetry_client.org_slug().cloned();
let manifest_path = args.common.resolved_manifest_path();
if tokio::fs::metadata(&manifest_path).await.is_err() {
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.status = Status::NoManifest;
env.dry_run = args.common.dry_run;
println!("{}", env.to_pretty_json());
} else if !args.common.silent {
println!("No .socket folder found, skipping patch application.");
}
return 0;
}
let socket_dir = manifest_path.parent().unwrap_or(Path::new("."));
let acquired = match acquire_or_emit(
socket_dir,
Command::Apply,
args.common.json,
args.common.silent,
args.common.dry_run,
Duration::from_secs(args.common.lock_timeout.unwrap_or(0)),
args.common.break_lock,
) {
Ok(acquired) => acquired,
Err(code) => return code,
};
let _lock = acquired.guard;
let lock_was_broken = acquired.broke_lock;
let pkg_manager = detect_npm_pkg_manager(&args.common.cwd);
match pkg_manager {
NpmPkgManager::YarnBerryPnP => {
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.dry_run = args.common.dry_run;
env.mark_error(EnvelopeError::new(
"yarn_pnp_unsupported",
"yarn-berry Plug'n'Play layout is not supported by socket-patch (packages live inside .yarn/cache zips). Use `yarn patch <pkg>` instead.",
));
println!("{}", env.to_pretty_json());
} else if !args.common.silent {
eprintln!("Error: yarn-berry Plug'n'Play layout is not supported.");
eprintln!(
" Packages live inside .yarn/cache/*.zip — socket-patch cannot rewrite them in place."
);
eprintln!(" Use `yarn patch <pkg>` instead.");
}
return 1;
}
NpmPkgManager::Pnpm => {
if !args.common.json && !args.common.silent {
eprintln!(
"Note: pnpm layout detected. Copy-on-write will keep the global store untouched."
);
}
}
NpmPkgManager::Bun => {
if !args.common.json && !args.common.silent {
eprintln!(
"Note: bun layout detected. Copy-on-write will keep ~/.bun/install/cache/ untouched."
);
}
}
_ => {}
}
match apply_patches_inner(&args, &manifest_path).await {
Ok((success, results, unmatched)) => {
let patched_count = results
.iter()
.filter(|r| r.success && !r.files_patched.is_empty())
.count();
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.dry_run = args.common.dry_run;
if lock_was_broken {
env.record(lock_broken_event(socket_dir));
}
for result in &results {
env.record(result_to_event(result, args.common.dry_run));
if let Some(ref sidecar) = result.sidecar {
env.record_sidecar(sidecar.clone());
}
}
for purl in &unmatched {
env.record(
PatchEvent::new(PatchAction::Skipped, purl.clone()).with_reason(
"package_not_installed",
"No installed package matches this PURL",
),
);
}
if !success {
env.mark_partial_failure();
}
println!("{}", env.to_pretty_json());
} else if !args.common.silent && !results.is_empty() {
let patched: Vec<_> = results.iter().filter(|r| r.success).collect();
let already_patched: Vec<_> = results
.iter()
.filter(|r| all_files_already_patched(r))
.collect();
if args.common.dry_run {
let can_be_patched = patched.len().saturating_sub(already_patched.len());
println!("\nPatch verification complete:");
println!(" {} package(s) can be patched", can_be_patched);
if !already_patched.is_empty() {
println!(" {} package(s) already patched", already_patched.len());
}
} else {
println!("\nPatched packages:");
for result in &patched {
if !result.files_patched.is_empty() {
let mut tags: Vec<&'static str> = result
.applied_via
.values()
.map(|v| v.as_tag())
.collect();
tags.sort_unstable();
tags.dedup();
let suffix = if tags.is_empty() {
String::new()
} else {
format!(" (via {})", tags.join("+"))
};
println!(" {}{}", result.package_key, suffix);
} else if all_files_already_patched(result) {
println!(" {} (already patched)", result.package_key);
}
}
}
if args.common.verbose {
println!("\nDetailed verification:");
for result in &results {
println!(" {}:", result.package_key);
for f in &result.files_verified {
let status_str = match f.status {
VerifyStatus::Ready => "ready",
VerifyStatus::AlreadyPatched => "already patched",
VerifyStatus::HashMismatch => "hash mismatch",
VerifyStatus::NotFound => "not found",
};
println!(" {} [{}]", f.file, status_str);
if let Some(ref msg) = f.message {
println!(" message: {msg}");
}
if args.common.verbose {
if let Some(ref h) = f.current_hash {
println!(" current: {h}");
}
if let Some(ref h) = f.expected_hash {
println!(" expected: {h}");
}
if let Some(ref h) = f.target_hash {
println!(" target: {h}");
}
}
}
}
}
}
if success {
track_patch_applied(patched_count, args.common.dry_run, api_token.as_deref(), org_slug.as_deref()).await;
} else {
track_patch_apply_failed("One or more patches failed to apply", args.common.dry_run, api_token.as_deref(), org_slug.as_deref()).await;
}
if success { 0 } else { 1 }
}
Err(e) => {
track_patch_apply_failed(&e, args.common.dry_run, api_token.as_deref(), org_slug.as_deref()).await;
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.dry_run = args.common.dry_run;
env.mark_error(EnvelopeError::new("apply_failed", e.clone()));
println!("{}", env.to_pretty_json());
} else if !args.common.silent {
eprintln!("Error: {e}");
}
1
}
}
}
async fn apply_patches_inner(
args: &ApplyArgs,
manifest_path: &Path,
) -> Result<(bool, Vec<ApplyResult>, Vec<String>), String> {
let manifest = read_manifest(manifest_path)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Invalid manifest".to_string())?;
let socket_dir = manifest_path.parent().unwrap();
let socket_blobs_path = socket_dir.join("blobs");
let socket_diffs_path = socket_dir.join("diffs");
let socket_packages_path = socket_dir.join("packages");
let download_mode = DownloadMode::parse(&args.common.download_mode).map_err(|e| e.to_string())?;
let missing_blobs = get_missing_blobs(&manifest, &socket_blobs_path).await;
let missing_diff_archives = get_missing_archives(&manifest, &socket_diffs_path).await;
let missing_package_archives = get_missing_archives(&manifest, &socket_packages_path).await;
let patches_without_source: Vec<&str> = manifest
.patches
.iter()
.filter_map(|(purl, record)| {
let all_blobs_present = record
.files
.values()
.all(|f| !missing_blobs.contains(&f.after_hash));
let diff_present = !missing_diff_archives.contains(&record.uuid);
let pkg_present = !missing_package_archives.contains(&record.uuid);
if all_blobs_present || diff_present || pkg_present {
None
} else {
Some(purl.as_str())
}
})
.collect();
if args.common.offline {
if !patches_without_source.is_empty() {
if !args.common.silent && !args.common.json {
eprintln!(
"Error: {} patch(es) have no local source and --offline is set:",
patches_without_source.len()
);
for purl in patches_without_source.iter().take(5) {
eprintln!(" - {}", purl);
}
if patches_without_source.len() > 5 {
eprintln!(" ... and {} more", patches_without_source.len() - 5);
}
eprintln!("Run \"socket-patch repair\" to download missing artifacts.");
}
return Ok((false, Vec::new(), Vec::new()));
}
}
let download_needed = !args.common.offline
&& match download_mode {
DownloadMode::File => !missing_blobs.is_empty(),
DownloadMode::Diff | DownloadMode::Package if missing_blobs.is_empty() => false,
DownloadMode::Diff => !missing_diff_archives.is_empty(),
DownloadMode::Package => !missing_package_archives.is_empty(),
};
let (blobs_path, diffs_path, packages_path, _stage_dir): (
PathBuf,
PathBuf,
PathBuf,
Option<TempDir>,
) = if download_needed {
let stage = tempfile::tempdir().map_err(|e| e.to_string())?;
let stage_blobs = stage.path().join("blobs");
let stage_diffs = stage.path().join("diffs");
let stage_packages = stage.path().join("packages");
for dir in [&stage_blobs, &stage_diffs, &stage_packages] {
tokio::fs::create_dir_all(dir)
.await
.map_err(|e| e.to_string())?;
}
overlay_dir(&socket_blobs_path, &stage_blobs).await;
overlay_dir(&socket_diffs_path, &stage_diffs).await;
overlay_dir(&socket_packages_path, &stage_packages).await;
if !args.common.silent && !args.common.json {
println!(
"Downloading missing patch artifacts (mode: {})...",
download_mode.as_tag()
);
}
let (client, _) =
get_api_client_with_overrides(args.common.api_client_overrides()).await;
let sources = PatchSources {
blobs_path: &stage_blobs,
packages_path: Some(&stage_packages),
diffs_path: Some(&stage_diffs),
};
let fetch_result =
fetch_missing_sources(&manifest, &sources, download_mode, &client, None).await;
if !args.common.silent && !args.common.json {
println!("{}", format_fetch_result(&fetch_result));
}
if download_mode != DownloadMode::File {
let still_missing_blobs = get_missing_blobs(&manifest, &stage_blobs).await;
if !still_missing_blobs.is_empty() {
if !args.common.silent && !args.common.json {
println!(
"Falling back to per-file blob downloads for {} blob(s)...",
still_missing_blobs.len()
);
}
let blob_result =
fetch_missing_blobs(&manifest, &stage_blobs, &client, None).await;
if !args.common.silent && !args.common.json {
println!("{}", format_fetch_result(&blob_result));
}
if blob_result.failed > 0 && fetch_result.failed > 0 {
if !args.common.silent && !args.common.json {
eprintln!("Some artifacts could not be downloaded. Cannot apply patches.");
}
return Ok((false, Vec::new(), Vec::new()));
}
}
} else if fetch_result.failed > 0 {
if !args.common.silent && !args.common.json {
eprintln!("Some blobs could not be downloaded. Cannot apply patches.");
}
return Ok((false, Vec::new(), Vec::new()));
}
(stage_blobs, stage_diffs, stage_packages, Some(stage))
} else {
(
socket_blobs_path.clone(),
socket_diffs_path.clone(),
socket_packages_path.clone(),
None,
)
};
let manifest_purls: Vec<String> = manifest.patches.keys().cloned().collect();
let partitioned =
partition_purls(&manifest_purls, args.common.ecosystems.as_deref());
let target_manifest_purls: HashSet<String> = partitioned
.values()
.flat_map(|purls| purls.iter().cloned())
.collect();
let crawler_options = CrawlerOptions {
cwd: args.common.cwd.clone(),
global: args.common.global,
global_prefix: args.common.global_prefix.clone(),
batch_size: 100,
};
let all_packages =
find_packages_for_purls(&partitioned, &crawler_options, args.common.silent || args.common.json).await;
let has_any_purls = !partitioned.is_empty();
if all_packages.is_empty() && !has_any_purls {
if !args.common.silent && !args.common.json {
if args.common.global || args.common.global_prefix.is_some() {
eprintln!("No global packages found");
} else {
eprintln!("No package directories found");
}
}
return Ok((false, Vec::new(), Vec::new()));
}
if all_packages.is_empty() {
if !args.common.silent && !args.common.json {
eprintln!("Warning: No packages found that match available patches");
eprintln!(
" {} targeted manifest patch(es) were in scope, but no matching packages were found on disk.",
target_manifest_purls.len()
);
eprintln!(" Check that packages are installed and --cwd points to the right directory.");
}
let unmatched: Vec<String> = target_manifest_purls.iter().cloned().collect();
return Ok((false, Vec::new(), unmatched));
}
let mut results: Vec<ApplyResult> = Vec::new();
let mut has_errors = false;
let mut variant_qualified_groups: HashMap<String, Vec<String>> = HashMap::new();
for (eco, purls) in &partitioned {
if eco.supports_release_variants() {
for purl in purls {
variant_qualified_groups
.entry(strip_purl_qualifiers(purl).to_string())
.or_default()
.push(purl.clone());
}
}
}
let mut applied_base_purls: HashSet<String> = HashSet::new();
let mut matched_manifest_purls: HashSet<String> = HashSet::new();
for (purl, pkg_path) in &all_packages {
if Ecosystem::from_purl(purl).is_some_and(|e| e.supports_release_variants()) {
let base_purl = strip_purl_qualifiers(purl).to_string();
if applied_base_purls.contains(&base_purl) {
continue;
}
let variants = variant_qualified_groups
.get(&base_purl)
.cloned()
.unwrap_or_else(|| vec![base_purl.clone()]);
let mut applied = false;
for variant_purl in &variants {
let patch = match manifest.patches.get(variant_purl) {
Some(p) => p,
None => continue,
};
if !args.force {
let first_status = match patch.files.iter().next() {
Some((file_name, file_info)) => {
Some(verify_file_patch(pkg_path, file_name, file_info).await.status)
}
None => None,
};
if !variant_matches_installed(first_status.as_ref()) {
continue;
}
}
let sources = PatchSources {
blobs_path: &blobs_path,
packages_path: Some(&packages_path),
diffs_path: Some(&diffs_path),
};
let result = apply_package_patch(
variant_purl,
pkg_path,
&patch.files,
&sources,
Some(&patch.uuid),
args.common.dry_run,
args.force,
)
.await;
if result.success {
applied = true;
results.push(result);
matched_manifest_purls.insert(variant_purl.clone());
} else {
results.push(result);
}
}
if applied {
applied_base_purls.insert(base_purl.clone());
} else {
has_errors = true;
if !args.common.silent && !args.common.json {
eprintln!("Failed to patch {base_purl}: no matching variant found");
}
}
} else {
let patch = match manifest.patches.get(purl) {
Some(p) => p,
None => continue,
};
let sources = PatchSources {
blobs_path: &blobs_path,
packages_path: Some(&packages_path),
diffs_path: Some(&diffs_path),
};
let result = apply_package_patch(
purl,
pkg_path,
&patch.files,
&sources,
Some(&patch.uuid),
args.common.dry_run,
args.force,
)
.await;
if !result.success {
has_errors = true;
if !args.common.silent && !args.common.json {
eprintln!(
"Failed to patch {}: {}",
purl,
result.error.as_deref().unwrap_or("unknown error")
);
}
}
results.push(result);
matched_manifest_purls.insert(purl.clone());
}
}
let unmatched: Vec<String> = target_manifest_purls
.iter()
.filter(|p| !matched_manifest_purls.contains(*p))
.cloned()
.collect();
if !unmatched.is_empty() && !args.common.silent && !args.common.json {
eprintln!("\nWarning: {} manifest patch(es) had no matching installed package:", unmatched.len());
for purl in &unmatched {
eprintln!(" - {}", purl);
}
}
if !target_manifest_purls.is_empty() && matched_manifest_purls.is_empty() && !all_packages.is_empty() {
if !args.common.silent && !args.common.json {
eprintln!("Warning: None of the targeted manifest patches matched installed packages.");
}
has_errors = true;
}
if !args.common.silent && !args.common.json {
let applied_count = results.iter().filter(|r| r.success && !r.files_patched.is_empty()).count();
let already_count = results.iter().filter(|r| all_files_already_patched(r)).count();
println!(
"\nSummary: {}/{} targeted patches applied, {} already patched, {} not found on disk",
applied_count,
target_manifest_purls.len(),
already_count,
unmatched.len()
);
}
Ok((!has_errors, results, unmatched))
}
#[cfg(test)]
mod tests {
use super::*;
use socket_patch_core::patch::apply::{
AppliedVia as CoreAppliedVia, ApplyResult, VerifyResult, VerifyStatus,
};
fn sample_applied(status: VerifyStatus) -> ApplyResult {
let mut applied_via = HashMap::new();
applied_via.insert("package/index.js".to_string(), CoreAppliedVia::Diff);
ApplyResult {
package_key: "pkg:npm/minimist@1.2.2".to_string(),
package_path: "/tmp/node_modules/minimist".to_string(),
success: true,
files_verified: vec![VerifyResult {
file: "package/index.js".to_string(),
status,
message: None,
current_hash: None,
expected_hash: None,
target_hash: None,
}],
files_patched: vec!["package/index.js".to_string()],
applied_via,
error: None,
sidecar: None,
}
}
#[test]
fn failed_result_maps_to_failed_action() {
let mut result = sample_applied(VerifyStatus::Ready);
result.success = false;
result.error = Some("hash mismatch".into());
let event = result_to_event(&result, false);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(v["action"], "failed");
assert_eq!(v["errorCode"], "apply_failed");
assert_eq!(v["error"], "hash mismatch");
}
#[test]
fn all_already_patched_maps_to_skipped() {
let result = sample_applied(VerifyStatus::AlreadyPatched);
let event = result_to_event(&result, false);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(v["action"], "skipped");
assert_eq!(v["errorCode"], "already_patched");
}
#[test]
fn dry_run_maps_to_verified() {
let result = sample_applied(VerifyStatus::Ready);
let event = result_to_event(&result, true);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(v["action"], "verified");
assert_eq!(v["files"][0]["path"], "package/index.js");
assert!(v["files"][0].as_object().unwrap().get("appliedVia").is_none());
}
#[test]
fn successful_apply_maps_to_applied_with_files() {
let result = sample_applied(VerifyStatus::Ready);
let event = result_to_event(&result, false);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(v["action"], "applied");
assert_eq!(v["purl"], "pkg:npm/minimist@1.2.2");
let files = v["files"].as_array().unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0]["path"], "package/index.js");
assert_eq!(files[0]["verified"], true);
assert_eq!(files[0]["appliedVia"], "diff");
}
#[test]
fn applied_event_emits_one_file_entry_per_patched_file() {
let mut applied_via = HashMap::new();
applied_via.insert("package/a.js".to_string(), CoreAppliedVia::Diff);
applied_via.insert("package/b.js".to_string(), CoreAppliedVia::Package);
applied_via.insert("package/c.js".to_string(), CoreAppliedVia::Blob);
let result = ApplyResult {
package_key: "pkg:npm/foo@1.0.0".to_string(),
package_path: "/tmp/foo".to_string(),
success: true,
files_verified: Vec::new(),
files_patched: vec![
"package/a.js".to_string(),
"package/b.js".to_string(),
"package/c.js".to_string(),
],
applied_via,
error: None,
sidecar: None,
};
let event = result_to_event(&result, false);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
let files = v["files"].as_array().unwrap();
assert_eq!(files.len(), 3);
let by_path: std::collections::HashMap<String, &serde_json::Value> = files
.iter()
.map(|f| (f["path"].as_str().unwrap().to_string(), f))
.collect();
assert_eq!(by_path["package/a.js"]["appliedVia"], "diff");
assert_eq!(by_path["package/b.js"]["appliedVia"], "package");
assert_eq!(by_path["package/c.js"]["appliedVia"], "blob");
}
fn sample_verified(statuses: &[VerifyStatus]) -> ApplyResult {
let files_verified = statuses
.iter()
.enumerate()
.map(|(i, status)| VerifyResult {
file: format!("package/f{i}.js"),
status: status.clone(),
message: None,
current_hash: None,
expected_hash: None,
target_hash: None,
})
.collect();
ApplyResult {
package_key: "pkg:npm/foo@1.0.0".to_string(),
package_path: "/tmp/foo".to_string(),
success: true,
files_verified,
files_patched: Vec::new(),
applied_via: HashMap::new(),
error: None,
sidecar: None,
}
}
#[test]
fn all_files_already_patched_true_when_every_file_matches() {
let result = sample_verified(&[
VerifyStatus::AlreadyPatched,
VerifyStatus::AlreadyPatched,
]);
assert!(all_files_already_patched(&result));
}
#[test]
fn all_files_already_patched_false_when_any_file_differs() {
let result = sample_verified(&[
VerifyStatus::AlreadyPatched,
VerifyStatus::Ready,
]);
assert!(!all_files_already_patched(&result));
}
#[test]
fn all_files_already_patched_false_when_no_verified_files() {
let mut result = sample_verified(&[]);
assert!(result.files_verified.is_empty());
assert!(!all_files_already_patched(&result));
result.files_patched = vec!["package/a.js".to_string()];
assert!(!all_files_already_patched(&result));
}
#[test]
fn variant_matches_only_when_first_file_ready_or_already_patched() {
assert!(variant_matches_installed(Some(&VerifyStatus::Ready)));
assert!(variant_matches_installed(Some(&VerifyStatus::AlreadyPatched)));
assert!(!variant_matches_installed(Some(&VerifyStatus::HashMismatch)));
assert!(!variant_matches_installed(Some(&VerifyStatus::NotFound)));
assert!(variant_matches_installed(None));
}
#[test]
fn applied_with_empty_verified_is_not_skipped() {
let mut applied_via = HashMap::new();
applied_via.insert("package/a.js".to_string(), CoreAppliedVia::Blob);
let result = ApplyResult {
package_key: "pkg:npm/foo@1.0.0".to_string(),
package_path: "/tmp/foo".to_string(),
success: true,
files_verified: Vec::new(),
files_patched: vec!["package/a.js".to_string()],
applied_via,
error: None,
sidecar: None,
};
let event = result_to_event(&result, false);
let v: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(v["action"], "applied");
}
}