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…
takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
1//! H4c e2e: el gate de **colisión de fichero observada** en `takana install` real.
2//!
3//! Reproduce el caso "logo" a nivel de fichero, sin declarar `slots`: dos paquetes escriben el
4//! mismo path. El primero ya está instalado (pre-seed de la DB); al instalar el segundo, el gate
5//! —que lee los paths que el `.swm` escribirá y los compara con lo instalado— debe ABORTAR antes
6//! de tocar nada. Con `--force-slots` la elección se respeta y el fichero se escribe.
7
8use std::process::Command;
9
10use base64::{engine::general_purpose::STANDARD, Engine as _};
11use takana_core::ArtifactHash;
12
13#[test]
14fn install_aborta_ante_colision_de_fichero_y_force_slots_la_supera() {
15 let tmp = tempfile::tempdir().unwrap();
16 let repo = tmp.path().join("repo");
17 std::fs::create_dir_all(&repo).unwrap();
18 let prefix = tmp.path().join("prefix");
19 let db = tmp.path().join("installed.json");
20
21 // Pre-seed: 'tema-claro' YA posee /share/logo.png.
22 std::fs::write(
23 &db,
24 r#"{"packages":[{"name":"tema-claro","version":"1","files":["/share/logo.png"]}]}"#,
25 )
26 .unwrap();
27
28 // Paquete entrante 'tema-oscuro' = un file_drop que escribe el MISMO path.
29 let content = b"logo-oscuro";
30 // apply_file_drop verifica con `of_inputs` (length-prefixed), no `of_bytes` — mismamos eso.
31 let hash = ArtifactHash::of_inputs(&[content.as_slice()]);
32 let b64 = STANDARD.encode(content);
33 let swm = format!(
34 "swm_version: 1\nbase:\n distro_version: dev\n pins: {{}}\nmutations:\n\
35 - type: file_drop\n path: /share/logo.png\n content_hash: {}\n content_b64: {}\n",
36 hash.as_str(),
37 b64
38 );
39 std::fs::write(repo.join("tema-oscuro-1.swm"), swm).unwrap();
40 let index = r#"{"packages":[{"name":"tema-oscuro","version":"1","file":"tema-oscuro-1.swm","distro_version":"dev","deps":[]}]}"#;
41 std::fs::write(repo.join("index.json"), index).unwrap();
42
43 let bin = env!("CARGO_BIN_EXE_hammer");
44 let base_args = [
45 "install",
46 "tema-oscuro",
47 "--repo",
48 repo.to_str().unwrap(),
49 "--db",
50 db.to_str().unwrap(),
51 "--prefix",
52 prefix.to_str().unwrap(),
53 ];
54
55 // 1) Sin --force-slots: colisión ⇒ aborta, y el fichero NO se escribe (aborta pre-hidratación).
56 let out = Command::new(bin).args(base_args).output().unwrap();
57 assert!(!out.status.success(), "debía abortar por colisión de fichero");
58 let err = String::from_utf8_lossy(&out.stderr);
59 assert!(err.contains("COLISIÓN de fichero"), "stderr inesperado: {err}");
60 assert!(
61 !prefix.join("share/logo.png").exists(),
62 "no debía escribir el fichero al abortar"
63 );
64
65 // 2) Con --force-slots: la elección se respeta y el fichero se escribe.
66 let out = Command::new(bin)
67 .args(base_args)
68 .arg("--force-slots")
69 .output()
70 .unwrap();
71 let err = String::from_utf8_lossy(&out.stderr);
72 assert!(out.status.success(), "con --force-slots debía instalar. stderr: {err}");
73 assert!(
74 prefix.join("share/logo.png").exists(),
75 "el fichero debía escribirse con --force-slots. stderr: {err}"
76 );
77}
78
79/// H4d e2e: `takana compat <repo>` particiona un repo contra el estado instalado. La búsqueda
80/// del prototipo `wawa-memo` (`filtrar`), ahora sobre el repo real y read-only.
81#[test]
82fn compat_particiona_el_repo_contra_lo_instalado() {
83 let tmp = tempfile::tempdir().unwrap();
84 let repo = tmp.path().join("repo");
85 std::fs::create_dir_all(&repo).unwrap();
86 let db = tmp.path().join("installed.json");
87
88 // Instalado: 'tema-claro' posee /share/logo.png.
89 std::fs::write(
90 &db,
91 r#"{"packages":[{"name":"tema-claro","version":"1","files":["/share/logo.png"]}]}"#,
92 )
93 .unwrap();
94
95 // Dos paquetes en el repo: uno choca (mismo path que tema-claro), otro es limpio (otro path).
96 for (name, path) in [("tema-oscuro", "/share/logo.png"), ("fondo-montaña", "/share/fondo.png")] {
97 let content = format!("bytes-{name}");
98 let hash = ArtifactHash::of_inputs(&[content.as_bytes()]);
99 let b64 = STANDARD.encode(content.as_bytes());
100 let swm = format!(
101 "swm_version: 1\nbase:\n distro_version: dev\n pins: {{}}\nmutations:\n\
102 - type: file_drop\n path: {path}\n content_hash: {}\n content_b64: {}\n",
103 hash.as_str(), b64
104 );
105 std::fs::write(repo.join(format!("{name}-1.swm")), swm).unwrap();
106 }
107 let index = r#"{"packages":[
108 {"name":"tema-oscuro","version":"1","file":"tema-oscuro-1.swm","distro_version":"dev","deps":[]},
109 {"name":"fondo-montaña","version":"1","file":"fondo-montaña-1.swm","distro_version":"dev","deps":[]}
110 ]}"#;
111 std::fs::write(repo.join("index.json"), index).unwrap();
112
113 let out = Command::new(env!("CARGO_BIN_EXE_hammer"))
114 .args(["compat", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap()])
115 .output()
116 .unwrap();
117 assert!(out.status.success());
118 let stdout = String::from_utf8_lossy(&out.stdout);
119 // fondo-montaña es compatible; tema-oscuro requiere elección (colisión de fichero).
120 assert!(stdout.contains("compatibles (1)"), "stdout: {stdout}");
121 assert!(stdout.contains("✓ fondo-montaña"), "stdout: {stdout}");
122 assert!(stdout.contains("requieren elección (1)"), "stdout: {stdout}");
123 assert!(stdout.contains("tema-oscuro"), "stdout: {stdout}");
124}
125
126/// H4e e2e: `takana compat` marca INCOMPATIBLE un paquete cuya dep de runtime divergiste
127/// (el caso wayland, DERIVADO — sin que el paquete declare `requires`). Read-only: ve el
128/// source_patch sin construirlo.
129#[test]
130fn compat_marca_incompatible_una_dep_divergida() {
131 let tmp = tempfile::tempdir().unwrap();
132 let repo = tmp.path().join("repo");
133 std::fs::create_dir_all(&repo).unwrap();
134 let db = tmp.path().join("installed.json");
135
136 // Tengo wayland-protocol instalado a b3:mine (lo diverguí del stock del repo).
137 std::fs::write(
138 &db,
139 r#"{"packages":[{"name":"wayland-protocol","version":"1","hash":"b3:mine","files":[]}]}"#,
140 )
141 .unwrap();
142
143 // 'app' es un source_patch que depende (runtime) de wayland-protocol.
144 let app_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations:\n\
145 - type: source_patch\n repo: git://x/app.git\n commit: abc\n target_bin: /usr/bin/app\n\
146 \x20 build:\n compiler: zig-cc\n deps:\n runtime:\n - wayland-protocol\n";
147 std::fs::write(repo.join("app-1.swm"), app_swm).unwrap();
148 // wayland-protocol también está en el repo, a b3:stock (la versión contra la que app se hizo).
149 let w_swm = "swm_version: 1\nbase:\n distro_version: dev\n pins: {}\nmutations: []\n";
150 std::fs::write(repo.join("w-1.swm"), w_swm).unwrap();
151 let index = r#"{"packages":[
152 {"name":"app","version":"1","file":"app-1.swm","distro_version":"dev","deps":["wayland-protocol"],"expected_hash":"b3:app"},
153 {"name":"wayland-protocol","version":"1","file":"w-1.swm","distro_version":"dev","deps":[],"expected_hash":"b3:stock"}
154 ]}"#;
155 std::fs::write(repo.join("index.json"), index).unwrap();
156
157 let out = Command::new(env!("CARGO_BIN_EXE_hammer"))
158 .args(["compat", "--repo", repo.to_str().unwrap(), "--db", db.to_str().unwrap()])
159 .output()
160 .unwrap();
161 assert!(out.status.success());
162 let stdout = String::from_utf8_lossy(&out.stdout);
163 assert!(stdout.contains("incompatibles (1)"), "stdout: {stdout}");
164 assert!(stdout.contains("app") && stdout.contains("deps divergidas"), "stdout: {stdout}");
165}