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…
takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
1//! E2E del bucle agéntico (Fase 6):
2//!
3//! Camino directo (sin bus): un `MockTranslator` resuelve un intent conocido y el
4//! `Orchestrator` aplica las mutaciones bajo un prefix temporal. La prueba final pinta el
5//! cambio en disco y verifica que es el esperado.
6//!
7//! NO ejercita el bus aquí — el camino bus + cliente se cubre en
8//! `crates/hammer-agent/tests/client_e2e.rs` cuando lo construyamos sobre los crates
9//! `hammerd` (Fase 5). Phase 6 ya prueba bus + cliente en bus_e2e.rs del propio hammerd.
10
11use std::collections::BTreeMap;
12
13use base64::{engine::general_purpose::STANDARD, Engine as _};
14use takana_agent::{
15 orchestrator::{ApplyTarget, CompileMode},
16 MockTranslator, Orchestrator,
17};
18use takana_core::swm::{Base, Mutation, Swm};
19use takana_core::{ArtifactHash, BaseRef};
20
21fn baseref() -> BaseRef {
22 let mut pins = BTreeMap::new();
23 pins.insert("musl".into(), "deadbeef".into());
24 BaseRef { distro_version: "2026-06-06".into(), pins }
25}
26
27fn swm_two_mutations() -> Swm {
28 let payload = b"datos finales\n";
29 let payload_hash = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
30 let payload_b64 = STANDARD.encode(payload);
31 Swm {
32 swm_version: 1,
33 base: Base {
34 distro_version: "2026-06-06".into(),
35 pins: {
36 let mut m = BTreeMap::new();
37 m.insert("musl".into(), "deadbeef".into());
38 m
39 },
40 },
41 mutations: vec![
42 Mutation::ConfigEdit {
43 file: "/etc/network.conf".into(),
44 inline_diff: "- DHCP=yes\n+ IP=192.168.1.100\n".into(),
45 },
46 Mutation::FileDrop {
47 path: "/var/lib/agent/notes.txt".into(),
48 content_hash: payload_hash,
49 content_b64: Some(payload_b64),
50 content_url: None,
51 },
52 ],
53 signature: None,
54 }
55}
56
57#[test]
58fn intent_to_overlay_proposal_via_mock_translator() {
59 let d = tempfile::tempdir().unwrap();
60 let prefix = d.path().join("prefix");
61 std::fs::create_dir_all(prefix.join("etc")).unwrap();
62 std::fs::write(
63 prefix.join("etc/network.conf"),
64 "USE=eth0\nDHCP=yes\nMTU=1500\n",
65 )
66 .unwrap();
67
68 let translator = MockTranslator::empty()
69 .with_intent("set static ip and drop notes", swm_two_mutations());
70
71 let orch = Orchestrator::new(
72 translator,
73 baseref(),
74 ApplyTarget::Prefix { prefix: prefix.clone() },
75 CompileMode::Skip,
76 );
77
78 let proposal = orch.run("set static ip and drop notes").expect("proposal");
79
80 assert_eq!(proposal.applied.config_edit, 1);
81 assert_eq!(proposal.applied.file_drop, 1);
82 assert_eq!(proposal.applied.source_patch, 0);
83 assert!(proposal.overlay_id.is_none(), "modo Prefix no abre overlay");
84 assert_eq!(proposal.checks.len(), 2);
85 assert!(
86 proposal
87 .checks
88 .iter()
89 .all(|c| matches!(c.level, takana_agent::orchestrator::CheckLevel::Pass)),
90 "todas las checks deben pasar"
91 );
92
93 // Verificación post-condición: los archivos en disco coinciden con lo esperado.
94 let net = std::fs::read_to_string(prefix.join("etc/network.conf")).unwrap();
95 assert_eq!(net, "USE=eth0\nIP=192.168.1.100\nMTU=1500\n");
96 let notes = std::fs::read(prefix.join("var/lib/agent/notes.txt")).unwrap();
97 assert_eq!(notes, b"datos finales\n");
98}
99
100#[test]
101fn unknown_intent_surfaces_translator_error() {
102 let d = tempfile::tempdir().unwrap();
103 let orch = Orchestrator::new(
104 MockTranslator::empty(),
105 baseref(),
106 ApplyTarget::Prefix { prefix: d.path().to_path_buf() },
107 CompileMode::Skip,
108 );
109 let err = orch
110 .run("intencion sin entrada en el catalogo")
111 .unwrap_err()
112 .to_string();
113 assert!(err.contains("no reconocida"), "{err}");
114}
115
116#[test]
117fn proposal_is_serializable_for_humans() {
118 // Sanidad: el Proposal serializa a JSON. La CLI o un cliente externo lo puede mostrar.
119 let d = tempfile::tempdir().unwrap();
120 let prefix = d.path().join("prefix");
121 std::fs::create_dir_all(prefix.join("etc")).unwrap();
122 std::fs::write(prefix.join("etc/network.conf"), "DHCP=yes\n").unwrap();
123 let swm = Swm {
124 swm_version: 1,
125 base: Base {
126 distro_version: "2026-06-06".into(),
127 pins: {
128 let mut m = BTreeMap::new();
129 m.insert("musl".into(), "deadbeef".into());
130 m
131 },
132 },
133 mutations: vec![Mutation::ConfigEdit {
134 file: "/etc/network.conf".into(),
135 inline_diff: "- DHCP=yes\n+ STATIC=1\n".into(),
136 }],
137 signature: None,
138 };
139 let translator = MockTranslator::empty().with_intent("x", swm);
140 let orch = Orchestrator::new(
141 translator,
142 baseref(),
143 ApplyTarget::Prefix { prefix },
144 CompileMode::Skip,
145 );
146 let proposal = orch.run("x").unwrap();
147 let json = serde_json::to_string(&proposal).unwrap();
148 assert!(json.contains("\"applied\""));
149 assert!(json.contains("\"checks\""));
150}