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…
1//! [H1c] Gate de evidencia en el orquestador contra un bus stub.
2//!
3//! El lab (aquí, un stub) ejecuta la evidencia tras sellar el artefacto y devuelve el veredicto
4//! en `BuildReady`. Si algún check falla, `Orchestrator::run` debe ABORTAR con "no se propone" y
5//! NO hidratar nada al `prefix` — el gate "falla ⇒ no se propone" del SDD 15 §H1.
6
7use std::io::{BufRead, BufReader, Write};
8use std::os::unix::net::{UnixListener, UnixStream};
9use std::path::PathBuf;
10use std::thread;
11use std::time::Duration;
12
13use takana_agent::orchestrator::{ApplyTarget, CompileMode, Orchestrator};
14use takana_agent::MockTranslator;
15use takana_core::proto::{
16 Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, PROTOCOL_VERSION,
17};
18use takana_core::swm::{Base, Mutation, Swm, SwmBuild};
19use takana_core::{BaseRef, Evidence, EvidenceCheck, EvidenceKind};
20use std::collections::BTreeMap;
21
22/// Bus stub que responde a `Compile` con `BuildReady` cuyo veredicto tiene `all_passed=false`.
23fn start_failing_bus(sock: PathBuf) {
24 let _ = std::fs::remove_file(&sock);
25 let listener = UnixListener::bind(&sock).expect("bind stub");
26 thread::spawn(move || {
27 if let Some(Ok(stream)) = listener.incoming().next() {
28 thread::spawn(move || serve(stream));
29 }
30 });
31}
32
33fn serve(stream: UnixStream) {
34 let mut writer = stream.try_clone().unwrap();
35 let mut reader = BufReader::new(stream);
36 let send = |w: &mut UnixStream, ev: &Event| {
37 w.write_all(serde_json::to_string(ev).unwrap().as_bytes()).unwrap();
38 w.write_all(b"\n").unwrap();
39 w.flush().unwrap();
40 };
41
42 let mut first = String::new();
43 reader.read_line(&mut first).unwrap();
44 let Ok(Command::Hello { .. }) = serde_json::from_str::<Command>(first.trim()) else {
45 return;
46 };
47 send(
48 &mut writer,
49 &Event::Welcome {
50 ver: PROTOCOL_VERSION,
51 caps: vec![Cap::Query, Cap::Compile, Cap::Inject, Cap::Init],
52 peer: Peer { uid: 1000, gid: 1000, pid: 1 },
53 },
54 );
55
56 loop {
57 let mut line = String::new();
58 match reader.read_line(&mut line) {
59 Ok(0) | Err(_) => break,
60 Ok(_) => {}
61 }
62 if line.trim().is_empty() {
63 continue;
64 }
65 let Ok(cmd) = serde_json::from_str::<Command>(line.trim()) else {
66 continue;
67 };
68 if let Command::Compile { recipe } = cmd {
69 send(
70 &mut writer,
71 &Event::BuildReady {
72 recipe: recipe.name.clone(),
73 artifact: "b3:stub-fail".into(),
74 verdict: Some(EvidenceVerdict {
75 all_passed: false,
76 max_level: None,
77 checks: vec![EvidenceCheckVerdict {
78 kind: EvidenceKind::CmdExit,
79 cmd: "grep --version".into(),
80 passed: false,
81 detail: "exit 7 ≠ esperado 0".into(),
82 }],
83 }),
84 },
85 );
86 }
87 }
88}
89
90fn swm_with_evidence() -> Swm {
91 Swm {
92 swm_version: 1,
93 base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
94 mutations: vec![Mutation::SourcePatch {
95 repo: Some("git://x/grep.git".into()),
96 commit: Some("abc".into()),
97 tarball: None,
98 sha256: None,
99 strip_components: None,
100 patch: None,
101 patches: Vec::new(),
102 patch_url: None,
103 build: SwmBuild {
104 compiler: "zig-cc".into(),
105 target: "x86_64-linux-musl".into(),
106 link: "static".into(),
107 flags: vec![],
108 phases: Default::default(),
109 zig_version: None,
110 strip_debug: None,
111 },
112 target_bin: "/usr/bin/grep".into(),
113 expected_hash: None,
114 deps: Default::default(),
115 evidence: Evidence {
116 checks: vec![EvidenceCheck {
117 kind: EvidenceKind::CmdExit,
118 cmd: "grep --version".into(),
119 expected_exit: 0,
120 expected_output: None,
121 }],
122 },
123 slots: Default::default(),
124 }],
125 signature: None,
126 }
127}
128
129#[test]
130fn failing_evidence_aborts_and_does_not_hydrate() {
131 let d = tempfile::tempdir().unwrap();
132 let sock = d.path().join("agent.sock");
133 start_failing_bus(sock.clone());
134 for _ in 0..50 {
135 if sock.exists() {
136 break;
137 }
138 thread::sleep(Duration::from_millis(10));
139 }
140
141 let prefix = d.path().join("prefix");
142 let store = d.path().join("store");
143 let base = BaseRef { distro_version: "2026-06-06".into(), pins: BTreeMap::new() };
144 let t = MockTranslator::empty().with_intent("compile grep", swm_with_evidence());
145 let o = Orchestrator::new(
146 t,
147 base,
148 ApplyTarget::Prefix { prefix: prefix.clone() },
149 CompileMode::ViaBus { sock, store, timeout: Duration::from_secs(5) },
150 );
151
152 let err = o.run("compile grep").unwrap_err().to_string();
153 assert!(err.contains("no se propone"), "mensaje inesperado: {err}");
154 assert!(err.contains("evidencia") || err.contains("check(s)"), "{err}");
155
156 // El gate corta ANTES de hidratar: el target_bin no debe existir bajo el prefix.
157 assert!(
158 !prefix.join("usr/bin/grep").exists(),
159 "el binario se hidrató pese al fallo de evidencia"
160 );
161}