tawasuyu

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 1570Shell 203Rust 109Markdown 993728 Definitions

⎇ main crates/takana-agent/tests/ai_loop_e2e.rs Rust · 150 lines · 5.0 KB History Who touched what raw

takana etapa 4: los 10 crates de librería y el CLI pasan a takana-* 24bcf1783 · Sergio ·

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}