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 1389Shell 137Markdown 80Rust 802602 Definiciones

⎇ main crates/takana-cli/tests/boot_menu.rs Rust · 139 líneas · 6.0 KB Historia Quién tocó qué crudo

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

1//! E2E del orquestador `takana boot menu` (ADR 0010, glue de arranque): emite el grafo, lanza el
2//! compositor (mirada) y activa el nodo elegido. Probamos el GLUE headless con stubs de compositor —
3//! el pivot de generaciones ya lo cubren los tests de `boot_graph::activate`. Tres caminos:
4//!   A) sin compositor instalado (servidor headless)  → emite el grafo y sigue el arranque (exit 0).
5//!   B) el compositor no elige nada                    → "sin selección", sigue el arranque (exit 0).
6//!   C) el compositor escribe un id en boot-select     → menu lo consume y se lo pasa a `activate`.
7
8use std::path::Path;
9use std::process::Command;
10
11const BIN: &str = env!("CARGO_BIN_EXE_hammer");
12
13fn menu(state: &Path, work: &Path, compositor: &str) -> std::process::Output {
14    Command::new(BIN)
15        .args(["boot", "--state-root"])
16        .arg(state)
17        .arg("--journal")
18        .arg(work.join("journal"))
19        .arg("menu")
20        .arg("--compositor")
21        .arg(compositor)
22        .arg("--out")
23        .arg(work.join("graph.json"))
24        .arg("--select")
25        .arg(work.join("boot-select"))
26        .output()
27        .expect("ejecutar hammer boot menu")
28}
29
30fn write_stub(path: &Path, body: &str) -> String {
31    std::fs::write(path, body).unwrap();
32    #[cfg(unix)]
33    {
34        use std::os::unix::fs::PermissionsExt;
35        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
36    }
37    path.display().to_string()
38}
39
40#[test]
41fn a_sin_compositor_emite_grafo_y_sigue() {
42    let tmp = tempfile::tempdir().unwrap();
43    let state = tmp.path().join("state");
44    std::fs::create_dir_all(&state).unwrap();
45    let out = menu(&state, tmp.path(), "no-existe-compositor-xyz --arranque");
46    assert!(out.status.success(), "graceful sin compositor debe exit 0: {out:?}");
47    // el grafo se publica igual (otros lectores lo usan)
48    assert!(tmp.path().join("graph.json").exists(), "el grafo debe emitirse aun sin compositor");
49    let so = String::from_utf8_lossy(&out.stdout);
50    assert!(so.contains("sin compositor"), "debe reportar la ausencia del compositor: {so}");
51}
52
53#[test]
54fn b_compositor_sin_seleccion_sigue_arranque() {
55    let tmp = tempfile::tempdir().unwrap();
56    let state = tmp.path().join("state");
57    std::fs::create_dir_all(&state).unwrap();
58    let stub = write_stub(&tmp.path().join("noop.sh"), "#!/bin/sh\nexit 0\n");
59    let out = menu(&state, tmp.path(), &stub);
60    assert!(out.status.success(), "sin selección debe exit 0: {out:?}");
61    let so = String::from_utf8_lossy(&out.stdout);
62    assert!(so.contains("sin selección"), "debe reportar que no hubo elección: {so}");
63}
64
65#[test]
66fn c_seleccion_viaja_de_boot_select_a_activate() {
67    let tmp = tempfile::tempdir().unwrap();
68    let state = tmp.path().join("state");
69    std::fs::create_dir_all(&state).unwrap();
70    // stub que "elige" un id escribiéndolo en el canal de vuelta (el path que menu le pasa por --select).
71    let sel = tmp.path().join("boot-select");
72    let stub = write_stub(
73        &tmp.path().join("pick.sh"),
74        &format!("#!/bin/sh\necho cafe1234deadbeef > '{}'\n", sel.display()),
75    );
76    let out = menu(&state, tmp.path(), &stub);
77    // El state_root de prueba está vacío (sin generación viva) ⇒ activate falla; PERO eso prueba que el
78    // id viajó de boot-select → activate (el glue). El pivot exitoso lo cubren los tests de boot_graph.
79    let err = String::from_utf8_lossy(&out.stderr);
80    assert!(!out.status.success(), "con un id inválido y grafo vacío, activate debe fallar");
81    assert!(
82        err.contains("generación viva") || err.contains("cafe1234") || err.contains("grafo"),
83        "el fallo debe venir de activate (el id llegó): stderr={err}"
84    );
85}
86
87// --- `takana boot activate --from-select` idempotente (contrato PLAN-KIKIN §9) ---------------------
88// arje-zero lo invoca INCONDICIONAL en todo apagado: sin selección debe ser un no-op limpio (exit 0),
89// y el fichero sólo se consume si la activación tuvo éxito (si falla, queda para diagnóstico).
90
91fn activate_from_select(state: &Path, work: &Path, select: &Path) -> std::process::Output {
92    Command::new(BIN)
93        .args(["boot", "--state-root"])
94        .arg(state)
95        .arg("--journal")
96        .arg(work.join("journal"))
97        .args(["activate", "--from-select", "--select"])
98        .arg(select)
99        .output()
100        .expect("ejecutar hammer boot activate")
101}
102
103#[test]
104fn d_activate_from_select_sin_fichero_es_noop() {
105    let tmp = tempfile::tempdir().unwrap();
106    let state = tmp.path().join("state");
107    std::fs::create_dir_all(&state).unwrap();
108    let out = activate_from_select(&state, tmp.path(), &tmp.path().join("boot-select"));
109    assert!(out.status.success(), "sin fichero debe ser no-op exit 0: {out:?}");
110    let so = String::from_utf8_lossy(&out.stdout);
111    assert!(so.contains("no-op"), "debe reportar el no-op: {so}");
112}
113
114#[test]
115fn e_activate_from_select_vacio_es_noop() {
116    let tmp = tempfile::tempdir().unwrap();
117    let state = tmp.path().join("state");
118    std::fs::create_dir_all(&state).unwrap();
119    let sel = tmp.path().join("boot-select");
120    std::fs::write(&sel, "\n").unwrap();
121    let out = activate_from_select(&state, tmp.path(), &sel);
122    assert!(out.status.success(), "fichero vacío debe ser no-op exit 0: {out:?}");
123    let so = String::from_utf8_lossy(&out.stdout);
124    assert!(so.contains("no-op"), "debe reportar el no-op: {so}");
125}
126
127#[test]
128fn f_activate_fallido_no_consume_el_select() {
129    let tmp = tempfile::tempdir().unwrap();
130    let state = tmp.path().join("state");
131    std::fs::create_dir_all(&state).unwrap();
132    let sel = tmp.path().join("boot-select");
133    std::fs::write(&sel, "cafe1234deadbeef\n").unwrap();
134    // state_root vacío ⇒ no hay generación viva ⇒ activate falla…
135    let out = activate_from_select(&state, tmp.path(), &sel);
136    assert!(!out.status.success(), "con grafo vacío activate debe fallar: {out:?}");
137    // …y el canal queda intacto para diagnóstico (sólo se consume en éxito).
138    assert!(sel.exists(), "el boot-select NO debe consumirse cuando activate falla");
139}