Files
takana/crates/takana-cli/tests/swm_roundtrip.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

164 lines
5.9 KiB
Rust

//! Roundtrip e2e de Fase 4: construimos un `.swm` a mano (los tipos viven en takana-core),
//! lo aplicamos a un prefix temporal, leemos el resultado y verificamos que los archivos
//! coinciden con lo esperado.
//!
//! NO ejercitamos `source_patch` porque arrastra todo el lab (red + sandbox); para esos
//! casos existen los tests gated en `TAKANA_NETWORK_TESTS`. Aquí cubrimos los caminos puros
//! de takana-core: `verify_schema`, `verify_base`, `apply_config_edit`, `apply_file_drop`.
use std::collections::BTreeMap;
use std::path::Path;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use takana_core::apply::{apply_config_edit, apply_file_drop, rebase_path, verify_content_hash};
use takana_core::swm::{Base, BaseCompat, BaseRef, Mutation, Swm};
use takana_core::ArtifactHash;
fn write(p: &Path, content: &str) {
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, content).unwrap();
}
fn base(distro: &str, pins: &[(&str, &str)]) -> Base {
Base {
distro_version: distro.into(),
pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
}
}
fn baseref(distro: &str, pins: &[(&str, &str)]) -> BaseRef {
BaseRef {
distro_version: distro.into(),
pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
}
}
fn mk_swm(mutations: Vec<Mutation>) -> Swm {
Swm {
swm_version: 1,
base: base("2026-06-06", &[("musl", "deadbeef")]),
mutations,
signature: None,
}
}
/// Verifica que un `.swm` pasa schema y base, y luego aplica cada mutación bajo un prefix.
/// Este es el camino que la CLI ejercita; aquí lo reproducimos como librería para que el
/// test se mantenga aunque la CLI cambie superficialmente.
fn apply_swm_under_prefix(swm: &Swm, prefix: &Path) {
swm.verify_schema().expect("schema válido");
// Base compatible (idéntica a la del .swm).
let local = BaseRef::from(&swm.base);
assert_eq!(swm.verify_base(&local), BaseCompat::Ok);
for m in &swm.mutations {
match m {
Mutation::ConfigEdit { file, inline_diff } => {
let target = rebase_path(file, Some(prefix));
apply_config_edit(&target, inline_diff).unwrap();
}
Mutation::FileDrop { path, content_hash, content_b64, .. } => {
let target = rebase_path(path, Some(prefix));
let b64 = content_b64.as_ref().expect("inline para tests");
apply_file_drop(&target, b64, content_hash).unwrap();
}
_ => panic!("este test sólo cubre config_edit/file_drop"),
}
}
}
#[test]
fn roundtrip_yaml_apply_reproduces_files() {
let payload = b"hola hammer\n";
let payload_hash = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
let payload_b64 = STANDARD.encode(payload);
let swm = mk_swm(vec![
Mutation::ConfigEdit {
file: "/etc/network.conf".into(),
inline_diff: "- DHCP=yes\n+ IP=192.168.1.100\n".into(),
},
Mutation::FileDrop {
path: "/var/lib/hammer/saludo.txt".into(),
content_hash: payload_hash.clone(),
content_b64: Some(payload_b64),
content_url: None,
},
]);
// Roundtrip YAML completo: serializa, deserializa y reusa.
let yaml = swm.to_yaml().unwrap();
let swm2 = Swm::from_yaml(&yaml).unwrap();
assert_eq!(swm2.mutations.len(), 2);
let prefix_dir = tempfile::tempdir().unwrap();
let prefix = prefix_dir.path();
// El archivo objetivo del config_edit debe existir antes (config_edit no crea archivos).
write(
&prefix.join("etc/network.conf"),
"USE=eth0\nDHCP=yes\nMTU=1500\n",
);
apply_swm_under_prefix(&swm2, prefix);
let got_net = std::fs::read_to_string(prefix.join("etc/network.conf")).unwrap();
assert_eq!(got_net, "USE=eth0\nIP=192.168.1.100\nMTU=1500\n");
let got_drop = std::fs::read(prefix.join("var/lib/hammer/saludo.txt")).unwrap();
assert_eq!(got_drop, payload);
}
#[test]
fn verify_base_distinguishes_compatible_and_not() {
let swm = mk_swm(vec![Mutation::ConfigEdit {
file: "/etc/x".into(),
inline_diff: "- a\n+ b\n".into(),
}]);
// distro distinta
let other = baseref("2026-09-09", &[("musl", "deadbeef")]);
assert!(matches!(
swm.verify_base(&other),
BaseCompat::DistroMismatch { .. }
));
// pin distinta
let bad_pin = baseref("2026-06-06", &[("musl", "ZZZZ")]);
assert!(matches!(swm.verify_base(&bad_pin), BaseCompat::PinMismatch(_)));
// pins extras locales no son problema
let mut more = BTreeMap::new();
more.insert("musl".into(), "deadbeef".into());
more.insert("kernel".into(), "abcdef".into());
let ok = BaseRef { distro_version: "2026-06-06".into(), pins: more };
assert_eq!(swm.verify_base(&ok), BaseCompat::Ok);
}
#[test]
fn verify_content_hash_catches_payload_tampering() {
let payload = b"datos integros\n";
let b64 = STANDARD.encode(payload);
let h = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
verify_content_hash(&b64, &h).expect("hash ok");
// Mutar un byte del payload sin actualizar el hash ⇒ falla.
let mut bad = payload.to_vec();
bad[0] ^= 0x01;
let bad_b64 = STANDARD.encode(&bad);
let err = verify_content_hash(&bad_b64, &h).unwrap_err().to_string();
assert!(err.contains("no coincide"), "{err}");
}
#[test]
fn yaml_includes_only_inline_field_when_url_is_none() {
// Un .swm autocontenido (lo que produce export) no debe acabar emitiendo `content_url`.
let swm = mk_swm(vec![Mutation::FileDrop {
path: "/var/lib/x".into(),
content_hash: "b3:00".into(),
content_b64: Some("AA==".into()),
content_url: None,
}]);
let y = swm.to_yaml().unwrap();
assert!(y.contains("content_b64"));
assert!(!y.contains("content_url"), "skip_serializing_if=None debe omitir el campo");
}