⎇ 3dc85ebdcdd5fda0a1be0db5f8fbce5859266f2e
shared/format/src/pruebas.rs
Historia
Quién tocó qué
crudo
feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro)
1 use super::*;
2
3 #[test]
4 fn blob_no_tiene_hijos() {
5 let b = objeto_blob(vec![0xAA, 0xBB, 0xCC]);
6 assert_eq!(b.datos, vec![0xAA, 0xBB, 0xCC]);
7 assert!(b.hijos.is_empty());
8 }
9
10 #[test]
11 fn arbol_ordena_entradas_por_nombre() {
12 // Entradas en orden caótico — el objeto-árbol debe ordenarlas.
13 let entradas = vec![
14 EntradaArbol { nombre: "zeta.rs".into(), modo: ModoEntrada::Archivo, hash: [1; 32] },
15 EntradaArbol { nombre: "alfa.rs".into(), modo: ModoEntrada::Archivo, hash: [2; 32] },
16 EntradaArbol { nombre: "sub".into(), modo: ModoEntrada::Directorio, hash: [3; 32] },
17 ];
18 let obj = objeto_arbol(entradas).unwrap();
19 let arbol = Arbol::deserializar(&obj.datos).unwrap();
20 let nombres: Vec<&str> = arbol.entradas.iter().map(|e| e.nombre.as_str()).collect();
21 assert_eq!(nombres, ["alfa.rs", "sub", "zeta.rs"]);
22 // `hijos` viaja en el MISMO orden que las entradas ordenadas.
23 assert_eq!(obj.hijos, vec![[2u8; 32], [3u8; 32], [1u8; 32]]);
24 }
25
26 #[test]
27 fn arbol_es_determinista_independiente_del_orden_de_entrada() {
28 // El mismo directorio dado en dos órdenes distintos => MISMO hash.
29 let a = vec![
30 EntradaArbol { nombre: "b".into(), modo: ModoEntrada::Archivo, hash: [5; 32] },
31 EntradaArbol { nombre: "a".into(), modo: ModoEntrada::Archivo, hash: [6; 32] },
32 ];
33 let b = vec![
34 EntradaArbol { nombre: "a".into(), modo: ModoEntrada::Archivo, hash: [6; 32] },
35 EntradaArbol { nombre: "b".into(), modo: ModoEntrada::Archivo, hash: [5; 32] },
36 ];
37 let ha = hash(&objeto_arbol(a).unwrap().serializar().unwrap());
38 let hb = hash(&objeto_arbol(b).unwrap().serializar().unwrap());
39 assert_eq!(ha, hb);
40 }
41
42 #[test]
43 fn arbol_rechaza_version_desconocida() {
44 let mut arbol = Arbol { version: VERSION_ARBOL, entradas: vec![] };
45 assert!(Arbol::deserializar(&arbol.serializar().unwrap()).is_ok());
46 arbol.version = 99;
47 assert!(Arbol::deserializar(&arbol.serializar().unwrap()).is_err());
48 }
49
50 #[test]
51 fn indice_de_blob_grande_tiene_datos_vacio_e_hijos() {
52 let idx = objeto_blob_indice(vec![[1; 32], [2; 32], [3; 32]]);
53 assert!(idx.datos.is_empty(), "el índice no porta datos, solo hijos");
54 assert_eq!(idx.hijos, vec![[1u8; 32], [2u8; 32], [3u8; 32]]);
55 // Distinguible de un archivo vacío (blob plano): hijos no vacío.
56 let vacio = objeto_blob(vec![]);
57 assert!(vacio.hijos.is_empty());
58 }
59
60 #[test]
61 fn modo_es_archivo_distingue_contenido_de_estructura() {
62 assert!(ModoEntrada::Archivo.es_archivo());
63 assert!(ModoEntrada::Ejecutable.es_archivo());
64 assert!(!ModoEntrada::Symlink.es_archivo());
65 assert!(!ModoEntrada::Directorio.es_archivo());
66 }
67
68 #[test]
69 fn modos_nuevos_sobreviven_round_trip_en_arbol() {
70 let entradas = vec![
71 EntradaArbol { nombre: "run.sh".into(), modo: ModoEntrada::Ejecutable, hash: [1; 32] },
72 EntradaArbol { nombre: "link".into(), modo: ModoEntrada::Symlink, hash: [2; 32] },
73 ];
74 let obj = objeto_arbol(entradas).unwrap();
75 let arbol = Arbol::deserializar(&obj.datos).unwrap();
76 assert_eq!(arbol.entradas[0].nombre, "link");
77 assert_eq!(arbol.entradas[0].modo, ModoEntrada::Symlink);
78 assert_eq!(arbol.entradas[1].modo, ModoEntrada::Ejecutable);
79 }
80
81 #[test]
82 fn objeto_ida_y_vuelta() {
83 let objeto = Objeto {
84 datos: vec![1, 2, 3, 4, 5],
85 hijos: vec![[7u8; 32], [9u8; 32]],
86 };
87 let bytes = objeto.serializar().unwrap();
88 assert_eq!(Objeto::deserializar(&bytes).unwrap(), objeto);
89 }
90
91 #[test]
92 fn registro_alineado_a_sector() {
93 let payload = vec![0xABu8; 600];
94 let registro = componer_registro(&payload);
95 // 4 + 600 = 604 bytes => dos sectores de 512.
96 assert_eq!(registro.len(), 2 * TAM_SECTOR);
97 assert_eq!(registro.len() % TAM_SECTOR, 0);
98 assert_eq!(longitud_registro(®istro), Some(600));
99 assert_eq!(®istro[4..604], &payload[..]);
100 }
101
102 #[test]
103 fn cuaderno_ida_y_vuelta_con_celdas_mixtas() {
104 // FASE 43 :: el modelo unificado CeldaWawa empaqueta los cinco
105 // campos en una sola struct. Roundtrip cubre:
106 // - celda exitosa con binario y retorno legitimo
107 // - celda fallida sin binario, sin retorno, con `marca_error`
108 // - celda fallida con binario pero retorno negativo y error
109 let celdas: Vec<CeldaWawa> = vec![
110 CeldaWawa {
111 id_secuencial: 0,
112 fuente_hash: [0xA1; 32],
113 binario_hash: Some([0xB2; 32]),
114 ultimo_retorno: Some(42),
115 marca_error: false,
116 },
117 CeldaWawa {
118 id_secuencial: 1,
119 fuente_hash: [0xC3; 32],
120 binario_hash: None,
121 ultimo_retorno: None,
122 marca_error: true,
123 },
124 CeldaWawa {
125 id_secuencial: 2,
126 fuente_hash: [0xD4; 32],
127 binario_hash: Some([0xE5; 32]),
128 ultimo_retorno: Some(-7),
129 marca_error: true,
130 },
131 ];
132 let bytes = serializar_celdas(&celdas).unwrap();
133 let leido = deserializar_celdas(&bytes).unwrap();
134 assert_eq!(leido, celdas);
135
136 // Single-cell payload (el caso que produce la PRIMERA anexion
137 // de `sys_cuaderno_anexar_celda` sobre un cuaderno virgen).
138 let una: Vec<CeldaWawa> = vec![CeldaWawa {
139 id_secuencial: 99,
140 fuente_hash: [0xF0; 32],
141 binario_hash: None,
142 ultimo_retorno: Some(0),
143 marca_error: false,
144 }];
145 let bytes = serializar_celdas(&una).unwrap();
146 let leido = deserializar_celdas(&bytes).unwrap();
147 assert_eq!(leido, una);
148 }
149
150 #[test]
151 fn cuaderno_acumulativo_anexa_celdas_en_orden() {
152 // FASE 47 :: la nueva syscall `sys_cuaderno_anexar_celda` opera
153 // en el kernel como: recuperar -> deserializar Vec<CeldaWawa> ->
154 // push(nueva) -> reserializar. Este test reproduce esa cadena
155 // en miniatura, asegurando que el roundtrip respeta el orden
156 // cronologico real con id_secuencial creciente.
157 let mut acumulado: Vec<CeldaWawa> = Vec::new();
158 for i in 0..5u32 {
159 // Re-deserializar lo que el kernel "tendria en disco" antes
160 // del push — refleja exactamente la operacion del host.
161 let acumulado_actual = if acumulado.is_empty() {
162 Vec::new()
163 } else {
164 let bytes = serializar_celdas(&acumulado).unwrap();
165 deserializar_celdas(&bytes).unwrap()
166 };
167 let mut siguiente = acumulado_actual;
168 siguiente.push(CeldaWawa {
169 id_secuencial: i,
170 fuente_hash: [i as u8; 32],
171 binario_hash: if i % 2 == 0 {
172 Some([(i + 0x10) as u8; 32])
173 } else {
174 None
175 },
176 ultimo_retorno: Some(i as i32),
177 marca_error: i % 3 == 0,
178 });
179 acumulado = siguiente;
180 }
181 // Tras 5 anexiones, el cuaderno tiene 5 celdas en orden 0..5.
182 assert_eq!(acumulado.len(), 5);
183 for (i, c) in acumulado.iter().enumerate() {
184 assert_eq!(c.id_secuencial, i as u32);
185 }
186 // Roundtrip final del vector acumulado preserva la cadena.
187 let bytes = serializar_celdas(&acumulado).unwrap();
188 let leido = deserializar_celdas(&bytes).unwrap();
189 assert_eq!(leido, acumulado);
190 }
191
192 #[test]
193 fn cabecera_a_cero_es_fin_del_log() {
194 assert_eq!(longitud_registro(&[0, 0, 0, 0]), None);
195 assert_eq!(longitud_registro(&[0xFF, 0xFF, 0xFF, 0xFF]), None);
196 assert_eq!(longitud_registro(&[3, 0, 0, 0]), Some(3));
197 }
198
199 #[test]
200 fn manifiesto_rechaza_version_ajena() {
201 let mut manifiesto = Manifiesto {
202 version: 99,
203 apps: Vec::new(),
204 configuracion: None,
205 overlay_revocacion: None,
206 marco: None,
207 };
208 let bytes = postcard::to_allocvec(&manifiesto).unwrap();
209 assert!(Manifiesto::deserializar(&bytes).is_err());
210 manifiesto.version = VERSION_MANIFIESTO;
211 assert!(Manifiesto::deserializar(&manifiesto.serializar().unwrap()).is_ok());
212 }
213
214 #[test]
215 fn manifiesto_transporta_enlace_de_configuracion() {
216 // Un manifiesto puede nacer sin configuracion (defecto) o cargar el
217 // hash de un nodo de configuracion en el grafo. Lo que el `serializar`
218 // escribe es exactamente lo que el `deserializar` recupera.
219 let con_enlace = Manifiesto {
220 version: VERSION_MANIFIESTO,
221 apps: Vec::new(),
222 configuracion: Some([0xC5; 32]),
223 overlay_revocacion: None,
224 marco: None,
225 };
226 let bytes = con_enlace.serializar().unwrap();
227 let leido = Manifiesto::deserializar(&bytes).unwrap();
228 assert_eq!(leido.configuracion, Some([0xC5; 32]));
229
230 let sin_enlace = Manifiesto {
231 version: VERSION_MANIFIESTO,
232 apps: Vec::new(),
233 configuracion: None,
234 overlay_revocacion: None,
235 marco: None,
236 };
237 let bytes = sin_enlace.serializar().unwrap();
238 assert!(Manifiesto::deserializar(&bytes)
239 .unwrap()
240 .configuracion
241 .is_none());
242 }
243
244 #[test]
245 fn configuracion_ida_y_vuelta_y_rechaza_version() {
246 let cfg = Configuracion {
247 version: VERSION_CONFIGURACION,
248 idioma: idioma_iso639(*b"qu"),
249 paleta: [
250 0x11, 0x22, 0x33, 0xFF, 0x44, 0x55, 0x66, 0xFF, 0x77, 0x88, 0x99, 0xFF, 0xAA, 0xBB,
251 0xCC, 0xFF, 0xDD, 0xEE, 0xFF, 0xFF,
252 ],
253 };
254 let bytes = cfg.serializar().unwrap();
255 assert_eq!(Configuracion::deserializar(&bytes).unwrap(), cfg);
256
257 // Hashes distintos => identidades distintas. Cambiar la paleta o el
258 // idioma engendra un nodo nuevo del grafo; ningun cambio se cuela
259 // bajo el mismo hash.
260 let mut otro = cfg;
261 otro.idioma = idioma_iso639(*b"en");
262 assert_ne!(hash(&otro.serializar().unwrap()), hash(&bytes));
263
264 // Version desconocida: se rechaza al deserializar.
265 let mut ajeno = cfg;
266 ajeno.version = 99;
267 let bytes_ajenos = postcard::to_allocvec(&ajeno).unwrap();
268 assert!(Configuracion::deserializar(&bytes_ajenos).is_err());
269 }
270
271 #[test]
272 fn configuracion_por_defecto_es_estable() {
273 // El `por_defecto` debe ser determinista y reconstruirse desde su
274 // forma binaria sin perder ningun campo. El kernel lo inyecta tal
275 // cual cuando el manifiesto no enlaza configuracion alguna.
276 let defecto = Configuracion::por_defecto();
277 assert_eq!(defecto.version, VERSION_CONFIGURACION);
278 assert_eq!(defecto.idioma, IDIOMA_DEFECTO);
279 assert_eq!(defecto.paleta, PALETA_DEFECTO);
280 let bytes = defecto.serializar().unwrap();
281 assert_eq!(Configuracion::deserializar(&bytes).unwrap(), defecto);
282 }
283
284 #[test]
285 fn entrada_app_transporta_permisos_y_distingue_hash() {
286 // Una entrada con permisos distintos engendra un manifiesto con un
287 // hash distinto: el bit es CONTENIDO direccionado, no metadato lateral.
288 // Una app que se "regala" un permiso a si misma no puede pasar por
289 // la misma app del manifiesto anterior — el grafo lo delata.
290 let base = EntradaApp {
291 nombre: String::from("test"),
292 bytecode: [0x11; 32],
293 region_x: 0,
294 region_y: 0,
295 region_ancho: 100,
296 region_alto: 100,
297 techo_memoria: 4 * 1024 * 1024,
298 fuel_fotograma: 1_000_000,
299 estado: None,
300 permisos: 0,
301 concesion: None,
302 };
303 let mut con_red = base.clone();
304 con_red.permisos = PERMISO_RED;
305 let manifiesto_a = Manifiesto {
306 version: VERSION_MANIFIESTO,
307 apps: vec![base.clone()],
308 configuracion: None,
309 overlay_revocacion: None,
310 marco: None,
311 };
312 let manifiesto_b = Manifiesto {
313 version: VERSION_MANIFIESTO,
314 apps: vec![con_red],
315 configuracion: None,
316 overlay_revocacion: None,
317 marco: None,
318 };
319 assert_ne!(
320 hash(&manifiesto_a.serializar().unwrap()),
321 hash(&manifiesto_b.serializar().unwrap()),
322 "manifiestos con distintos permisos deben dar hashes distintos"
323 );
324
325 // El roundtrip preserva la mascara entera.
326 let con_todo = EntradaApp {
327 permisos: PERMISO_RED
328 | PERMISO_GRAFO_ESCRITURA
329 | PERMISO_RAIZ
330 | PERMISO_ALTAVOZ
331 | PERMISO_CONFIG
332 | PERMISO_COMPACTAR,
333 ..base.clone()
334 };
335 let m = Manifiesto {
336 version: VERSION_MANIFIESTO,
337 apps: vec![con_todo],
338 configuracion: None,
339 overlay_revocacion: None,
340 marco: None,
341 };
342 let bytes = m.serializar().unwrap();
343 let leido = Manifiesto::deserializar(&bytes).unwrap();
344 assert_eq!(leido.apps[0].permisos, 0b111111);
345 }
346
347 #[test]
348 fn manifiesto_firmado_ida_y_vuelta() {
349 // Roundtrip serializar->deserializar preserva los tres campos del
350 // sobre criptografico: hash del manifiesto, llave publica del autor
351 // y firma. Es el contrato basico de la Fase 25 con el wire/log.
352 let mf = ManifiestoFirmado {
353 manifiesto_hash: [0xC5; 32],
354 autor: [0xA1; 32],
355 firma: [0x77; 64],
356 };
357 let bytes = mf.serializar().unwrap();
358 let leido = ManifiestoFirmado::deserializar(&bytes).unwrap();
359 assert_eq!(leido, mf);
360 // Tamaño acotado: 32 + 32 + 64 = 128 bytes crudos + el preludio
361 // postcard. Debe caber holgado en un sector y en un frame Ethernet.
362 assert!(bytes.len() <= 160, "MF demasiado grande: {} bytes", bytes.len());
363 }
364
365 #[test]
366 fn cuaderno_firmado_ida_y_vuelta() {
367 // Roundtrip estructural del sobre criptografico del cuaderno
368 // (Fase 37). Gemelo a `manifiesto_firmado_ida_y_vuelta` — el
369 // mismo contrato de los tres campos contra el wire/log.
370 let cf = CuadernoFirmado {
371 cuaderno_raiz_hash: [0xCE; 32],
372 autor: [0xA1; 32],
373 firma: [0x66; 64],
374 };
375 let bytes = cf.serializar().unwrap();
376 let leido = CuadernoFirmado::deserializar(&bytes).unwrap();
377 assert_eq!(leido, cf);
378 assert!(
379 bytes.len() <= 160,
380 "CuadernoFirmado demasiado grande: {} bytes",
381 bytes.len()
382 );
383 }
384
385 #[test]
386 fn codigo_error_tiene_valores_estables() {
387 // Anadir una variante NUEVA al enum jamas debe renumerar las
388 // existentes: el binario WASM viejo compila contra el numero
389 // literal y kernel + userspace tienen que coincidir aunque el
390 // catalogo crezca. Este test es el contrato.
391 assert_eq!(CodigoError::Ok.como_i32(), 0);
392 assert_eq!(CodigoError::Ausente.como_i32(), -1);
393 assert_eq!(CodigoError::CapacidadInsuficiente.como_i32(), -2);
394 assert_eq!(CodigoError::AlmacenamientoFallo.como_i32(), -3);
395 assert_eq!(CodigoError::SinFoco.como_i32(), -4);
396 assert_eq!(CodigoError::EnvioFallo.como_i32(), -5);
397 assert_eq!(CodigoError::Saturado.como_i32(), -6);
398 assert_eq!(CodigoError::PayloadInvalido.como_i32(), -7);
399 }
400
401 #[test]
402 fn idioma_iso639_empaqueta_en_little_endian() {
403 // `es` => 'e' (0x65) en el byte bajo, 's' (0x73) en el alto.
404 assert_eq!(idioma_iso639(*b"es"), 0x7365);
405 assert_eq!(idioma_iso639(*b"en"), 0x6E65);
406 assert_eq!(idioma_iso639(*b"qu"), 0x7571);
407 }
408
409 #[test]
410 fn canal_ida_y_vuelta_con_dos_raices() {
411 let canal = Canal {
412 version: VERSION_CANAL,
413 nombre: String::from("estable"),
414 autor: [0xA1; 32],
415 raices: vec![
416 RaizFirmada {
417 timestamp: 1_700_000_000,
418 raiz_manifiesto: [0x11; 32],
419 firma: [0x22; 64],
420 },
421 RaizFirmada {
422 timestamp: 1_700_000_100,
423 raiz_manifiesto: [0x33; 32],
424 firma: [0x44; 64],
425 },
426 ],
427 };
428 let bytes = canal.serializar().unwrap();
429 let recuperado = Canal::deserializar(&bytes).unwrap();
430 assert_eq!(recuperado, canal);
431 // `vigente` devuelve la ultima entrada por orden, no la mas reciente
432 // por timestamp — el contrato es que las entradas vienen ordenadas;
433 // verificarlo es responsabilidad de quien construye el canal.
434 assert_eq!(recuperado.vigente().unwrap().raiz_manifiesto, [0x33; 32]);
435 }
436
437 #[test]
438 fn canal_rechaza_version_y_nombre_excedido() {
439 let mut canal = Canal {
440 version: 99,
441 nombre: String::from("dev"),
442 autor: [0; 32],
443 raices: Vec::new(),
444 };
445 let bytes = postcard::to_allocvec(&canal).unwrap();
446 assert!(Canal::deserializar(&bytes).is_err());
447 canal.version = VERSION_CANAL;
448 assert!(Canal::deserializar(&canal.serializar().unwrap()).is_ok());
449
450 // Nombre excedido: el serializador lo veta sin escribir nada al disco.
451 let largo = Canal {
452 version: VERSION_CANAL,
453 nombre: "x".repeat(NOMBRE_CANAL_LIMITE + 1),
454 autor: [0; 32],
455 raices: Vec::new(),
456 };
457 assert!(largo.serializar().is_err());
458 }
459
460 #[test]
461 fn mensaje_a_firmar_es_canonico_y_distingue_canales() {
462 let raiz: Hash = [0x55; 32];
463 let m1 = mensaje_a_firmar("estable", 42, &raiz);
464 let m2 = mensaje_a_firmar("estable", 42, &raiz);
465 assert_eq!(m1, m2, "el mensaje firmable debe ser deterministico");
466
467 // Cambiar el canal cambia el mensaje: una firma valida en `dev` no se
468 // replica en `estable`.
469 let m3 = mensaje_a_firmar("dev", 42, &raiz);
470 assert_ne!(m1, m3);
471
472 // Cambiar el timestamp tambien — no se replica una recomendacion vieja
473 // como si fuera nueva.
474 let m4 = mensaje_a_firmar("estable", 43, &raiz);
475 assert_ne!(m1, m4);
476 }
477
478 #[test]
479 fn mensaje_capacidad_es_canonico_y_distingue_bytecode_y_permisos() {
480 let bc: Hash = [0xAB; 32];
481 let m1 = mensaje_capacidad(&bc, PERMISO_RED);
482 assert_eq!(m1, mensaje_capacidad(&bc, PERMISO_RED), "deterministico");
483 // Layout: bytecode(32) || permisos_le(4).
484 assert_eq!(&m1[..32], &bc);
485 assert_eq!(&m1[32..], &PERMISO_RED.to_le_bytes());
486
487 // Distinto bytecode => distinto mensaje: una concesion no se transplanta.
488 let otro: Hash = [0xCD; 32];
489 assert_ne!(m1, mensaje_capacidad(&otro, PERMISO_RED));
490 // Distintos permisos => distinto mensaje: subir un bit invalida la firma.
491 assert_ne!(m1, mensaje_capacidad(&bc, PERMISO_RED | PERMISO_RAIZ));
492 }
493
494 #[test]
495 fn overlay_revocacion_roundtrip_y_rechaza_version_ajena() {
496 let overlay = OverlayRevocacion {
497 version: VERSION_OVERLAY,
498 revocaciones: vec![RevocacionFirmada {
499 objetivo: [0x42; 32],
500 motivo: 0, // Compromised
501 emitida_en: 1_700_000_000,
502 vence_en: None,
503 firmantes: vec![
504 FirmaRevocacion { autor: [0x10; 32], firma: [0xAA; 64] },
505 FirmaRevocacion { autor: [0x11; 32], firma: [0xBB; 64] },
506 ],
507 }],
508 };
509 let bytes = overlay.serializar().unwrap();
510 let leido = OverlayRevocacion::deserializar(&bytes).unwrap();
511 assert_eq!(leido, overlay);
512 assert_eq!(leido.revocaciones[0].firmantes.len(), 2);
513
514 // Un overlay con versión ajena se rechaza, no se malinterpreta.
515 let ajeno = OverlayRevocacion { version: 99, revocaciones: Vec::new() };
516 let bytes = postcard::to_allocvec(&ajeno).unwrap();
517 assert!(OverlayRevocacion::deserializar(&bytes).is_err());
518 }
519
520 #[test]
521 fn mensaje_rotacion_clave_layout_y_dominio() {
522 let vieja = [0x11; 32];
523 let nueva = [0x22; 32];
524 let m = mensaje_rotacion_clave(&vieja, &nueva, 0x0A0B0C0D);
525 // Layout: DOM || old(32) || new(32) || issued_at_le(8).
526 assert_eq!(&m[..DOM_ROTACION_CLAVE.len()], DOM_ROTACION_CLAVE);
527 let p = DOM_ROTACION_CLAVE.len();
528 assert_eq!(&m[p..p + 32], &vieja);
529 assert_eq!(&m[p + 32..p + 64], &nueva);
530 assert_eq!(&m[p + 64..], &0x0A0B0C0Du64.to_le_bytes());
531 // Distinto timestamp => distinto canonico (no se revive una rotacion vieja).
532 assert_ne!(m, mensaje_rotacion_clave(&vieja, &nueva, 0x0A0B0C0E));
533 }
534
535 #[test]
536 fn mensaje_revocacion_clave_distingue_motivo_y_no_colisiona_none_some_cero() {
537 let target = [0x99; 32];
538 // El motivo entra en el canonico: no se "asciende" un retiro a compromiso.
539 let comprometida = mensaje_revocacion_clave(&target, 0, 5, None);
540 let retirada = mensaje_revocacion_clave(&target, 1, 5, None);
541 assert_ne!(comprometida, retirada);
542 // Layout permanente: DOM || target(32) || [motivo] || issued_le(8) || 0.
543 let p = DOM_REVOCACION_CLAVE.len();
544 assert_eq!(&comprometida[..p], DOM_REVOCACION_CLAVE);
545 assert_eq!(&comprometida[p..p + 32], &target);
546 assert_eq!(comprometida[p + 32], 0u8);
547 assert_eq!(&comprometida[p + 33..p + 41], &5u64.to_le_bytes());
548 assert_eq!(*comprometida.last().unwrap(), 0u8); // tag None
549 // `None` y `Some(0)` no colisionan: el tag los separa.
550 let none = mensaje_revocacion_clave(&target, 1, 5, None);
551 let some_cero = mensaje_revocacion_clave(&target, 1, 5, Some(0));
552 assert_ne!(none, some_cero);
553 assert_eq!(*some_cero.last().unwrap(), 0u8); // ultimo byte de 0u64 LE
554 assert_eq!(some_cero[p + 41], 1u8); // tag Some
555 }
556
557 #[test]
558 fn concesion_capacidad_roundtrip() {
559 let c = ConcesionCapacidad {
560 bytecode: [0x11; 32],
561 permisos: PERMISO_RED | PERMISO_RAIZ,
562 autor: [0x22; 32],
563 firma: [0x33; 64],
564 };
565 let bytes = c.serializar().unwrap();
566 let vuelta = ConcesionCapacidad::deserializar(&bytes).unwrap();
567 assert_eq!(c, vuelta);
568 }
569
570 #[test]
571 fn permisos_efectivos_es_la_interseccion() {
572 // El manifiesto pide RED|RAIZ pero la concesion solo autoriza RED:
573 // efectivos = RED. El manifiesto no puede escalar a RAIZ por su cuenta.
574 let declarados = PERMISO_RED | PERMISO_RAIZ;
575 let concedidos = PERMISO_RED;
576 assert_eq!(permisos_efectivos(declarados, concedidos), PERMISO_RED);
577 // Concesion generosa, manifiesto modesto: efectivos = lo que el
578 // manifiesto pidio (no enciende lo que no se declaro).
579 assert_eq!(
580 permisos_efectivos(PERMISO_RED, PERMISO_RED | PERMISO_ALTAVOZ),
581 PERMISO_RED
582 );
583 // Sin concesion (concedidos=0): cero capacidades gateadas.
584 assert_eq!(permisos_efectivos(declarados, 0), 0);
585 }
586
587 #[test]
588 fn superbloque_cabe_en_un_sector_y_vuelve_intacto() {
589 let sb = SuperBloque {
590 magia: MAGIA,
591 version: VERSION_SUPERBLOQUE,
592 log_inicio: 1,
593 cursor: 4096,
594 raiz: Some([1u8; 32]),
595 manifiesto: Some([2u8; 32]),
596 };
597 let bytes = sb.serializar().unwrap();
598 assert!(bytes.len() <= TAM_SECTOR);
599 assert_eq!(SuperBloque::deserializar(&bytes).unwrap(), sb);
600 }
601
602 #[test]
603 fn test_wawa_ecosystem_immutable_vanguard() {
604 // =====================================================================
605 // FASE 50 :: VANGUARDIA INMUTABLE DEL ABI WAWA
606 // ---------------------------------------------------------------------
607 // Sello de cierre del Manifiesto Tecnico. La firma numerica de las
608 // ocho variantes licitas de `CodigoError` —el lenguaje compartido
609 // entre el kernel Ring 0, los modulos WASM Ring 3 y el explorador
610 // host-side— ha quedado fijada. Este test la consagra:
611 //
612 // * Cada variante tiene su valor i32 FIJO en el orden negociado
613 // a lo largo de las primeras 49 fases. Renumerar una existente
614 // seria romper, byte a byte, todo binario Ring 3 ya inscrito
615 // en el grafo direccionado por contenido.
616 //
617 // * La conversion `as i32` y la `const fn como_i32` son gemelas:
618 // ambas extraen el discriminante `#[repr(i32)]` —sin trampa,
619 // sin tabla auxiliar—.
620 //
621 // * El catalogo permanece de cardinalidad ocho: ni una variante
622 // menos (siempre Ok=0 + siete fallas controladas), ni una mas
623 // escondida tras renumeracion. Anadir una NUEVA codifica un
624 // valor entero NUEVO; el contrato no se rompe.
625 //
626 // Quien pretenda extender el catalogo en una fase futura debera,
627 // ANTES de mover una variante, actualizar esta tabla de cierre
628 // y aceptar que el wire del ecosistema entero ha cambiado de era.
629 // =====================================================================
630
631 // 1. Firma numerica congelada de la vanguardia (Ok + 7 fallas).
632 const VANGUARDIA: [(CodigoError, i32); 8] = [
633 (CodigoError::Ok, 0),
634 (CodigoError::Ausente, -1),
635 (CodigoError::CapacidadInsuficiente, -2),
636 (CodigoError::AlmacenamientoFallo, -3),
637 (CodigoError::SinFoco, -4),
638 (CodigoError::EnvioFallo, -5),
639 (CodigoError::Saturado, -6),
640 (CodigoError::PayloadInvalido, -7),
641 ];
642 for &(variante, valor) in VANGUARDIA.iter() {
643 assert_eq!(
644 variante.como_i32(),
645 valor,
646 "ABI roto: {:?} dejo de valer {} — mutacion accidental detectada",
647 variante,
648 valor,
649 );
650 // `as i32` directo: el `#[repr(i32)]` fija el discriminante en
651 // ambos caminos —el const fn y el cast— sin tabla auxiliar.
652 assert_eq!(variante as i32, valor);
653 }
654
655 // 2. La proyeccion debe ser inyectiva: dos variantes distintas no
656 // pueden compartir su valor i32 — el catalogo de la vanguardia
657 // no tolera colisiones.
658 for i in 0..VANGUARDIA.len() {
659 for j in (i + 1)..VANGUARDIA.len() {
660 assert_ne!(
661 VANGUARDIA[i].1, VANGUARDIA[j].1,
662 "ABI roto: dos variantes comparten valor i32"
663 );
664 }
665 }
666
667 // 3. Cardinalidad inmutable: 1 (Ok) + 7 fallas controladas. Cualquier
668 // fase que pretenda crecer este catalogo debe actualizar el test
669 // explicitamente; un cambio silencioso se delata aqui.
670 assert_eq!(
671 VANGUARDIA.len(),
672 8,
673 "ABI roto: cardinalidad del catalogo CodigoError mutada"
674 );
675
676 // 4. Rango cerrado de fallas en [-7, -1]. La cascada de Pluma
677 // (apps/pluma) y el dispatcher Ring 0 cuentan con este rango
678 // EXACTO para distinguir codigos de error de retornos legitimos.
679 let fallas_min = VANGUARDIA.iter().skip(1).map(|&(_, v)| v).min().unwrap();
680 let fallas_max = VANGUARDIA.iter().skip(1).map(|&(_, v)| v).max().unwrap();
681 assert_eq!(fallas_min, -7, "ABI roto: el suelo de fallas se desplazo");
682 assert_eq!(fallas_max, -1, "ABI roto: el techo de fallas se desplazo");
683 }
684
685 #[test]
686 fn superbloque_porta_log_inicio_distinto_de_uno() {
687 // Tras una compactacion semantica, `log_inicio` no es 1: apunta al
688 // sector donde empieza el segmento limpio recien escrito. El
689 // superbloque sigue cabiendo en su sector y el roundtrip preserva
690 // el campo: el GC depende de esa simetria.
691 let sb = SuperBloque {
692 magia: MAGIA,
693 version: VERSION_SUPERBLOQUE,
694 log_inicio: 32_768,
695 cursor: 33_500,
696 raiz: Some([0xAA; 32]),
697 manifiesto: Some([0xBB; 32]),
698 };
699 let bytes = sb.serializar().unwrap();
700 assert!(bytes.len() <= TAM_SECTOR);
701 let leido = SuperBloque::deserializar(&bytes).unwrap();
702 assert_eq!(leido.log_inicio, 32_768);
703 assert_eq!(leido.cursor, 33_500);
704 }
705
706 // === Fase 60: MensajeAsistente ===
707
708 #[test]
709 fn mensaje_asistente_consulta_ida_y_vuelta() {
710 let msg = MensajeAsistente::Consulta {
711 id: 0xDEADBEEF,
712 prompt: "lanza pluma".into(),
713 contexto: Contexto {
714 apps: vec!["pluma".into(), "bitacora".into()],
715 manifiesto_actual: Some([0x11; 32]),
716 configuracion_activa: None,
717 },
718 };
719 let bytes = msg.serializar().unwrap();
720 let leido = MensajeAsistente::deserializar(&bytes).unwrap();
721 assert_eq!(leido, msg);
722 }
723
724 #[test]
725 fn mensaje_asistente_propuesta_lanzar_app() {
726 let msg = MensajeAsistente::Propuesta {
727 id: 42,
728 accion: AccionPropuesta::LanzarApp { plantilla: 7 },
729 explicacion: "abre pluma para tomar notas".into(),
730 confianza: 0.95,
731 };
732 let bytes = msg.serializar().unwrap();
733 let leido = MensajeAsistente::deserializar(&bytes).unwrap();
734 assert_eq!(leido, msg);
735 }
736
737 #[test]
738 fn mensaje_asistente_propuesta_instalar_app() {
739 let msg = MensajeAsistente::Propuesta {
740 id: 100,
741 accion: AccionPropuesta::InstalarApp {
742 manifiesto_propuesto: [0xAB; 32],
743 },
744 explicacion: "manifiesto v2 firmado".into(),
745 confianza: 1.0,
746 };
747 let bytes = msg.serializar().unwrap();
748 let leido = MensajeAsistente::deserializar(&bytes).unwrap();
749 assert_eq!(leido, msg);
750 }
751
752 #[test]
753 fn mensaje_asistente_error_ida_y_vuelta() {
754 let msg = MensajeAsistente::Error {
755 id: 0,
756 motivo: "LLM rate-limited".into(),
757 };
758 let bytes = msg.serializar().unwrap();
759 let leido = MensajeAsistente::deserializar(&bytes).unwrap();
760 assert_eq!(leido, msg);
761 }
762
763 #[test]
764 fn mensaje_asistente_basura_rechazada() {
765 // Bytes arbitrarios — postcard debe rechazar sin panic.
766 let basura = [0xFFu8; 16];
767 assert!(MensajeAsistente::deserializar(&basura).is_err());
768 }
769
770 #[test]
771 fn mensaje_asistente_propuesta_notar_sin_efecto() {
772 // `Notar` permite respuestas informativas: el LLM contesta una
773 // pregunta sin proponer una accion ejecutable.
774 let msg = MensajeAsistente::Propuesta {
775 id: 1,
776 accion: AccionPropuesta::Notar {
777 texto: "tienes 3 apps abiertas en el escritorio 1".into(),
778 },
779 explicacion: String::new(),
780 confianza: 1.0,
781 };
782 let bytes = msg.serializar().unwrap();
783 let leido = MensajeAsistente::deserializar(&bytes).unwrap();
784 assert_eq!(leido, msg);
785 }
786
787 #[test]
788 fn canal_asistente_no_choca_con_otros() {
789 // 0x4153 = "AS". Si más adelante se registran otros canales
790 // (chasqui, agora, etc.) este test recuerda el namespace
791 // ocupado. Cambiar el valor requiere actualizar el doc.
792 assert_eq!(CANAL_ASISTENTE, 0x4153);
793 assert_eq!(&CANAL_ASISTENTE.to_be_bytes(), b"AS");
794 }
795
796 #[test]
797 fn ethertype_asistente_distinto_de_akasha() {
798 // El demuxer Akasha del kernel descarta payloads que no parsean
799 // como `MensajeAkasha`. Si usaramos 0x88B5, los frames del
800 // asistente caerian como `PayloadInvalido` y se contarian en
801 // `RX_DESCARTADOS` antes de pasar al usuario. Con 0x88B6 caen
802 // en la rama `EtherTypeAjeno` que va directo al usuario.
803 assert_eq!(ETHERTYPE_ASISTENTE, 0x88B6);
804 assert_ne!(ETHERTYPE_ASISTENTE, 0x88B5);
805 }
806
807 #[test]
808 fn cabecera_cable_round_trip_consulta() {
809 let mut buf = [0u8; 32];
810 let n = escribir_cabecera_cable(&mut buf, TipoCable::Consulta, 0xDEADBEEFCAFEBABE)
811 .expect("cabe");
812 assert_eq!(n, TAM_CABECERA_CABLE);
813 let (tipo, id) = leer_cabecera_cable(&buf).expect("valida");
814 assert_eq!(tipo, TipoCable::Consulta);
815 assert_eq!(id, 0xDEADBEEFCAFEBABE);
816 }
817
818 #[test]
819 fn cabecera_cable_round_trip_propuesta_lanzar() {
820 let mut buf = [0u8; 12];
821 escribir_cabecera_cable(&mut buf, TipoCable::PropuestaLanzarApp, 7).unwrap();
822 let (tipo, id) = leer_cabecera_cable(&buf).unwrap();
823 assert_eq!(tipo, TipoCable::PropuestaLanzarApp);
824 assert_eq!(id, 7);
825 }
826
827 #[test]
828 fn cabecera_cable_rechaza_canal_ajeno() {
829 let mut buf = [0u8; 12];
830 // Forjamos una cabecera con canal distinto al asistente.
831 buf[0..2].copy_from_slice(&0xABCDu16.to_be_bytes());
832 buf[2..4].copy_from_slice(&(TipoCable::Consulta as u16).to_be_bytes());
833 assert!(leer_cabecera_cable(&buf).is_none());
834 }
835
836 #[test]
837 fn cabecera_cable_rechaza_tipo_desconocido() {
838 let mut buf = [0u8; 12];
839 buf[0..2].copy_from_slice(&CANAL_ASISTENTE.to_be_bytes());
840 buf[2..4].copy_from_slice(&999u16.to_be_bytes()); // tipo inválido
841 assert!(leer_cabecera_cable(&buf).is_none());
842 }
843
844 #[test]
845 fn cabecera_cable_rechaza_truncada() {
846 let buf = [0u8; 5];
847 assert!(leer_cabecera_cable(&buf).is_none());
848 }
849
850 #[test]
851 fn escribir_cabecera_cable_rechaza_buffer_corto() {
852 let mut buf = [0u8; 5];
853 assert!(escribir_cabecera_cable(&mut buf, TipoCable::Consulta, 0).is_none());
854 }
855
856 #[test]
857 fn tipo_cable_codigos_estables() {
858 // Si alguien renumera los discriminantes, los lectores
859 // binarios viejos rompen. Este test caza el cambio.
860 assert_eq!(TipoCable::Consulta as u16, 1);
861 assert_eq!(TipoCable::PropuestaNotar as u16, 2);
862 assert_eq!(TipoCable::PropuestaLanzarApp as u16, 3);
863 assert_eq!(TipoCable::PropuestaInstalarApp as u16, 4);
864 assert_eq!(TipoCable::PropuestaCambiarConfig as u16, 5);
865 assert_eq!(TipoCable::Error as u16, 6);
866 assert_eq!(TipoCable::RequestFirma as u16, 7);
867 assert_eq!(TipoCable::Firma as u16, 8);
868 }
869
870 #[test]
871 fn cabecera_cable_round_trip_request_firma() {
872 // Fase 60 v4 :: la app pide firma humana. Round-trip por la
873 // misma puerta — el `id` corresponde al de la propuesta original.
874 let mut buf = [0u8; 12];
875 escribir_cabecera_cable(&mut buf, TipoCable::RequestFirma, 99).unwrap();
876 let (tipo, id) = leer_cabecera_cable(&buf).unwrap();
877 assert_eq!(tipo, TipoCable::RequestFirma);
878 assert_eq!(id, 99);
879 }
880
881 #[test]
882 fn cabecera_cable_round_trip_firma() {
883 let mut buf = [0u8; 12];
884 escribir_cabecera_cable(&mut buf, TipoCable::Firma, 99).unwrap();
885 let (tipo, id) = leer_cabecera_cable(&buf).unwrap();
886 assert_eq!(tipo, TipoCable::Firma);
887 assert_eq!(id, 99);
888 }
889
890 #[test]
891 fn tipo_objeto_codigos_estables() {
892 // El primer byte del payload de RequestFirma. La app wasm y
893 // el puente leen estos numeros literalmente — renumerarlos
894 // rompe el cable.
895 assert_eq!(TIPO_OBJETO_CUADERNO, 1);
896 assert_eq!(TIPO_OBJETO_CONFIGURACION, 2);
897 }
898
899 #[test]
900 fn tipo_cable_de_u16_acepta_nuevos() {
901 assert_eq!(TipoCable::de_u16(7), Some(TipoCable::RequestFirma));
902 assert_eq!(TipoCable::de_u16(8), Some(TipoCable::Firma));
903 assert_eq!(TipoCable::de_u16(9), None);
904 }