Files
takana/crates/takana-agent/tests/orchestrator_evidence_gate.rs
T
Sergio 9fe1e010e2 paquetería: zsh se puede instalar — los parches múltiples viajaban unidos y el target_bin era una adivinanza
Cerrar el lazo servir→instalar sobre `zsh` (pedido del usuario) destapó dos fallos de la familia
del `strip_debug` del SDD 28 §5.2: cosas que el `.swm` no transportaba fiel y que sólo se ven
cerrando el lazo, no leyendo el código.

1. `pack` CONCATENABA los N parches en el `patch` inline único, y `hash_inputs` mete una entrada
   por parche (con `of_inputs` length-prefijado) ⇒ el receptor sellaba en otra dirección y el
   `expected_hash` no coincidía NUNCA. Medido: zsh anclado b3:0be3630d…, reproducido b3:d6329a62…
   (idéntico al de una copia de la receta con sus 6 parches concatenados a mano). Afectaba a las
   16 recetas del corpus con ≥2 parches. Ahora viajan como lista `patches`, en orden, un fichero
   por parche del lado del receptor; el `patch` único se sigue leyendo para no invalidar lo
   publicado. Regresión con control negativo en swm_bridge.

2. `target_bin` se adivinaba `/usr/bin/{name}`; zsh instala en `/bin` (--bindir=/bin), así que el
   paquete prometía un path que el artefacto no tiene y el error salía en el cliente DESPUÉS de
   hidratar 1331 ficheros. Con `--build` el artefacto está delante: se comprueba, se corrige
   diciéndolo, y si no hay ningún binario con ese nombre se ABORTA al publicar.

Y el verbo que faltaba para «avisame cuándo actualizar»: `takana outdated` compara la DB de
instalados con el índice firmado (por hash, no por versión: una re-publicación con la misma
etiqueta es otro artefacto). No construye, no baja .swm y no actualiza — instalar es verificar.

Medido tras el arreglo: install zsh ⇒ cache-hit del hash anclado, 1331 ficheros en 0,14 s, zsh 5.9
corriendo.
2026-09-21 18:31:14 +00:00

162 lines
5.5 KiB
Rust

//! [H1c] Gate de evidencia en el orquestador contra un bus stub.
//!
//! El lab (aquí, un stub) ejecuta la evidencia tras sellar el artefacto y devuelve el veredicto
//! en `BuildReady`. Si algún check falla, `Orchestrator::run` debe ABORTAR con "no se propone" y
//! NO hidratar nada al `prefix` — el gate "falla ⇒ no se propone" del SDD 15 §H1.
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use takana_agent::orchestrator::{ApplyTarget, CompileMode, Orchestrator};
use takana_agent::MockTranslator;
use takana_core::proto::{
Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, PROTOCOL_VERSION,
};
use takana_core::swm::{Base, Mutation, Swm, SwmBuild};
use takana_core::{BaseRef, Evidence, EvidenceCheck, EvidenceKind};
use std::collections::BTreeMap;
/// Bus stub que responde a `Compile` con `BuildReady` cuyo veredicto tiene `all_passed=false`.
fn start_failing_bus(sock: PathBuf) {
let _ = std::fs::remove_file(&sock);
let listener = UnixListener::bind(&sock).expect("bind stub");
thread::spawn(move || {
if let Some(Ok(stream)) = listener.incoming().next() {
thread::spawn(move || serve(stream));
}
});
}
fn serve(stream: UnixStream) {
let mut writer = stream.try_clone().unwrap();
let mut reader = BufReader::new(stream);
let send = |w: &mut UnixStream, ev: &Event| {
w.write_all(serde_json::to_string(ev).unwrap().as_bytes()).unwrap();
w.write_all(b"\n").unwrap();
w.flush().unwrap();
};
let mut first = String::new();
reader.read_line(&mut first).unwrap();
let Ok(Command::Hello { .. }) = serde_json::from_str::<Command>(first.trim()) else {
return;
};
send(
&mut writer,
&Event::Welcome {
ver: PROTOCOL_VERSION,
caps: vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init],
peer: Peer { uid: 1000, gid: 1000, pid: 1 },
},
);
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {}
}
if line.trim().is_empty() {
continue;
}
let Ok(cmd) = serde_json::from_str::<Command>(line.trim()) else {
continue;
};
if let Command::Compile { recipe } = cmd {
send(
&mut writer,
&Event::BuildReady {
recipe: recipe.name.clone(),
artifact: "b3:stub-fail".into(),
verdict: Some(EvidenceVerdict {
all_passed: false,
max_level: None,
checks: vec![EvidenceCheckVerdict {
kind: EvidenceKind::CmdExit,
cmd: "grep --version".into(),
passed: false,
detail: "exit 7 ≠ esperado 0".into(),
}],
}),
},
);
}
}
}
fn swm_with_evidence() -> Swm {
Swm {
swm_version: 1,
base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
mutations: vec![Mutation::SourcePatch {
repo: Some("git://x/grep.git".into()),
commit: Some("abc".into()),
tarball: None,
sha256: None,
strip_components: None,
patch: None,
patches: Vec::new(),
patch_url: None,
build: SwmBuild {
compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(),
link: "static".into(),
flags: vec![],
phases: Default::default(),
zig_version: None,
strip_debug: None,
},
target_bin: "/usr/bin/grep".into(),
expected_hash: None,
deps: Default::default(),
evidence: Evidence {
checks: vec![EvidenceCheck {
kind: EvidenceKind::CmdExit,
cmd: "grep --version".into(),
expected_exit: 0,
expected_output: None,
}],
},
slots: Default::default(),
}],
signature: None,
}
}
#[test]
fn failing_evidence_aborts_and_does_not_hydrate() {
let d = tempfile::tempdir().unwrap();
let sock = d.path().join("agent.sock");
start_failing_bus(sock.clone());
for _ in 0..50 {
if sock.exists() {
break;
}
thread::sleep(Duration::from_millis(10));
}
let prefix = d.path().join("prefix");
let store = d.path().join("store");
let base = BaseRef { distro_version: "2026-06-06".into(), pins: BTreeMap::new() };
let t = MockTranslator::empty().with_intent("compile grep", swm_with_evidence());
let o = Orchestrator::new(
t,
base,
ApplyTarget::Prefix { prefix: prefix.clone() },
CompileMode::ViaBus { sock, store, timeout: Duration::from_secs(5) },
);
let err = o.run("compile grep").unwrap_err().to_string();
assert!(err.contains("no se propone"), "mensaje inesperado: {err}");
assert!(err.contains("evidencia") || err.contains("check(s)"), "{err}");
// El gate corta ANTES de hidratar: el target_bin no debe existir bajo el prefix.
assert!(
!prefix.join("usr/bin/grep").exists(),
"el binario se hidrató pese al fallo de evidencia"
);
}