Files
egutierrez 618e3b0295 chore: auto-commit (13 archivos)
- CAPABILITIES_TODO.md
- demo_e2e/RESUMEN.md
- demo_e2e/results/prueba_1_quotes.json
- demo_e2e/results/prueba_2_perceive.json
- demo_e2e/results/prueba_3_search.json
- demo_e2e/results/prueba_4_login_session.json
- demo_e2e/results/prueba_5_books.json
- demo_e2e/results/prueba_6_session_storage.json
- demo_e2e/results/prueba_7_find_honesto.json
- demo_e2e/results/prueba_8_verificacion.json
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 13:20:36 +02:00

150 lines
9.9 KiB
Markdown

# Ejercicio e2e — validación de capacidades del browser_mcp
Fecha: 06/06/2026. Objetivo: comprobar, ejecutando de verdad contra sitios reales, que el servidor
`browser_mcp` (control de navegador vía CDP) puede hacer tareas distintas de recopilación de datos —
de simples a complejas. No "compila", sino "funciona".
## Montaje
- **Servidor**: `projects/web_scraping/apps/browser_mcp/browser_mcp` (36 tools, pool de conexiones).
- **Navegador**: Chrome/Chromium 148 aislado en el puerto CDP **9333** (no el 9222 del navegador diario),
`user-data-dir` dedicado. Lanzado headless para la batería y luego **sin headless** (ventana visible)
para inspección humana — ambos con 5/5.
- **Cliente**: `mcp_client.py` — cliente JSON-RPC stdio **secuencial** (espera la respuesta de cada tool
antes de mandar la siguiente, como hace un cliente MCP real). Un primer intento mandando todos los
mensajes de golpe falló por una race: el servidor procesa requests de forma concurrente.
- **Runner**: `run_demo.py` — ejecuta las 5 pruebas, guarda pasos/respuestas/veredicto en `results/`.
## Resultado: 9/9 PASS (headless y con ventana visible)
Las pruebas 1-5 son la batería inicial; 6-9 se añadieron para validar las tandas de fixes (A/D/E, B, y gap #1).
| # | Prueba | Sitio sandbox | Capacidades ejercitadas | Resultado |
|---|---|---|---|---|
| 1 | Extraer citas estructuradas | quotes.toscrape.com | navigate, wait_load, **eval → JSON real** | PASS — 10 citas `{text,author,tags}` |
| 2 | Percibir página como agente | the-internet.herokuapp.com | **page_perceive** (AX outline) | PASS — outline 4021 chars con `#ref` accionables |
| 3 | Submit de formulario con teclado | the-internet.herokuapp.com/login | dom_click, dom_type, **press_key Enter**, wait_element | PASS — "You logged into a secure area!" |
| 4 | Login + sesión persistente | the-internet.herokuapp.com | form, **storage_save/load**, cookie_clear | PASS — sesión restaurada sin re-login |
| 5 | Scraping paginado + dedup | books.toscrape.com | navegación multi-página, eval, composición | PASS — 60 libros únicos (3 páginas) |
| 6 | sessionStorage en storage_state | the-internet.herokuapp.com | set→save→clear→load→get | PASS — `clear=null`, `restore=demo_v` (fix D) |
| 7 | `find_by_text` honesto | quotes.toscrape.com | dom_find_by_text presente vs inexistente | PASS — texto inexistente devuelve error, no vacío (fix E) |
| 8 | Verificación post-acción | quotes.toscrape.com | dom_click sobre oculto / dom_type sin foco | PASS — ambos devuelven error en vez de actuar al vacío (fix B) |
| 9 | Bucle percibir→actuar por `#ref` | the-internet.herokuapp.com/login | page_perceive → dom_type_ref/dom_click_ref + auto-observe | PASS — login solo por `#ref` (sin selector); cada acción devuelve el outline nuevo (gap #1) |
Cobertura conjunta: lanzar/atar navegador, navegar, esperar carga, evaluar JS con JSON real, percibir
(AX outline), leer texto, teclado, formularios, cookies, estado de sesión, y composición multi-página.
## Valor del ejercicio: 3 bugs reales encontrados y arreglados
Ejecutar de verdad reveló defectos que "compila" jamás habría detectado:
1. **`page_perceive` roto** (bug de integración del MCP). Invocaba `fn run cdp_perceive_outline
--debug-port 9333 ...` con flags, pero `fn run` pasa los argumentos **posicionalmente** a la función
del pipeline → `int('--debug-port')` reventaba. Toda la percepción AX caía. Fix: argumentos
posicionales en el orden de la firma. (`tools_read.go`)
2. **`cdp_save_storage_state` guardaba cookies de todos los dominios.** Usaba `Network.getAllCookies`
(global), así que el `storage_state` arrastraba cookies de sitios visitados antes en la misma sesión
(en la prueba, cookies de Wikipedia contaminaban la sesión de the-internet). Fix: filtrar por el host
actual (`location.hostname`). (`cdp_save_storage_state.go`)
3. **`cdp_load_storage_state` no restauraba la sesión httpOnly.** `Network.setCookies` no aplicaba de
forma fiable la cookie de sesión (`rack.session`, httpOnly) porque a cada cookie le faltaba el campo
`url`. Fix: sintetizar `url` por cookie a partir de `domain`/`secure`/`path`. Con esto el login
persistente (la pieza estrella) funciona de verdad. (`cdp_load_storage_state.go`)
## Segunda tanda de fixes (A + D + E) — a partir del análisis de deuda
Tras la primera batería se atacaron tres deudas, cada una validada:
- **A — Aislamiento robusto del navegador del agente** (`chrome_launch.go`). El wrapper del sistema
`/etc/chromium.d/cdp` inyecta `--user-data-dir`/`--remote-debugging-port` globales a todo chromium;
el aislamiento dependía de que nuestros flags fueran al final (Chrome usa el último duplicado). Fix:
`findChrome` prefiere el **binario real** (`/usr/lib/chromium/chromium`), que al ejecutarse directo no
pasa por el wrapper y por tanto no hereda esos flags. Validado por construcción (el binario existe y va
primero; `browser_launch` devuelve PID correcto); la inspección observable del cmdline choca con el
exit-144 del entorno de pruebas al lanzar chromium desde el harness, no con el fix.
- **D — `sessionStorage` en `storage_state`** (`cdp_save_storage_state.go`, `cdp_load_storage_state.go`).
Antes solo cookies + localStorage. Ahora también el "cajón temporal". Validado por la prueba 6.
- **E — `cdp_find_by_text` honesto** (`cdp_find_by_text.go`). Antes devolvía `("", nil)` cuando no
encontraba (el caller creía que había encontrado algo). Ahora devuelve error explícito. Validado por la
prueba 7.
## Gap #1 — bucle percibir→actuar por `#ref` (lo que cierra el loop del agente)
El cambio que más sube el nivel del agente. Un solo cambio resolvió tres cosas a la vez:
- **`#ref` = `backendDOMNodeId`** (no el `nodeId` efímero del AX tree). `render_ax_outline` ahora emite el id
del nodo DOM real, estable mientras el nodo viva → el `#ref` que el LLM lee sigue válido cuando actúa un
instante después. Resuelve "ref→acción" y "refs estables" de golpe, **sin mantener mapa de estado** en el MCP.
- **Funciones por ref** (`cdp_click_ref`, `cdp_type_ref`, `cdp_hover_ref`): resuelven `backendDOMNodeId` →
bbox (`DOM.getBoxModel`) → centro → acción humanizada.
- **Primitivo único `cdp_click_xy_human`**: click humanizado por coordenadas, compartido por las tres vías
(selector, `#ref`, y futura visión OCR/YOLO). `cdp_click_human` se refactorizó para usarlo (un solo camino).
- **Auto-observe**: cada tool `_ref` del MCP devuelve el outline AX actualizado tras la acción → el LLM ve el
efecto sin pedir otra lectura = verificación implícita (como Playwright MCP).
- **Defectos de calidad de `render_ax_outline` corregidos** en la misma edición: guard de ciclo + límite de
profundidad (evita `RecursionError`), se quitó el `ljust(60)` (gastaba tokens en relleno), y ahora renderiza
el `value` de los inputs (texto escrito, estado).
Validado por la prueba 9: login completo en the-internet **actuando solo por `#ref`** (sin un solo selector CSS).
Pendiente de esta familia: política de humanización por sesión (`human`/`fast`/`instant`) para scraping masivo.
## Tercera tanda de fix (B) — verificación post-acción
- **B — fin del "fire-and-forget"** (`cdp_click.go`, `cdp_type_text.go`). `cdp_click` ahora verifica que
el elemento es **visible** antes de clicar (display:none / tamaño 0 / opacity 0 → error, en vez de clicar
en (0,0) sin efecto). `cdp_type_text` verifica que hay un **campo editable enfocado** (input/textarea/
select/contenteditable) antes de escribir (sin foco → error claro "usa CdpClick primero", en vez de
escribir a la nada). Validado por la prueba 8. Pendiente de esta familia: `cdp_scroll` con target
explícito (P1.5) y el puente percepción→acción por nodeId (P1.3).
## Hallazgos secundarios
- **`press_key Enter` no dispara widgets JS complejos.** Contra el buscador de Wikipedia (typeahead Vue
del skin Vector) el keyevent sintético se ejecutó sin error pero el widget no reaccionó. La prueba 3 se
reorientó a un formulario HTML normal (login de the-internet), donde Enter sí envía el form. Deuda: para
widgets JS-driven puede hacer falta disparar el evento del framework o submit explícito.
- **Las pruebas comparten un mismo Chrome**, por lo que el estado (cookies) se acumula entre ellas. Se
añadió `cookie_clear` al inicio de las pruebas con login para aislarlas. Un harness más estricto usaría
un contexto/perfil por prueba.
## Deuda pendiente (no bloqueó el ejercicio)
- `storage_state` aún no captura `sessionStorage` (sí cookies + localStorage). Suficiente para sesiones
basadas en cookie como the-internet; insuficiente para sitios que guardan el token en sessionStorage.
- Verificación post-acción (P1 del análisis LLM-readiness): `dom_click`/`dom_type` siguen siendo
fire-and-forget. En estas pruebas se compensó con `dom_wait_element` y checks por `eval`, pero las tools
no confirman su efecto por sí mismas.
## Cómo reproducir
```bash
# 1. Chrome aislado en 9333 (headless)
systemd-run --user -q --unit=browser_demo \
chromium --headless=new --remote-debugging-port=9333 \
--user-data-dir=/tmp/browser_mcp_userdata about:blank
# 1-bis. O con ventana visible (Linux con sesión gráfica):
systemd-run --user -q --unit=browser_demo \
--setenv=DISPLAY=:0 --setenv=XAUTHORITY=$HOME/.Xauthority \
chromium --remote-debugging-port=9333 \
--user-data-dir=/tmp/browser_mcp_visible --start-maximized about:blank
# 2. Compilar el MCP y ejecutar la batería
cd projects/web_scraping/apps/browser_mcp && go build -o browser_mcp .
cd ../../demo_e2e && python3 run_demo.py
# 3. Parar el navegador
systemctl --user stop browser_demo.service
```
## Archivos
- `mcp_client.py` — cliente MCP stdio secuencial (reutilizable).
- `run_demo.py` — las 5 pruebas.
- `results/prueba_N_*.json` — pasos, respuestas y datos extraídos por prueba.
- `results/run.log` — log de la corrida.
- `results/summary.json` — veredicto agregado.
- `results/mcp_stderr.log` — stderr del servidor MCP.