Files
takana/crates/hammer-cli/tests/compat_gate.rs
T
sergioandClaude Opus 4.8 9be7bdd707 H4c: superficies OBSERVADAS — colisión de fichero en install sin declarar slots (SDD 15 §H4)
La misma subida de H1 (prometer -> verificar), ahora sobre topología: la superficie más
común de un paquete es el conjunto de paths que escribe, y esos paths ya están declarados
en el .swm (target_bin + file_drop.path), conocidos ANTES de hidratar.

- compat::output_paths(swm) lee esos paths; compat::path_collisions(db, name, paths) detecta
  cuáles ya posee OTRO paquete instalado (reusa InstalledDb.files + owner_of, cero declaración
  nueva). Reinstalar el mismo paquete sobre sus propios paths NO colisiona (upgrade).
- Gate en `install` corre el chequeo observado JUNTO al declarado (H4b): pisar el fichero de
  otro paquete = caso logo a nivel de fichero (elección) -> aborta salvo --force-slots.
- Verificado e2e REAL (tests/compat_gate.rs, shell-ea al binario hammer): dos paquetes
  escriben /share/logo.png; el 2do aborta con "COLISIÓN de fichero" sin escribir nada; con
  --force-slots la elección se respeta y el fichero se escribe.

Un paquete SIN declarar slots ya participa del gate por lo que de verdad toca.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:17:17 -04:00

78 lines
3.1 KiB
Rust

//! 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 hammer_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}"
);
}