//! [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::(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::(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" ); }