tawasuyu

tawasuyu/mirada

Sovereign tiling Wayland compositor + window manager in Rust, on Llimphi.

Rust 50Markdown 30TOML 211282 Definiciones

⎇ 3dc85ebdcdd5fda0a1be0db5f8fbce5859266f2e shared/format/src/pruebas.rs Rust · 904 líneas · 35.8 KB Historia Quién tocó qué crudo

feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro) 3dc85ebdc · Sergio ·

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(&registro), Some(600));
99        assert_eq!(&registro[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    }