gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Definitions

⎇ main crates/takana-cli/tests/swm_roundtrip.rs Rust · 163 lines · 5.9 KB History Who touched what raw

takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja b818f5249 · Sergio ·

1//! Roundtrip e2e de Fase 4: construimos un `.swm` a mano (los tipos viven en takana-core),
2//! lo aplicamos a un prefix temporal, leemos el resultado y verificamos que los archivos
3//! coinciden con lo esperado.
4//!
5//! NO ejercitamos `source_patch` porque arrastra todo el lab (red + sandbox); para esos
6//! casos existen los tests gated en `TAKANA_NETWORK_TESTS`. Aquí cubrimos los caminos puros
7//! de takana-core: `verify_schema`, `verify_base`, `apply_config_edit`, `apply_file_drop`.
8
9use std::collections::BTreeMap;
10use std::path::Path;
11
12use base64::{engine::general_purpose::STANDARD, Engine as _};
13use takana_core::apply::{apply_config_edit, apply_file_drop, rebase_path, verify_content_hash};
14use takana_core::swm::{Base, BaseCompat, BaseRef, Mutation, Swm};
15use takana_core::ArtifactHash;
16
17fn write(p: &Path, content: &str) {
18    if let Some(parent) = p.parent() {
19        std::fs::create_dir_all(parent).unwrap();
20    }
21    std::fs::write(p, content).unwrap();
22}
23
24fn base(distro: &str, pins: &[(&str, &str)]) -> Base {
25    Base {
26        distro_version: distro.into(),
27        pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
28    }
29}
30
31fn baseref(distro: &str, pins: &[(&str, &str)]) -> BaseRef {
32    BaseRef {
33        distro_version: distro.into(),
34        pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
35    }
36}
37
38fn mk_swm(mutations: Vec<Mutation>) -> Swm {
39    Swm {
40        swm_version: 1,
41        base: base("2026-06-06", &[("musl", "deadbeef")]),
42        mutations,
43        signature: None,
44    }
45}
46
47/// Verifica que un `.swm` pasa schema y base, y luego aplica cada mutación bajo un prefix.
48/// Este es el camino que la CLI ejercita; aquí lo reproducimos como librería para que el
49/// test se mantenga aunque la CLI cambie superficialmente.
50fn apply_swm_under_prefix(swm: &Swm, prefix: &Path) {
51    swm.verify_schema().expect("schema válido");
52    // Base compatible (idéntica a la del .swm).
53    let local = BaseRef::from(&swm.base);
54    assert_eq!(swm.verify_base(&local), BaseCompat::Ok);
55
56    for m in &swm.mutations {
57        match m {
58            Mutation::ConfigEdit { file, inline_diff } => {
59                let target = rebase_path(file, Some(prefix));
60                apply_config_edit(&target, inline_diff).unwrap();
61            }
62            Mutation::FileDrop { path, content_hash, content_b64, .. } => {
63                let target = rebase_path(path, Some(prefix));
64                let b64 = content_b64.as_ref().expect("inline para tests");
65                apply_file_drop(&target, b64, content_hash).unwrap();
66            }
67            _ => panic!("este test sólo cubre config_edit/file_drop"),
68        }
69    }
70}
71
72#[test]
73fn roundtrip_yaml_apply_reproduces_files() {
74    let payload = b"hola hammer\n";
75    let payload_hash = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
76    let payload_b64 = STANDARD.encode(payload);
77
78    let swm = mk_swm(vec![
79        Mutation::ConfigEdit {
80            file: "/etc/network.conf".into(),
81            inline_diff: "- DHCP=yes\n+ IP=192.168.1.100\n".into(),
82        },
83        Mutation::FileDrop {
84            path: "/var/lib/hammer/saludo.txt".into(),
85            content_hash: payload_hash.clone(),
86            content_b64: Some(payload_b64),
87            content_url: None,
88        },
89    ]);
90
91    // Roundtrip YAML completo: serializa, deserializa y reusa.
92    let yaml = swm.to_yaml().unwrap();
93    let swm2 = Swm::from_yaml(&yaml).unwrap();
94    assert_eq!(swm2.mutations.len(), 2);
95
96    let prefix_dir = tempfile::tempdir().unwrap();
97    let prefix = prefix_dir.path();
98    // El archivo objetivo del config_edit debe existir antes (config_edit no crea archivos).
99    write(
100        &prefix.join("etc/network.conf"),
101        "USE=eth0\nDHCP=yes\nMTU=1500\n",
102    );
103
104    apply_swm_under_prefix(&swm2, prefix);
105
106    let got_net = std::fs::read_to_string(prefix.join("etc/network.conf")).unwrap();
107    assert_eq!(got_net, "USE=eth0\nIP=192.168.1.100\nMTU=1500\n");
108
109    let got_drop = std::fs::read(prefix.join("var/lib/hammer/saludo.txt")).unwrap();
110    assert_eq!(got_drop, payload);
111}
112
113#[test]
114fn verify_base_distinguishes_compatible_and_not() {
115    let swm = mk_swm(vec![Mutation::ConfigEdit {
116        file: "/etc/x".into(),
117        inline_diff: "- a\n+ b\n".into(),
118    }]);
119    // distro distinta
120    let other = baseref("2026-09-09", &[("musl", "deadbeef")]);
121    assert!(matches!(
122        swm.verify_base(&other),
123        BaseCompat::DistroMismatch { .. }
124    ));
125    // pin distinta
126    let bad_pin = baseref("2026-06-06", &[("musl", "ZZZZ")]);
127    assert!(matches!(swm.verify_base(&bad_pin), BaseCompat::PinMismatch(_)));
128    // pins extras locales no son problema
129    let mut more = BTreeMap::new();
130    more.insert("musl".into(), "deadbeef".into());
131    more.insert("kernel".into(), "abcdef".into());
132    let ok = BaseRef { distro_version: "2026-06-06".into(), pins: more };
133    assert_eq!(swm.verify_base(&ok), BaseCompat::Ok);
134}
135
136#[test]
137fn verify_content_hash_catches_payload_tampering() {
138    let payload = b"datos integros\n";
139    let b64 = STANDARD.encode(payload);
140    let h = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
141    verify_content_hash(&b64, &h).expect("hash ok");
142
143    // Mutar un byte del payload sin actualizar el hash ⇒ falla.
144    let mut bad = payload.to_vec();
145    bad[0] ^= 0x01;
146    let bad_b64 = STANDARD.encode(&bad);
147    let err = verify_content_hash(&bad_b64, &h).unwrap_err().to_string();
148    assert!(err.contains("no coincide"), "{err}");
149}
150
151#[test]
152fn yaml_includes_only_inline_field_when_url_is_none() {
153    // Un .swm autocontenido (lo que produce export) no debe acabar emitiendo `content_url`.
154    let swm = mk_swm(vec![Mutation::FileDrop {
155        path: "/var/lib/x".into(),
156        content_hash: "b3:00".into(),
157        content_b64: Some("AA==".into()),
158        content_url: None,
159    }]);
160    let y = swm.to_yaml().unwrap();
161    assert!(y.contains("content_b64"));
162    assert!(!y.contains("content_url"), "skip_serializing_if=None debe omitir el campo");
163}