Files
takana/crates/takana-cli/tests/compat_gate.rs
T
Sergio b818f5249f takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).

EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.

El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:

  b"hammer-tree-v1"        <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
  b"hammer-seed-v1"           la funcion que hashea TODOS los artefactos:
  b"hammer-stage1-rootfs-v2"  cambiarla mueve los 4750 hashes del store
  b"hammer-product-rootfs-v3"
  b"hammer-product-attested-v2"
  b"hammer-builder-rootfs-v1"
  b"hammer-attest-dev-rootkey-0001!!"  <- clave raiz de atestacion, [u8;32]

Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.

Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.

La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
2026-09-09 19:38:33 +00:00

166 lines
7.5 KiB
Rust

//! H4c e2e: el gate de **colisión de fichero observada** en `takana 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: `takana compat <repo>` 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: `takana 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}");
}