# Windup — full documentation > Pruebas E2E en lenguaje natural con replay determinista — el LLM planifica una vez, los replays se ejecutan sin él. ~1s, $0 por replay. This file concatenates the entire Windup documentation as clean markdown for LLM consumption. Individual pages are also available at https://windup.run/es/docs/.md and mapped in https://windup.run/es/llms.txt. --- # Primeros pasos **Pruebas E2E en lenguaje natural con replay determinista — el LLM planifica una vez, los replays se ejecutan sin él.** Describe una prueba en lenguaje natural — *"inicia sesión con la cuenta de prueba, añade el producto X al carrito, finaliza la compra y verifica la confirmación del pedido"* — y Windup la convierte en un plan JSON determinista de acciones del navegador. A partir de la segunda ejecución, la prueba se reproduce **con cero llamadas al LLM**: ~1 segundo, $0, resultados estables. ```bash npm i -D windupjs # Chromium is provisioned automatically (one-time, machine-wide cache) npx windup init # 3 questions → windup.config.ts + example scenario npx windup scan # index your app's routes & elements from source code npx windup new "log in as admin and create an invoice" # LLM-assisted scenario authoring npx windup run checkout # 1st run: the LLM plans · every run after: ~1s replay, $0 ``` ## Requisitos - **Node ≥ 20.** - Una **clave de API** para tu LLM planificador en `.env.local` o `.env` (`.env.local` gana — úsalo cuando tu `.env` esté versionado): `GOOGLE_GENERATIVE_AI_API_KEY` para Google (por defecto) o `OPENAI_API_KEY` para OpenAI. Las claves se usan solo para planificar; los replays en caché nunca llaman a un LLM. Para usar un Chrome existente en vez del Chromium descargado automáticamente, define `CHROME_PATH`; para omitir la descarga por completo, define `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`. ## Un recorrido de cinco minutos El flujo completo en un proyecto nuevo, con lo que deberías esperar ver: ```bash # 1. Install — Chromium is provisioned automatically npm i -D windupjs # 2. Initialize — 3 questions (base URL, model, scenarios dir) npx windup init # → windup.config.ts + e2e/scenarios/ + .windup/ (gitignored) # 3. Index your app from source — before anything ever runs npx windup scan # scan complete (full): framework=react-router routes=106 elements=1125 # The site map now knows your real routes and selectors; the planner # will use them instead of guessing. Re-run after big changes # (windup scan --update re-indexes only files changed since, via git). # 4. Register test credentials once — values never touch git npx windup secret set admin # hidden prompts → .env.local + mapping # 5. Author a scenario from a rough instruction npx windup new "log in with the admin account and create an invoice for ACME" # → e2e/scenarios/create-invoice-acme.json — precise task grounded in # your real screens, account referenced by name, final verification # 6. First run — the LLM plans once (~3s, ~$0.002) npx windup run create-invoice-acme # PASS create-invoice-acme cache=miss llm_calls=1 ... cost=$0.0024 # 7. Every run after — deterministic replay, zero LLM npx windup run create-invoice-acme # PASS create-invoice-acme cache=hit llm_calls=0 total=600ms cost=$0 # 8. Read results like a human, ship reports to CI npx windup run --all --summary --reporter html npx windup costs # AI spend: totals, per provider/model ``` Si una ejecución falla tras un cambio en la app, el plan en caché se invalida y se replanifica automáticamente en la siguiente ejecución — **editas escenarios, no selectores.** --- # Cómo funciona ``` natural-language task ──▶ planner (LLM, 1 call) ──▶ JSON action plan │ trajectory cache ◀── cheap verification ◀── deterministic executor │ └──▶ subsequent runs: zero LLM, ~1s, $0 ``` La parte cara — averiguar las acciones del navegador — ocurre una sola vez y se convierte en datos verificables guardados en caché. - **Los planes son datos, no código** — JSON validado por esquema; sin scripts generados, sin condicionales. - **Verificación barata** — postcondiciones de DOM/URL después de cada acción. Una verificación fallida invalida el plan en caché y dispara una replanificación automática. - **Mapa del sitio** — cada ejecución alimenta un grafo de páginas y transiciones; `windup scan` siembra ese grafo directamente desde tu código fuente antes de la primera ejecución, de modo que el planificador usa los selectores *reales* de tu app en lugar de adivinar. - **Fragmentos** — bloques de acciones probados (p. ej. login) que el planificador compone mediante `{ "type": "use" }` en vez de regenerarlos. - **Cero conocimiento del sitio incrustado** — el motor conoce frameworks y la web, nunca *tu* sitio. Todo el conocimiento del sitio llega como entrada (escenarios, config, manifiesto) o se descubre en tiempo de ejecución. ## Por qué Windup Los scripts escritos a mano son baratos de ejecutar pero caros de mantener. Los agentes de IA por ejecución son fáciles de escribir pero lentos y no deterministas. Windup toma la mitad buena de cada uno. | | Scripts escritos a mano | Agente de IA por ejecución | **Windup** | |---|---|---|---| | Autoría | código + selectores a mano | lenguaje natural | lenguaje natural | | Coste por ejecución | $0 | LLM en **cada** ejecución | LLM solo en la **primera** ejecución | | Velocidad de ejecución | rápido | lento (modelo en el bucle) | ~1s replay | | Determinismo | alto | bajo — improvisa cada vez | alto — el mismo plan en cada replay | | La app cambió | arreglas el script | puede hacer algo distinto en silencio | la verificación falla → replanifica automáticamente | Para la mecánica más profunda — límites de módulos, formatos de datos, postura de coste y seguridad — consulta [Arquitectura y especificación](/es/docs/architecture). --- # Escenarios Un escenario es un archivo JSON en tu directorio de escenarios (por defecto `e2e/scenarios/`): ```json { "scenario_id": "checkout", "start_url": "/", "task": "Log in as the qa account, add 'Backpack' to the cart, check out and verify the order confirmation message appears.", "hints": ["Optional site-specific tips for the planner. Delete if not needed."] } ``` - `start_url` es **opcional** (por defecto `/`) y debería mantenerse libre de entorno: una ruta, resuelta contra la base URL efectiva. - Termina la tarea con **qué verificar** — eso se convierte en la postcondición final del plan. - Nunca pongas secretos en las tareas. Referencia las cuentas desde el manifiesto del proyecto (consulta [Credenciales de prueba](/es/docs/credentials)); el plan usará `value_ref: "ENV:VAR"` y el valor real se resuelve solo en tiempo de ejecución, nunca se guarda en caché. ## Dependencias entre escenarios (`depends_on`) Los flujos rara vez empiezan de cero — crear una cuenta bancaria requiere haber iniciado sesión. Declara los prerrequisitos y cada escenario se mantiene pequeño, enfocado y cacheable de forma individual: ```json { "scenario_id": "create-bank-account", "depends_on": ["login"], "task": "Already on the dashboard, open Settings > Bank accounts, create an account named 'Inter' and verify it appears in the list." } ``` - Las dependencias se ejecutan **en la misma sesión del navegador**, en orden, cada una con su propia caché — una suite caliente reproduce toda la cadena con cero llamadas al LLM. - Sin un `start_url`, el escenario dependiente **continúa desde donde terminó la última dependencia** — y en la primera planificación el LLM ve esa página real (el dashboard tras el login), en lugar de planificar a ciegas. - Las cadenas funcionan (`login` → `select-company` → `create-account`), los ciclos se rechazan, y una dependencia fallida hace fallar la ejecución con el tipo `dependency` antes de que el escenario en sí empiece. - Cada dependencia conserva su propia autorreparación: si su plan en caché se rompe, se replanifica y se vuelve a cachear — los dependientes se benefician automáticamente. - Editar el `task` de un escenario invalida su plan en caché (una prueba reescrita es una prueba distinta). `windup new` maneja las dependencias en ambos sentidos: `--depends-on login` las declara explícitamente, y **el LLM autor también las sugiere por su cuenta** — ve cada escenario existente (id + tarea) y, cuando la instrucción presupone un estado que uno de ellos produce ("ya con sesión iniciada…"), emite `depends_on` automáticamente (filtrado mecánicamente contra los ids de escenarios reales — nunca inventados). ## Autoría con `windup new` No tienes que escribir tareas detalladas a mano. Dale a `windup new` una instrucción imprecisa y el LLM actúa como autor de pruebas — la reescribe en un escenario preciso y verificable usando el **mapa del sitio** (pantallas, menús y elementos reales de `windup scan` y ejecuciones pasadas) y el **manifiesto del proyecto** (cuentas referenciadas por nombre, nunca credenciales literales): ```bash npx windup new "log in with the qa user, add the backpack to the cart and check out" # → e2e/scenarios/purchase-backpack-qa.json — real screen names, concrete fake # form data, account referenced as "the qa account", explicit final verification ``` Genera el `scenario_id`, elige el `start_url` entre las rutas conocidas (con `/` como respaldo — nunca inventa rutas) y añade sugerencias de selectores del mapa cuando ayudan. Añade **`--validate`** para que ejecute el escenario generado y, si falla, lo refine a partir del fallo y lo reintente (hasta 3 intentos) — recibes de vuelta un escenario que *ya pasó una vez*, con una caché caliente: ```bash npx windup new "log in and create a cost center named Marketing" --validate # attempt 1: FAIL — element button:has-text('Save') not visible # attempt 2: PASSED # ✓ validated in 2 attempts — the plan is cached ``` **Las credenciales en la instrucción nunca llegan al archivo del escenario**: se registran automáticamente como una cuenta con nombre (valores en `.env.local`, mapeo en `windup.credentials.json`) y la tarea referencia la cuenta — consulta [Credenciales de prueba](/es/docs/credentials). Flags: `--id `, `--force` (sobrescribir), `--depends-on `, `--llm `. La salida es un archivo para que **tú lo revises, edites y versiones** — la autoría es asistida, la prueba sigue siendo tuya. Una llamada al LLM (~$0.001), registrada en el libro mayor de `windup costs` bajo `authoring`. --- # Credenciales de prueba Las credenciales nunca viven en los archivos de escenario, los planes, la caché ni git — solo **referencias**. Los valores permanecen en `.env.local` (gitignored) o en secretos de CI; el mapeo cuenta → nombre ENV vive en `windup.credentials.json` (versionado — no contiene valores) y se fusiona en el manifiesto del proyecto automáticamente. ```bash npx windup secret set admin # hidden interactive prompts → .env.local + mapping npx windup secret list # accounts + whether each ENV is set (never prints values) ``` Las tareas luego referencian la cuenta por nombre — *"inicia sesión con la cuenta admin"* — y los planes usan `value_ref: "ENV:WINDUP_ADMIN_PASSWORD"`, resuelto solo en tiempo de ejecución. `windup new` hace esto automáticamente: las credenciales escritas en la instrucción se detectan, se registran y se depuran — el escenario generado menciona la cuenta, nunca los valores. En CI, define los mismos nombres de variables como secretos del pipeline. --- # Entornos (dev / staging / CI) El origen de la URL de inicio proviene de, en orden de precedencia: 1. el flag `--base-url` 2. la variable de entorno `WINDUP_BASE_URL` 3. `baseUrl` en `windup.config.ts` 4. un `start_url` absoluto en el escenario Una sobrescritura explícita rebasa incluso las URLs absolutas del escenario (se preservan la ruta y la query). La caché de planes es **portable entre entornos**: la identidad de la caché usa la *ruta* de la URL de inicio, no el host/puerto. Un plan generado contra `localhost:8080` se reproduce en staging o CI con cero llamadas al LLM. ```bash npx windup run checkout --base-url https://staging.example.com WINDUP_BASE_URL=http://localhost:8080 npx windup run --all ``` --- # Proveedores de LLM El planificador es agnóstico al proveedor. Se admiten Google Gemini y OpenAI; configura varios a la vez y elige uno por ejecución: ```ts // windup.config.ts llm: { provider: "google", // default for runs without --llm model: "gemini-3.1-flash-lite", providers: { openai: { model: "gpt-5-mini" }, // default model when --llm openai is used // openai: { apiKeyEnv: "MY_OPENAI_KEY", baseUrl: "https://my-proxy/v1" }, }, }, ``` ```bash npx windup run checkout # config default (google) npx windup run checkout --llm openai # provider default model (gpt-5-mini) npx windup run checkout --llm openai:gpt-5-nano # explicit provider:model WINDUP_LLM=openai:gpt-5-mini npx windup run --all # same thing via env (CI) ``` - `--llm` funciona en `run`, `bench` (compara proveedores en el mismo escenario) y `scan` (capa de asistencia LLM). - Claves de API: `GOOGLE_GENERATIVE_AI_API_KEY` / `OPENAI_API_KEY` por defecto; sobrescribe el nombre de la variable de entorno con `apiKeyEnv`. - `baseUrl` (solo OpenAI) apunta a cualquier endpoint compatible con OpenAI — Azure, un proxy o un servidor de modelo local. - Cambiar de proveedor nunca invalida la caché de planes: los planes son datos, los replays no usan LLM sin importar quién planificó. - `windup costs` desglosa el gasto **por proveedor y por modelo**, de modo que alternar entre LLMs mantiene visible el gasto por proveedor. --- # CI / CD ```bash npx windup run --all --reporter junit --report-file reports/windup.xml ``` - `--all` ejecuta cada escenario del directorio (un navegador caliente para toda la suite). - El código de salida es distinto de cero cuando cualquier escenario falla. - `--concurrency ` ejecuta escenarios en paralelo sobre un único navegador caliente compartido (~2× más rápido en una suite mixta); `--browser firefox|webkit` ejecuta la suite en modo multinavegador. - `--reporter junit` emite JUnit XML (GitHub Actions, GitLab y Jenkins lo consumen de forma nativa); `--reporter json` emite un resumen legible por máquina; `--reporter html` emite una página autocontenida y amigable para humanos (sin JS/dependencias — súbela como artefacto de CI o ábrela localmente). Salida por defecto: `.windup/reports/`. - `windup costs --json` reporta el gasto de IA para el seguimiento del pipeline. ## Ejemplo: GitHub Actions ```yaml - run: npm ci && npx playwright install chromium - run: npx windup run --all --base-url http://localhost:8080 --reporter junit --report-file reports/windup.xml env: GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GEMINI_KEY }} - uses: dorny/test-reporter@v1 if: always() with: { name: windup, path: reports/windup.xml, reporter: java-junit } ``` --- # Configuración (`windup.config.ts`) ```ts import { defineConfig } from "windupjs"; export default defineConfig({ baseUrl: "http://localhost:3000", llm: { provider: "google", model: "gemini-3.1-flash-lite", // Several providers at once — pick per run with --llm (see "LLM providers"): providers: { openai: { model: "gpt-5-mini" } }, }, scenarios: "e2e/scenarios", framework: "react-router", // detected by init; used by scan // browser: "chromium", // or "firefox" / "webkit" (need: npx playwright install ) scan: { llmAssist: { enabled: true, maxCalls: 20 }, // hard cost cap per scan }, // Project manifest: team-provided knowledge injected into the planner prompt. context: { conventions: ["every interactive element has a data-testid"], credentials: { qa: { user: "ENV:QA_USER", password: "ENV:QA_PASSWORD" }, }, vocabulary: { "order": "the Order entity, screen /orders" }, }, }); ``` - **`context.credentials`** mapea nombres de cuenta a referencias ENV. Cuando una tarea menciona la cuenta, el plan usa `value_ref` — las credenciales del manifiesto tienen precedencia aunque la página muestre valores, y al planificador se le prohíbe inventar nombres ENV. - **Asistencia LLM** (capa 3 de scan) lee archivos que las capas estáticas no pudieron resolver (rutas construidas dinámicamente, componentes indirectos), limitada por `maxCalls`. Los resultados se recuerdan por hash de archivo — los archivos sin cambios nunca vuelven a costar. Los costes se registran en el libro mayor y se muestran con `windup costs`. ## Qué vive dónde | Ruta | Contenido | ¿Versionar? | |---|---|---| | `windup.config.ts` | Configuración | ✅ | | `e2e/scenarios/*.json` | Tus pruebas, en lenguaje natural | ✅ | | `e2e/fragments/*.json` | Bloques reutilizables curados | ✅ | | `windup.credentials.json` | Mapeo cuenta → nombre ENV (sin valores) | ✅ | | `.env.local` | Valores de credenciales | ❌ (auto-gitignored; CI usa secretos con los mismos nombres) | | `.windup/` | Estado derivado: caché de planes, libro mayor de ejecuciones, mapa del sitio, informes | ❌ (init lo añade a `.gitignore`) | --- # API programática y runners de pruebas Ejecuta un escenario desde código y recibe de vuelta las métricas de la ejecución: ```ts import { run } from "windupjs"; const result = await run("checkout"); // RunMetrics: result, llm_calls, cost, per-action timing ``` Intégralo en **vitest** (contrato compatible con jest) — una prueba nativa por escenario, compartiendo el motor caliente: ```ts // e2e/windup.test.ts — vitest import { windupSuite } from "windupjs/vitest"; await windupSuite(); // one native test per scenario ``` --- # Comandos | Comando | Descripción | |---|---| | `windup init` | Crea `windup.config.ts`, `.windup/` (gitignored) y un escenario de ejemplo | | `windup new "" [--id x] [--force] [--depends-on ids] [--validate]` | Genera un escenario a partir de una instrucción imprecisa; `--validate` lo ejecuta y refina hasta que pasa (≤3 intentos) | | `windup run [scenario]` | Ejecuta un escenario (replay cuando está en caché, planifica ante un miss) | | `windup run --all` | Ejecuta cada escenario — modo CI | | `windup scan [--update] [--no-assist]` | Indexa estáticamente rutas y elementos interactivos en el mapa del sitio; `--update` reindexa solo los archivos cambiados desde el último scan (git diff); `--no-assist` omite la capa LLM (coste cero) | | `windup costs [--last n] [--days n] [--json]` | Informe de uso de IA desde el libro mayor de ejecuciones: totales, replays gratuitos, desglose por proveedor, por modelo y por escenario, gasto de scan y de autoría | | `windup status` | Páginas del mapa del sitio por fuente, obsolescencia, escenarios en caché, fragmentos | | `windup fragment extract --id --description ` | Promueve una porción de un plan en caché a un fragmento reutilizable | | `windup secret set [--user u] [--password p]` | Registra credenciales de prueba: valores → `.env.local`, mapeo → `windup.credentials.json` | | `windup secret list` | Cuentas + si cada ENV está definida (nunca imprime valores) | | `windup sig [--repeat n]` | Firma estructural de la página (diagnósticos) | | `windup bench ` | Protocolo de validación completo (generación, determinismo del replay, recuperación ante fallos) | | `windup cache clear` | Descarta la caché de trayectorias (las siguientes ejecuciones replanifican) | ### Flags de `run` | Flag | Qué hace | |---|---| | `--all` | Ejecuta cada escenario del directorio — modo CI, un navegador caliente para toda la suite. Código de salida distinto de cero si algún escenario falla. | | `--concurrency ` | Ejecuta hasta `n` escenarios en paralelo sobre un único navegador caliente compartido con contextos aislados — ~2× más rápido en una suite mixta. Secuencial por defecto. | | `--no-cache` | Ignora el plan en caché y replanifica desde cero (fuerza una llamada al LLM), incluso cuando existe una trayectoria válida. Úsalo para regenerar un plan a propósito. | | `--no-map` | Planifica sin el grafo del mapa del sitio — omite las rutas y selectores indexados. Útil para depurar el planificador o un entorno recién creado. | | `--repeat ` | Ejecuta el escenario `n` veces seguidas sobre el mismo navegador caliente — comprobaciones de estabilidad y flakes. | | `--headed` | Muestra la ventana del navegador en vez de ejecutar en modo headless. | | `--slowmo ` | Añade un retardo entre acciones para que puedas observar cada paso — ritmo de demo y depuración. | | `--base-url ` | Sobrescribe el origen de la URL de inicio para esta ejecución (dev / staging / CI). Rebasa incluso las URLs absolutas del escenario, preservando ruta y query. | | `--browser chromium\|firefox\|webkit` | Ejecuta en el motor elegido (Chromium por defecto). El mismo plan se reproduce en los tres — escribe una vez, ejecuta en todos. | | `--llm ` | Elige el LLM planificador para esta ejecución (p. ej. `openai:gpt-5-mini`). Solo afecta a la planificación; los replays en caché nunca llaman a un LLM. | | `--summary` | Tras la ejecución, una llamada extra al LLM escribe un informe legible por humanos citando valores reales observados en la página final. Desactivado por defecto para que los replays sigan a $0. | | `--suggest` | En una ejecución **fallida**, una llamada extra al LLM propone una corrección concreta para el escenario. Se dispara solo ante un fallo. | | `--reporter junit\|json\|html` | Emite un informe de CI — JUnit XML, un resumen JSON legible por máquina o una página HTML autocontenida. | | `--report-file ` | Escribe el informe en una ruta específica (por defecto `.windup/reports/`). | ## Informe de IA (`--summary`) Para humanos que leen resultados (no CI), `--summary` añade una llamada al LLM después de cada ejecución que escribe un breve informe: qué hizo la prueba, el resultado, **valores concretos observados en la página final** (precios, mensajes, nombres de productos — citados literalmente desde la página) y cualquier dificultad (pasos lentos, replanificación, fallos). Se imprime en la terminal, queda en el libro mayor de ejecuciones y se muestra como un bloque destacado en los informes HTML/JSON. ```bash npx windup run checkout --summary --reporter html # summary: "The test logged in and completed checkout for 3 items; the # confirmation page showed 'Thank you for your order'. Prices observed: ..." ``` Desactivado por defecto a propósito — los replays en caché se mantienen en cero llamadas al LLM y $0. El coste del informe (~$0.0005 en el modelo por defecto) se rastrea por separado en las métricas de la ejecución y se incluye en `estimated_cost_usd`. ## Sugerencias de corrección ante fallos (`--suggest`) Cuando una ejecución **falla**, `--suggest` añade una llamada al LLM que actúa como un ingeniero senior de QA depurándola: compara el plan ejecutado y el paso fallido contra la **página final real** y los selectores conocidos del mapa del sitio, y luego propone una corrección concreta para el escenario — el selector equivocado y el real, una pantalla objetivo que no contiene lo que la tarea espera, un paso faltante, o un timeout demasiado corto para una página lenta. ```bash npx windup run create-invoice --suggest # FAIL create-invoice ... element button:has-text('Save') not visible # suggested fix: The 'Save' button does not exist; the dialog's real button # is labeled 'Create'. Change the hint to button:has-text('Create'). ``` Convierte una ejecución en rojo en una edición concreta — en vez de tener que hacer ingeniería inversa de la app a mano. Solo se dispara ante un fallo (las ejecuciones en verde no cuestan nada), nunca edita el escenario en sí, y se muestra como un bloque destacado en los informes HTML/JSON. Combina de forma natural con `--summary`. --- # Arquitectura y especificación Una vista condensada de la [especificación viva](https://github.com/windupjs/windup/blob/main/docs/specs/SPEC.md). Describe el sistema tal como se entrega. ## Tesis Las pruebas E2E de navegador pueden usar un LLM **solo para planificar** (y replanificar ante fallos), con ejecución determinista y verificación barata — de modo que las ejecuciones repetidas hacen **cero llamadas al LLM**. Validado de extremo a extremo: la primera ejecución planifica en segundos por fracciones de céntimo; los replays tardan ~0.5–1s y cuestan $0. ## Principios 1. **El LLM es la excepción, no la regla.** Una llamada por miss de caché; los replays nunca lo llaman. 2. **Cero conocimiento del sitio incrustado.** El motor puede conocer *frameworks* (Next.js, react-router, nomenclatura de design systems) y la plataforma web — nunca un sitio específico. Todo el conocimiento del sitio llega como entrada o se descubre en tiempo de ejecución. 3. **Cada ejecución es también recolección.** El ejecutor ya visita cada página de un flujo; persistir lo que ve cuesta ~un `evaluate` por acción. 4. **El conocimiento es caché, no verdad.** Cualquier cosa que el mapa del sitio o el scan estático afirme puede estar obsoleta; degrada a descubrimiento en tiempo de ejecución. Precedencia: `execution > static > llm`. 5. **El coste nunca sorprende.** Cada punto de contacto con el LLM tiene un tope explícito; cada llamada se registra en un libro mayor; el trabajo estático/de asistencia repetido se memoiza. 6. **Los planes son datos, no programas.** Sin condicionales, sin bucles, sin código libre. Si un flujo necesita bifurcación, divide el escenario. ## Arquitectura ``` scenario (natural language, versioned) │ ▼ ┌─ runner ─────────────────────────┐ │ cache lookup (path-keyed) │ │ miss → planner (LLM, 1 call) │ │ hit → cached plan (rebased) │ │ → executor (deterministic) │ │ → verifier (postconditions) │─▶ fail → invalidate → re-plan │ → cache save + run ledger │ └───────────────────────────────────┘ site map (graph) ◀── windup scan (static + LLM-assist) ◀── passive collection (every run) ``` Límites de módulos (todos en `packages/windup/src/`): | Módulo | Responsabilidad | |---|---| | `browser.ts` | Frontera única del motor (Playwright). `BrowserContext` fresco por sesión sobre un singleton perezoso de Chromium; clics confiables con actionability nativa; `ariaSnapshot()` alimenta al planificador. Apunta a la primera coincidencia *visible*, no a la primera del DOM. | | `llm.ts` | Frontera del LLM multiproveedor. Una interfaz `LlmClient`; implementaciones de Google Gemini (SDK) y OpenAI (REST puro). Varios proveedores configurables a la vez; seleccionados por ejecución. Cambiar de proveedor nunca toca la caché de planes. | | `planner.ts` | Lógica de planificación (agnóstica al proveedor). Prompt = tarea + árbol de accesibilidad de la página + elementos reales + porción del mapa del sitio + catálogo de fragmentos + manifiesto + hints. Salida estructurada; dos niveles de reintento (semántico + transitorio); salida saneada y luego validada (Ajv es la autoridad). | | `executor.ts` | Bucle determinista: goto → por acción (comprobar visibilidad → actuar → verificar). Emite observaciones pasivas del mapa del sitio. | | `verifier.ts` | Postcondiciones, todas sin LLM: elemento visible, glob de URL, valor de input. Polling con esperas seguras entre frames. | | `cache.ts` | Caché de trayectorias indexada por `scenario_id` + **ruta** de la URL de inicio (portable entre entornos). Guardada solo tras una ejecución completa verificada; un replay fallido invalida y replanifica, conservando la entrada obsoleta como evidencia. | | `signature.ts` | Identidad estructural de la página: SHA-256 de los elementos interactivos normalizados — sin texto, sin datos — para que el ruido del entorno no divida identidades. | | `sitemap.ts` | Grafo de páginas/transiciones. Los nodos llevan `source: execution\|static\|llm` con obsolescencia y procedencia. Porción del prompt: BFS (profundidad ≤ 3), puntuado por términos, dentro de un presupuesto de caracteres. | | `scan/` | Indexación del proyecto. Capa 1: rutas por convención (Next.js, react-router). Capa 2: elementos interactivos mediante parsing de JSX consciente de llaves (etiquetas crudas + componentes de design system). Capa 3: asistencia LLM con tope, memoizada por hash de archivo. | | `fragments.ts` | Sub-trayectorias reutilizables y probadas, versionadas en `e2e/fragments/`. Los planes las referencian por id; la caché almacena la referencia (las actualizaciones se propagan). | | `authoring.ts` | `windup new` — el LLM reescribe una instrucción imprecisa en un escenario preciso anclado en el mapa del sitio y el manifiesto; credenciales autorregistradas y depuradas; la salida es un archivo versionado para revisión. | | `metrics.ts` / `costs.ts` | Cada ejecución escribe un registro en el libro mayor (tokens, llamadas, modelo, timing, clase de fallo). Los precios son una tabla por modelo con fecha; `windup costs` recalcula para que el historial siga siendo correcto. | | `summary.ts` / `suggest.ts` | Informe de IA posterior a la ejecución y sugerencia de corrección posterior al fallo, opcionales — una llamada extra al LLM cada uno, rastreados por separado, sin afectar nunca el resultado de la ejecución. | | `reporters.ts` | Informes JUnit XML, JSON y HTML autocontenido; salida distinta de cero ante fallo. | | `secrets.ts` | Credenciales sin secretos versionados: valores en `.env.local`, mapeo cuenta → nombre ENV versionado; los planes llevan solo `value_ref`, resuelto en tiempo de ejecución. | ## Formatos de datos **Plan de acciones** (`plan_version: "0.1"`): `{ plan_version, scenario_id, task, start_url, generated_by, actions[] }`. Una acción es `{ id, type: goto|click|fill|wait_for|use, target?, value? | value_ref?, url?, use?, expect?, timeout_ms }`. Reglas semánticas: click/fill/wait_for requieren `target.selector`; fill requiere exactamente uno de value/value_ref; `value_ref` debe estar mencionado por tarea/hints/manifiesto (sin nombres ENV inventados); la acción final debe llevar `expect` (o ser un `use`). **Entrada de caché** (`cache_version: "0.2"`): `{ key: {scenario_id, start_url(path), start_sig?}, plan, status: active|stale, stats }`. **Mapa del sitio** (`map_version: "0.1"`): `{ last_scan_sha, pages, transitions, assist_seen }`. ## Postura de modelo y coste Modelo planificador por defecto: **`gemini-3.1-flash-lite`** (medido: 1 llamada limpia/generación, ~3–4s, ≈ $0.0025/generación). Agnóstico al proveedor: Google Gemini y OpenAI vienen incluidos tras la frontera `llm.ts`. Cada registro del libro mayor lleva proveedor+modelo; los precios son una tabla plana por modelo. Añadir un proveedor = una implementación de cliente. ## Postura de seguridad El contenido de la página capturado de la app bajo prueba se pasa al LLM **delimitado y marcado como datos no confiables**, con una instrucción explícita de tratarlo como datos, nunca como instrucciones. Como los planes son datos validados por esquema y ejecutados de forma determinista, una página no puede hacer que Windup ejecute código arbitrario. Los valores de credenciales nunca entran en escenarios, planes, la caché ni prompts del LLM. Modelo de amenazas completo: [SECURITY.md](https://github.com/windupjs/windup/blob/main/SECURITY.md). ## Limitaciones conocidas - Las rutas relativas anidadas de react-router se recolectan del mejor modo posible (corregidas por las observaciones de ejecución). - El scan es consciente de un solo paquete; los monorepos se detectan y advierten, los índices por app son trabajo futuro. - No hay demonio entre invocaciones de la CLI (deliberado); el pool caliente es por proceso. - La autodetección de fragmentos no está construida; la extracción es manual. ## Verificación - `npm test` (packages/windup): ~97 pruebas unitarias/de integración, herméticas al LLM. CI las ejecuta con Chromium real en Ubuntu. - `windup bench `: el protocolo de validación — 5 generaciones (≥ 4/5 válidas), 10 replays (10/10 con `llm_calls=0`), replay ≥ 5× más rápido, replay a $0, recuperación al romper un selector. - Dogfood en proyecto real: una app de react-router con 106 rutas — los planes se reproducen a ~0.5s/$0, caché portable entre entornos. --- # Notas de ingeniería — las técnicas detrás de Windup Un resumen de los enfoques que hacen que las pruebas en lenguaje natural sean deterministas y baratas: - **Planifica una vez, reproduce gratis.** El LLM se usa exactamente una vez por escenario (más la replanificación automática cuando la app cambia). Su salida es un **plan de acciones JSON validado por esquema — datos, no código**: sin scripts generados, sin condicionales, sin improvisación en tiempo de ejecución. Los replays ejecutan el plan en caché con cero llamadas al modelo. - **Ejecución determinista.** Los planes se ejecutan sobre Playwright con comprobaciones nativas de actionability y eventos de entrada confiables. Cada acción lleva una postcondición explícita (`expect`: elemento visible / glob de URL / valor de input) verificada **sin LLM** — la verificación cuesta una consulta al DOM, no tokens. - **Caché autorreparable.** Las trayectorias se cachean indexadas por escenario + *ruta* de la URL de inicio (portable entre hosts de dev/staging/CI). Una verificación fallida invalida el plan, preserva la entrada obsoleta como evidencia y dispara una replanificación con el fallo como contexto. - **Firmas estructurales de página.** Las páginas se identifican por un SHA-256 de sus elementos interactivos normalizados — sin texto, sin datos — para que el ruido del entorno no divida identidades, y se detecta (con lenidad) la deriva de la página inicial en el replay. - **Conocimiento del sitio por capas.** Un grafo de mapa del sitio alimenta al planificador con rutas y selectores reales, construido a partir de tres fuentes con precedencia estricta — observación en tiempo de ejecución > scan estático del código > asistencia LLM con tope. El conocimiento es caché, no verdad: cualquier cosa obsoleta degrada a descubrimiento en tiempo de ejecución. - **Disciplina de presupuesto del prompt.** El prompt de planificación se mantiene de tamaño ≈ constante (~32k caracteres): el árbol de la página, la porción del mapa, el catálogo de fragmentos y el manifiesto tienen cada uno topes duros de caracteres. Los prompts largos degradan de forma medible a los modelos pequeños — los presupuestos son una característica de corrección, no una optimización. - **Normalización mecánica en vez de esperanza en el prompt.** La salida del modelo se sanea de forma determinista: campos vacíos descartados, ids renumerados, `wait_for`⇄`expect` normalizados, acciones de eco de fragmentos deduplicadas, credenciales depuradas de los escenarios autorados. Las pruebas A/B entre proveedores mostraron que las instrucciones del prompt por sí solas no se sostienen entre modelos — el código tiene la última palabra. - **Reintento de dos niveles.** Los fallos semánticos (plan inválido) reciben un reintento corto que lleva los errores de validación; las patologías transitorias de la API (degeneración por bucle de tokens, red) reciben nuevas llamadas con semillas variadas. Se evitan los reintentos de prompt completo — vuelven a disparar la degeneración de forma fiable. - **Bloques de construcción componibles.** Los fragmentos son sub-trayectorias curadas y versionadas (p. ej. login) que los planes referencian por id — actualizados una vez, propagados a todas partes, expandidos en tiempo de ejecución. El manifiesto del proyecto inyecta conocimiento del equipo (convenciones, cuentas, vocabulario) en cada plan. - **Secretos por referencia.** Los valores de credenciales viven en `.env.local`/secretos de CI; los archivos versionados llevan solo mapeos cuenta → nombre ENV. Los planes usan `value_ref`, resuelto en tiempo de ejecución — los secretos nunca llegan al LLM, la caché ni git. - **Frontera de LLM agnóstica al proveedor.** Una interfaz, implementaciones de Google y OpenAI (el cliente de OpenAI es REST puro — sin el peso de un SDK), seleccionable por ejecución. Cambiar el motor del navegador y añadir un proveedor fueron cada uno un cambio de un solo archivo — las fronteras son la arquitectura. - **Coste que puedes auditar.** Cada punto de contacto con el LLM tiene un tope explícito y queda en un libro mayor por ejecución con tokens, modelo y proveedor; `windup costs` recalcula desde una tabla de precios con fecha, de modo que el historial sigue siendo preciso a medida que los precios cambian.