QA — Smoke visual conducido de la GUI (2026-07-31, lanzador-conversacional)
Verificación de lanzador-conversacional mirando la ventana, según el método
que estableció gui-acabado-y-cierre-sdd (docs/qa/2026-07-26-gui-acabado-smoke.md)
y repitió pulido-pre-anuncio (docs/qa/2026-07-27-pulido-pre-anuncio-smoke.md).
Esta change es casi toda superficie —vista de llegada, navegación al enviar,
plegado de burbujas, tarjetas en línea, sección de proyectos— y los tests de
cableado leen fuentes: no ven layout, ni foco, ni lo que un texto se convierte al
volver del daemon. Este smoke sí, y encontró cuatro cosas que ninguno de ellos
podía ver (§Hallazgos).
- Binario de release construido con la CLI de Tauri (
ui/node_modules/.bin/tauri build --no-bundle; nuncacargo build --release, que no activatauri/custom-protocol), conadditionalBrowserArgs: --remote-debugging-port=9444temporal entauri.conf.json, revertido y el binario reconstruido limpio al terminar: el binario publicado no expone puerto alguno. - Entorno aislado: endpoint propio (
\\.\pipe\meltemi-smoke-lanzador),MELTEMI_DATA_DIRyMELTEMI_CONFIG_DIRpropios en el scratchpad, y tres repos fixture (harbourcon reglaallow,lighthousesin reglas para que el permiso escale,sparecon--load-sessionpara el estado reanudable). El único agente esmock-agent. Nunca este repositorio, nunca agentes reales, nunca red. El daemon del mantenedor siguió corriendo intacto: el smoke arrancó y detuvo solo sus dos procesos, por PID. - Driver Node sobre el WebSocket de CDP:
Runtime.evaluatemidiendogetBoundingClientRect()y leyendo el DOM real, clicks reales sobre los nodos,Page.captureScreenshotpara la mirada humana. Más un cliente JSON-RPC mínimo sobre el pipe para contrastar lo que la ventana muestra con lo que el daemon responde — dos fuentes, no una. - Ventana 1200×800 lógicos; perfil nuevo (sin
desktop-ui.json), que es también la prueba de la vista de llegada sin vista recordada.
Resultado: lo que la change promete, medido sobre el binario
Sección titulada «Resultado: lo que la change promete, medido sobre el binario»| # | Lo prometido | Medido |
|---|---|---|
| 1 | Home conversacional como vista de llegada, enfocado | .home .composer textarea presente y document.activeElement === textarea → true, con perfil nuevo |
| 2 | Compositor al centro, sin desbordar | escenario de 760 px dentro de main (216→1200): hueco izquierdo 112 px, derecho 112 px; scrollWidth 1200 = clientWidth 1200 |
| 3 | Contexto como chips dentro del compositor | «Proyecto harbour», «Agente o perfil el del proyecto», «Modo Libre» |
| 4 | El método se declara antes de enviar | método session/start junto al botón Enviar; el chip de modo lista los tres verbos: session/start, propose, sdd/explore |
| 5 | Los menús de chip no se salen ni tapan | los tres abren hacia arriba y insideViewport → true (cajas 279×95, 278×125, 318×95) |
| 6 | Perfiles ofrecidos por su nombre de suscripción | «MA mock-agent» + pill work, «MA mock-agent» + pill personal, junto al agente subyacente |
| 7 | Elegir otro proyecto no conmuta el ámbito | chip → lighthouse; el switcher del nav y el nodo actual siguen en harbour |
| 8 | Enviar navega hacia adentro | 524 ms desde el click hasta la conversación abierta, con el turno todavía corriendo (se quedó minutos esperando un permiso) |
| 9 | Burbujas de turno | burbuja humana con la instrucción, burbuja de agente con «Filling in the proposal.», bajo el nombre del agente |
| 10 | Tarjeta de permiso en línea | tarjeta con write-proposal y opciones Allow/Reject, en su posición dentro del diálogo |
| 11 | La bandeja sigue siendo la vista completa | con la tarjeta visible, el contador de Permisos del nav marcaba 1 |
| 12 | Decidir desde la tarjeta resuelve la misma petición | tras pulsar Allow: «decidido por client · concedido», 0 controles accionables, resolved |
| 13 | La instrucción encolada no se presenta como atendida | segunda burbuja humana marcada pending con pill «encolada, aún no enviada» |
| 14 | …y se despacha como siguiente turno | al decidir el permiso: 7 → 11 eventos, la burbuja pierde el pending, aparece un segundo turno de agente y su propia tarjeta |
| 15 | El conmutador no pierde nada | cabecera «14 eventos» ↔ log de operador con 14 líneas; volver a la conversación deja los mismos ítems; el conteo está en pantalla, contable a mano |
| 16 | Estado honesto de sesión terminada | «sesión terminada y no reanudable: su registro se sigue leyendo», sin campo de envío y sin cancelar |
| 17 | …y de sesión reanudable | en el fixture con --load-session: «la sesión terminó: enviar la reanuda como sesión nueva enlazada», botón Reanudar |
| 18 | Sección Proyectos permanente con sesiones anidadas | «PROYECTOS» en el sidebar sin abrir modal alguno; 3 nodos con sus sesiones debajo, cada uno con «Nueva sesión en X» y «Olvidar X (solo del listado)» |
| 19 | «Abrir carpeta…» abre el diálogo nativo | ventana Win32 clase #32770, título «Select Folder», 768×480, del proceso meltemi-desktop — no un modal del webview; cancelarla no dio de alta nada |
| 20 | El alta por contrato llega al nav sin reiniciar | project/register desde fuera de la app → el nodo aparece en el árbol |
| 21 | El texto de la baja dice lo que no hace | «No borra nada del disco: la carpeta queda intacta, sus sesiones se siguen listando, sus registros se siguen leyendo y la contabilidad local las sigue contando. Reaparecerá en cuanto se vuelva a usar o a dar de alta.» |
Nada estimado: cada cifra sale de getBoundingClientRect(), del texto renderizado
o de la respuesta del daemon por el pipe.
Hallazgos
Sección titulada «Hallazgos»Cuatro cosas que los tests de cableado no podían ver. Ninguna se arregló aquí: el rumbo de estructura dice que lo que surge se anota como propuesta futura y no se cuela en la change activa. Van con su evidencia y su ubicación exacta.
H1 — El texto no ASCII llega corrupto al agente (grave, previo a esta change)
Sección titulada «H1 — El texto no ASCII llega corrupto al agente (grave, previo a esta change)»Se escribe acción íntegra ñandú (20 caracteres) y el registro guarda
acción Ãntegra ñandú (24). No es un problema de pintado: la burbuja
muestra la corrupción porque el prompt ya está corrupto, y ese prompt es el que
recibe el agente.
Aislado hasta el fondo, sin GUI y sin shell de por medio: un cliente JSON-RPC
mínimo sobre el pipe manda session/start con la cadena correcta y session/log
la devuelve doble-codificada. La causa está en
core/meltemid/src/repo_map.rs:120, dentro de expand_refs:
out.push(bytes[i] as char); // un byte UTF-8 → un code point Latin-1Cada byte de un carácter multibyte se convierte en su propio carácter. Afecta a
todo prompt que pasa por la expansión de @ — propose, los verbos del
método, la sesión libre y la dirección —, es decir a cualquier usuario que
escriba en español, francés, alemán o con emoji. Es anterior a esta change
(gestion-contexto-repo), y esta change es la que lo vuelve visible: ahora el
compositor es la puerta principal y el transcript le devuelve al usuario su
propia frase.
La corrección es de tres líneas —empujar el carácter completo en la frontera y
avanzar len_utf8()— y merece su change, su escenario y su test: el escenario que
falta es exactamente «una instrucción con acentos llega idéntica al agente».
H2 — Olvidar un proyecto con sesiones no lo quita del árbol
Sección titulada «H2 — Olvidar un proyecto con sesiones no lo quita del árbol»Pulsar «Olvidar lighthouse», confirmar, y el nodo sigue ahí. El daemon hizo
su parte: project/list pasó de tres proyectos a dos y sus sesiones siguen
listándose (medido por el pipe en el mismo instante). Lo que ocurre es del
cliente: desktop/ui/src/lib/tree.ts:81 crea un nodo inferido para toda
sesión cuya raíz no case con un proyecto registrado, y el sidebar no distingue
los inferidos de los registrados. El usuario ve un aviso de éxito y ningún
cambio.
No se arregla aquí porque la respuesta no es obvia y es de producto: esconder el
nodo deja sesiones sin casa en el árbol —lo contrario de lo que el nodo inferido
existe para evitar (multiproyecto-suscripciones)—. Las salidas plausibles son
marcar el nodo inferido como tal (glifo y palabra, como el proyecto ausente) o
que el olvido sea explícito sobre la vista. Decisión del mantenedor.
H3 — Con un permiso pendiente, la posición en la cola no se muestra
Sección titulada «H3 — Con un permiso pendiente, la posición en la cola no se muestra»La línea de estado del compositor tiene un solo hueco y su cadena de {:else if}
pone el permiso por delante: mientras la sesión espera una decisión se lee «la
sesión espera tu decisión sobre permission» y no «encolada (posición 1)». La
promesa no se rompe —la instrucción encolada se ve en el transcript como burbuja
pendiente, con su pill, y jamás como atendida—, pero la posición, que es el dato
que la spec nombra, no está en pantalla en ese caso. Añadido menor: la palabra
que se muestra es permission y no el nombre de la herramienta esperada.
H4 — Tras una tarjeta de permiso, el cierre del turno cae como línea neutra
Sección titulada «H4 — Tras una tarjeta de permiso, el cierre del turno cae como línea neutra»El plegado cierra el turno del agente al llegar una tarjeta (para que la tarjeta
quede en su posición), así que el turn_completed posterior no encuentra turno
abierto y se renderiza como línea de sistema: ■ turn_completed {"stopReason":"completed"}. El motivo se ve —nada se esconde, que es lo que la
spec exige—, pero no cierra la burbuja. Debajo hay algo más de fondo: el daemon
emite TurnCompleted una vez por sesión, desde session_finalize, de modo
que los turnos intermedios de una sesión con instrucciones encoladas no tienen
evento de cierre en absoluto. El plegado es fiel al registro; el registro es el
que no marca cada turno.
H5 — El chip de agente puede mostrar el catálogo de la máquina, no el del proyecto
Sección titulada «H5 — El chip de agente puede mostrar el catálogo de la máquina, no el del proyecto»Con el registro de flota declarado en el proyecto, el chip listaba los
agentes reales de la máquina en vez del agente del fixture. El daemon no tiene la
culpa, y se comprobó preguntándoselo: fleet/list con projectRoot devuelve
["mock-agent","work","personal"] y sin él devuelve el catálogo embebido de diez
entradas. Lo que pasa es que el primer fleet/list del cliente sale antes de
que el ámbito de proyecto se resuelva (Home.svelte lo pide en su onMount;
App.svelte resuelve el proyecto tras dos await) y nada vuelve a pedirlo. El
smoke siguió con el registro declarado a nivel de usuario, donde el chip muestra
lo correcto (fila 6 de la tabla).
Deuda declarada
Sección titulada «Deuda declarada»Igual que en los dos precedentes: el smoke es manual y por release. Convertirlo en gate de CI —arrancar la app, recorrer las vistas, afirmar las invariantes— sigue fuera de alcance y sigue apuntado. Las capturas de esta corrida (home, chip de modo, conversación, log de operador) quedaron en el registro de la evaluación; no se publican porque contienen rutas del sistema de archivos de la máquina donde se corrió.