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.
140 lines
6.0 KiB
Rust
140 lines
6.0 KiB
Rust
//! E2E del orquestador `takana boot menu` (ADR 0010, glue de arranque): emite el grafo, lanza el
|
|
//! compositor (mirada) y activa el nodo elegido. Probamos el GLUE headless con stubs de compositor —
|
|
//! el pivot de generaciones ya lo cubren los tests de `boot_graph::activate`. Tres caminos:
|
|
//! A) sin compositor instalado (servidor headless) → emite el grafo y sigue el arranque (exit 0).
|
|
//! B) el compositor no elige nada → "sin selección", sigue el arranque (exit 0).
|
|
//! C) el compositor escribe un id en boot-select → menu lo consume y se lo pasa a `activate`.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_hammer");
|
|
|
|
fn menu(state: &Path, work: &Path, compositor: &str) -> std::process::Output {
|
|
Command::new(BIN)
|
|
.args(["boot", "--state-root"])
|
|
.arg(state)
|
|
.arg("--journal")
|
|
.arg(work.join("journal"))
|
|
.arg("menu")
|
|
.arg("--compositor")
|
|
.arg(compositor)
|
|
.arg("--out")
|
|
.arg(work.join("graph.json"))
|
|
.arg("--select")
|
|
.arg(work.join("boot-select"))
|
|
.output()
|
|
.expect("ejecutar hammer boot menu")
|
|
}
|
|
|
|
fn write_stub(path: &Path, body: &str) -> String {
|
|
std::fs::write(path, body).unwrap();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
}
|
|
path.display().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn a_sin_compositor_emite_grafo_y_sigue() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
let out = menu(&state, tmp.path(), "no-existe-compositor-xyz --arranque");
|
|
assert!(out.status.success(), "graceful sin compositor debe exit 0: {out:?}");
|
|
// el grafo se publica igual (otros lectores lo usan)
|
|
assert!(tmp.path().join("graph.json").exists(), "el grafo debe emitirse aun sin compositor");
|
|
let so = String::from_utf8_lossy(&out.stdout);
|
|
assert!(so.contains("sin compositor"), "debe reportar la ausencia del compositor: {so}");
|
|
}
|
|
|
|
#[test]
|
|
fn b_compositor_sin_seleccion_sigue_arranque() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
let stub = write_stub(&tmp.path().join("noop.sh"), "#!/bin/sh\nexit 0\n");
|
|
let out = menu(&state, tmp.path(), &stub);
|
|
assert!(out.status.success(), "sin selección debe exit 0: {out:?}");
|
|
let so = String::from_utf8_lossy(&out.stdout);
|
|
assert!(so.contains("sin selección"), "debe reportar que no hubo elección: {so}");
|
|
}
|
|
|
|
#[test]
|
|
fn c_seleccion_viaja_de_boot_select_a_activate() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
// stub que "elige" un id escribiéndolo en el canal de vuelta (el path que menu le pasa por --select).
|
|
let sel = tmp.path().join("boot-select");
|
|
let stub = write_stub(
|
|
&tmp.path().join("pick.sh"),
|
|
&format!("#!/bin/sh\necho cafe1234deadbeef > '{}'\n", sel.display()),
|
|
);
|
|
let out = menu(&state, tmp.path(), &stub);
|
|
// El state_root de prueba está vacío (sin generación viva) ⇒ activate falla; PERO eso prueba que el
|
|
// id viajó de boot-select → activate (el glue). El pivot exitoso lo cubren los tests de boot_graph.
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(!out.status.success(), "con un id inválido y grafo vacío, activate debe fallar");
|
|
assert!(
|
|
err.contains("generación viva") || err.contains("cafe1234") || err.contains("grafo"),
|
|
"el fallo debe venir de activate (el id llegó): stderr={err}"
|
|
);
|
|
}
|
|
|
|
// --- `takana boot activate --from-select` idempotente (contrato PLAN-KIKIN §9) ---------------------
|
|
// arje-zero lo invoca INCONDICIONAL en todo apagado: sin selección debe ser un no-op limpio (exit 0),
|
|
// y el fichero sólo se consume si la activación tuvo éxito (si falla, queda para diagnóstico).
|
|
|
|
fn activate_from_select(state: &Path, work: &Path, select: &Path) -> std::process::Output {
|
|
Command::new(BIN)
|
|
.args(["boot", "--state-root"])
|
|
.arg(state)
|
|
.arg("--journal")
|
|
.arg(work.join("journal"))
|
|
.args(["activate", "--from-select", "--select"])
|
|
.arg(select)
|
|
.output()
|
|
.expect("ejecutar hammer boot activate")
|
|
}
|
|
|
|
#[test]
|
|
fn d_activate_from_select_sin_fichero_es_noop() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
let out = activate_from_select(&state, tmp.path(), &tmp.path().join("boot-select"));
|
|
assert!(out.status.success(), "sin fichero debe ser no-op exit 0: {out:?}");
|
|
let so = String::from_utf8_lossy(&out.stdout);
|
|
assert!(so.contains("no-op"), "debe reportar el no-op: {so}");
|
|
}
|
|
|
|
#[test]
|
|
fn e_activate_from_select_vacio_es_noop() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
let sel = tmp.path().join("boot-select");
|
|
std::fs::write(&sel, "\n").unwrap();
|
|
let out = activate_from_select(&state, tmp.path(), &sel);
|
|
assert!(out.status.success(), "fichero vacío debe ser no-op exit 0: {out:?}");
|
|
let so = String::from_utf8_lossy(&out.stdout);
|
|
assert!(so.contains("no-op"), "debe reportar el no-op: {so}");
|
|
}
|
|
|
|
#[test]
|
|
fn f_activate_fallido_no_consume_el_select() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let state = tmp.path().join("state");
|
|
std::fs::create_dir_all(&state).unwrap();
|
|
let sel = tmp.path().join("boot-select");
|
|
std::fs::write(&sel, "cafe1234deadbeef\n").unwrap();
|
|
// state_root vacío ⇒ no hay generación viva ⇒ activate falla…
|
|
let out = activate_from_select(&state, tmp.path(), &sel);
|
|
assert!(!out.status.success(), "con grafo vacío activate debe fallar: {out:?}");
|
|
// …y el canal queda intacto para diagnóstico (sólo se consume en éxito).
|
|
assert!(sel.exists(), "el boot-select NO debe consumirse cuando activate falla");
|
|
}
|