//! H4c e2e: el gate de **colisión de fichero observada** en `hammer install` real. //! //! Reproduce el caso "logo" a nivel de fichero, sin declarar `slots`: dos paquetes escriben el //! mismo path. El primero ya está instalado (pre-seed de la DB); al instalar el segundo, el gate //! —que lee los paths que el `.swm` escribirá y los compara con lo instalado— debe ABORTAR antes //! de tocar nada. Con `--force-slots` la elección se respeta y el fichero se escribe. use std::process::Command; use base64::{engine::general_purpose::STANDARD, Engine as _}; use takana_core::ArtifactHash; #[test] fn install_aborta_ante_colision_de_fichero_y_force_slots_la_supera() { let tmp = tempfile::tempdir().unwrap(); let repo = tmp.path().join("repo"); std::fs::create_dir_all(&repo).unwrap(); let prefix = tmp.path().join("prefix"); let db = tmp.path().join("installed.json"); // Pre-seed: 'tema-claro' YA posee /share/logo.png. std::fs::write( &db, r#"{"packages":[{"name":"tema-claro","version":"1","files":["/share/logo.png"]}]}"#, ) .unwrap(); // Paquete entrante 'tema-oscuro' = un file_drop que escribe el MISMO path. let content = b"logo-oscuro"; // apply_file_drop verifica con `of_inputs` (length-prefixed), no `of_bytes` — mismamos eso. let hash = ArtifactHash::of_inputs(&[content.as_slice()]); let b64 = STANDARD.encode(content); let swm = format!( "swm_version: 1\nbase:\n distro_version: dev\n pins: {{}}\nmutations:\n\ - type: file_drop\n path: /share/logo.png\n content_hash: {}\n content_b64: {}\n", hash.as_str(), b64 ); std::fs::write(repo.join("tema-oscuro-1.swm"), swm).unwrap(); let index = r#"{"packages":[{"name":"tema-oscuro","version":"1","file":"tema-oscuro-1.swm","distro_version":"dev","deps":[]}]}"#; std::fs::write(repo.join("index.json"), index).unwrap(); let bin = env!("CARGO_BIN_EXE_hammer"); let base_args = [ "install", "tema-oscuro", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap(), "--prefix", prefix.to_str().unwrap(), ]; // 1) Sin --force-slots: colisión ⇒ aborta, y el fichero NO se escribe (aborta pre-hidratación). let out = Command::new(bin).args(base_args).output().unwrap(); assert!(!out.status.success(), "debía abortar por colisión de fichero"); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("COLISIÓN de fichero"), "stderr inesperado: {err}"); assert!( !prefix.join("share/logo.png").exists(), "no debía escribir el fichero al abortar" ); // 2) Con --force-slots: la elección se respeta y el fichero se escribe. let out = Command::new(bin) .args(base_args) .arg("--force-slots") .output() .unwrap(); let err = String::from_utf8_lossy(&out.stderr); assert!(out.status.success(), "con --force-slots debía instalar. stderr: {err}"); assert!( prefix.join("share/logo.png").exists(), "el fichero debía escribirse con --force-slots. stderr: {err}" ); } /// H4d e2e: `hammer compat ` particiona un repo contra el estado instalado. La búsqueda /// del prototipo `wawa-memo` (`filtrar`), ahora sobre el repo real y read-only. #[test] fn compat_particiona_el_repo_contra_lo_instalado() { let tmp = tempfile::tempdir().unwrap(); let repo = tmp.path().join("repo"); std::fs::create_dir_all(&repo).unwrap(); let db = tmp.path().join("installed.json"); // Instalado: 'tema-claro' posee /share/logo.png. std::fs::write( &db, r#"{"packages":[{"name":"tema-claro","version":"1","files":["/share/logo.png"]}]}"#, ) .unwrap(); // Dos paquetes en el repo: uno choca (mismo path que tema-claro), otro es limpio (otro path). for (name, path) in [("tema-oscuro", "/share/logo.png"), ("fondo-montaña", "/share/fondo.png")] { let content = format!("bytes-{name}"); let hash = ArtifactHash::of_inputs(&[content.as_bytes()]); let b64 = STANDARD.encode(content.as_bytes()); let swm = format!( "swm_version: 1\nbase:\n distro_version: dev\n pins: {{}}\nmutations:\n\ - type: file_drop\n path: {path}\n content_hash: {}\n content_b64: {}\n", hash.as_str(), b64 ); std::fs::write(repo.join(format!("{name}-1.swm")), swm).unwrap(); } let index = r#"{"packages":[ {"name":"tema-oscuro","version":"1","file":"tema-oscuro-1.swm","distro_version":"dev","deps":[]}, {"name":"fondo-montaña","version":"1","file":"fondo-montaña-1.swm","distro_version":"dev","deps":[]} ]}"#; std::fs::write(repo.join("index.json"), index).unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_hammer")) .args(["compat", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap()]) .output() .unwrap(); assert!(out.status.success()); let stdout = String::from_utf8_lossy(&out.stdout); // fondo-montaña es compatible; tema-oscuro requiere elección (colisión de fichero). assert!(stdout.contains("compatibles (1)"), "stdout: {stdout}"); assert!(stdout.contains("✓ fondo-montaña"), "stdout: {stdout}"); assert!(stdout.contains("requieren elección (1)"), "stdout: {stdout}"); assert!(stdout.contains("tema-oscuro"), "stdout: {stdout}"); } /// H4e e2e: `hammer compat` marca INCOMPATIBLE un paquete cuya dep de runtime divergiste /// (el caso wayland, DERIVADO — sin que el paquete declare `requires`). Read-only: ve el /// source_patch sin construirlo. #[test] fn compat_marca_incompatible_una_dep_divergida() { let tmp = tempfile::tempdir().unwrap(); let repo = tmp.path().join("repo"); std::fs::create_dir_all(&repo).unwrap(); let db = tmp.path().join("installed.json"); // Tengo wayland-protocol instalado a b3:mine (lo diverguí del stock del repo). std::fs::write( &db, r#"{"packages":[{"name":"wayland-protocol","version":"1","hash":"b3:mine","files":[]}]}"#, ) .unwrap(); // 'app' es un source_patch que depende (runtime) de wayland-protocol. let app_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations:\n\ - type: source_patch\n repo: git://x/app.git\n commit: abc\n target_bin: /usr/bin/app\n\ \x20 build:\n compiler: zig-cc\n deps:\n runtime:\n - wayland-protocol\n"; std::fs::write(repo.join("app-1.swm"), app_swm).unwrap(); // wayland-protocol también está en el repo, a b3:stock (la versión contra la que app se hizo). let w_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations: []\n"; std::fs::write(repo.join("w-1.swm"), w_swm).unwrap(); let index = r#"{"packages":[ {"name":"app","version":"1","file":"app-1.swm","distro_version":"dev","deps":["wayland-protocol"],"expected_hash":"b3:app"}, {"name":"wayland-protocol","version":"1","file":"w-1.swm","distro_version":"dev","deps":[],"expected_hash":"b3:stock"} ]}"#; std::fs::write(repo.join("index.json"), index).unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_hammer")) .args(["compat", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap()]) .output() .unwrap(); assert!(out.status.success()); let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.contains("incompatibles (1)"), "stdout: {stdout}"); assert!(stdout.contains("app") && stdout.contains("deps divergidas"), "stdout: {stdout}"); }