---
title: Limitaciones conocidas
icon: phosphor-duotone:warning
summary: Los vacíos honestos - qué está probado contra una aplicación real en ejecución y qué no.
description: Los vacíos honestos - qué está probado contra una aplicación real en ejecución y qué no.
tags: [reference, limitations]
---

# Limitaciones conocidas

BX Agents está en desarrollo activo. Esta página rastrea los vacíos honestos - qué está probado contra una aplicación real en ejecución, qué todavía solo corre contra el proveedor `"mock"` de bx-ai, y las peculiaridades reales ascendentes con las que este proyecto se topó.

## Las pruebas corren solo contra el proveedor `mock`

Cada spec de vía rápida (pipeline de build, generadores, verbos de CLI) y la [suite de integración de ColdBox](#real-coldbox-integration-testing) ejercitan el proveedor `"mock"` incorporado de bx-ai - nunca una llamada de red real a un LLM. Esto es deliberado (rápido, gratis, CI determinista), pero significa que ninguna prueba automatizada actualmente demuestra que un proveedor real (OpenAI, Anthropic, etc.) realmente funciona correctamente de extremo a extremo. Haz al menos una ejecución manual de `chat`/`serve` contra un proveedor real y una clave de API desechable antes de depender de esto en producción.

## Real ColdBox integration testing

`./gradlew testColdBoxIntegration` arranca un proceso **real** de `boxlang-miniserver` contra el propio `Application.bx`/`Bootstrap` de una app generada, y hace un request HTTP genuino a través de una ruta registrada con `toAi()`. Esta es la prueba más sólida de la suite - detectó tres bugs reales en `ColdBoxAppGenerator.bx` durante el desarrollo (un `extends` faltante en un `Binder` de WireBox, un orden incorrecto de argumentos del constructor de `Bootstrap`, y una llamada simple a `getInstance()` que no existe en `Binder`).

### La condición de carrera del primer request de `toAi()`

El **primer** request HTTP a la ruta `toAi()` de una app recién arrancada puede fallar transitoriamente con "Function [getInstance] not found" - una condición de carrera genuina de inyección perezosa de ColdBox/WireBox en el propio delegado `getInstance` del Router, no un bug de BX Agents. Tiene éxito de forma confiable en cada request posterior a que algo más ya haya forzado a WireBox a construir el singleton `GeneratedAgent` una vez (un health check, una sesión de `chat`, otra ruta). **Envía un request de calentamiento** antes de depender de una ruta `toAi()` recién desplegada bajo carga.

## Tres verificaciones de integración - cerradas por una ruta distinta a la planeada originalmente

`tests/specs/integration/RuntimeStartupSmokeSpec.bx` todavía tiene tres `xit()`, ya que este archivo se ejecuta vía el corredor de CLI (`runTests.bxs`/`testBx`), y el modo CLI de BoxLang nunca da un scope `cgi` - que el RoutingService de ColdBox necesita incluso para cargar el router en el arranque. Eso es un hecho estructural sobre el modo CLI, no un vacío: las tres verificaciones ahora están probadas de verdad en otro lugar, en `tests/specs/integration/coldbox/ColdBoxRuntimeSpec.bx` (que se ejecuta dentro de un request HTTP real servido por un proceso real de `boxlang-miniserver`):

- Un request HTTP enrutado por ColdBox real llegando al agente generado de extremo a extremo - probado por `ColdBoxRuntimeSpec.bx` más la propia aserción `POST /api/chat/invoke` de `runColdBoxIntegrationTests.bxs`.
- `schedules/*` realmente registrándose con un `Scheduler` de ColdBox **en vivo** - probado vía `SchedulerService.getSchedulers()["appScheduler@coldbox"].hasTask(...)` contra un arranque real, sin esperar a que se dispare un cron real (la granularidad más gruesa soportada es 1 minuto, lo que gravaría cada ejecución de CI por una prueba adicional marginal una vez que la tarea se confirma en vivo y registrada).
- `chat` y una ruta HTTP servida (`serve`) nunca divergiendo - corregido de su redacción original (`chat` deliberadamente nunca arranca WireBox en absoluto, así que nunca puede compartir el *objeto* singleton de WireBox por diseño) a lo que realmente importa: instanciar `GeneratedAgentFactory` fuera de WireBox produce un agente conductualmente equivalente al propio singleton de WireBox.

## Pruebas de CLI de proceso de SO real - y un bug real que encontró

`ModuleCliProcessTest.java` genera procesos hijo genuinos de `java -jar <boxlang-jar> module:bxagents <verb> ...` contra una copia de la estructura real del módulo instalable (`build/modules/bxagents`), apuntado a un `modulesDirectory` real - exactamente como una instalación real de BoxLang carga este módulo. Cada otro spec de CLI llama a `ModuleConfig.main()`/al `run()` de cada verbo en proceso en su lugar, lo cual es más rápido pero nunca prueba que el módulo se resuelva correctamente una vez genuinamente instalado.

Ese vacío era real: esta prueba detectó a todo verbo de CLI fallando con "class not located" en el momento en que se ejecutó a través de un proceso de módulo genuinamente instalado, porque las referencias cruzadas internas usaban una ruta con puntos simple `bxagents.models....` que solo se resolvía gracias al propio `boxlang.json` de dev/test de este repositorio (un mapeo `/bxagents` declarado a mano) - un módulo genuinamente instalado nunca obtiene ese mapeo. Se corrigió cambiando las referencias internas de cada clase anidada a la forma de sufijo relativo al módulo `Class@bxagents` (`ModuleConfig.bx` mismo, que está en la raíz del módulo, resuelve rutas relativas simples sin problema y no necesitó cambio) - ver el docblock de `init()` de `BuildPipeline.bx` para la explicación completa. La salida de estructura de módulo de `build.gradle` se movió de `build/module` a `build/modules/bxagents` (el nombre de la carpeta debe ser igual al nombre del módulo para que el descubrimiento de `modulesDirectory` lo encuentre en absoluto) y el `boxlang.json` de dev/test también lo carga ahora como un módulo real, así que toda la suite existente ejercita el mismo camino de resolución que usa producción, no solo el mapeo de conveniencia.

## Un bug real y fundamental encontrado al construir el framework de pruebas: los agentes nunca recibían sus propias tools

Construir el matcher `toHaveCalledTool` de `BaseAgentSpec` (M15) sacó a la luz un bug serio, no descubierto previamente: la llamada `aiAgent()` generada de `ColdBoxAppGenerator` nunca pasaba en absoluto un argumento `tools:` (confirmado contra el código fuente real de bx-ai - `AiAgent.bx` nunca referencia `aiToolRegistry()` internamente). Las `tools/` de un proyecto se copiaban en el build y se hacían resolubles por nombre para el cableado de MCP, pero **ningún agente que BX Agents haya construido jamás - en ningún contexto: una aplicación real servida, `chat`, o un spec de prueba - realmente recibió sus propias tools declaradas.** Esto había pasado desapercibido porque ninguna prueba existente afirmaba jamás sobre una invocación real de tool, solo sobre una respuesta no vacía.

Corregido en `ColdBoxAppGenerator.renderAgentFactory()`: cada `GeneratedAgentFactory.bx` generado ahora carga su propio directorio `tools/` vía un nuevo `ToolRegistryLoader.bx` (usando una ruta ABSOLUTA incrustada en tiempo de generación, no una relativa que dependa de cualquier mapeo "/" que esté en efecto para el contexto de carga - se confirmó que la propia resolución de ruta relativa de `aiToolRegistry().scan("tools")` falla silenciosamente cuando se llama desde un contexto cargado por `DynamicClassLoader` como `chat`), luego pasa `tools: aiToolRegistry().getAll()` a cada llamada de `aiAgent()`.

Una tercera trampa de scripts de nivel superior, encontrada por las malas en CI: **`var` no puede usarse en el nivel superior de un script `.bxs`.** `var` declara en el scope `local`, que solo existe dentro de una función, así que un `var x = ...` de nivel superior lanza `Scope [local] is not available in this context` en tiempo de ejecución - no en tiempo de análisis, así que sobrevive la revisión y solo se dispara en el camino de código que lo alcanza. Costó un ciclo de CI en `runColdBoxIntegrationTests.bxs`, donde la línea culpable estaba dentro de una rama de diagnóstico de fallas y por lo tanto se estrellaba justo cuando algo más ya había salido mal, reemplazando el fallo real con el suyo propio. Se siguen dos reglas: nunca escribir `var` fuera de una función en un `.bxs`, y envolver la salida de diagnóstico en su propio try/catch para que nunca pueda enmascarar el fallo que está describiendo.

**El ajuste de aplicación para un datasource por defecto es `this.datasource`, NO `this.defaultDatasource`.** La clave de registro plural realmente es `this.datasources[ "name" ] = { ... }`, así que `this.defaultDatasource` es lo que todo el mundo instintivamente usa - y BoxLang lo acepta silenciosamente y no hace nada con ello. Verificado directamente contra el runtime en lugar de inferido: con `this.defaultDatasource = "testds"` configurado, un `queryExecute()` sin calificar todavía falla con `No default datasource defined in the application or globally or in the query options. Registered datasources are: [testds]`; cambiando la única línea a `this.datasource = "testds"` resuelve el datasource y la llamada procede a una preocupación real de driver. No hay advertencia y ninguna pista en el error de que el ajuste que escribiste fue ignorado - el mensaje nombra el datasource registrado que estás intentando seleccionar, lo cual se lee como si el mecanismo de selección estuviera roto en lugar de mal escrito.

**El `moduleSettings.qb.defaultOptions` de qb no llegaba a `QueryBuilder` en un arranque real de ColdBox.** El `config/ColdBox.bx` generado establece `moduleSettings.qb.defaultOptions = { datasource : "<name>" }`, y el propio `ModuleConfig.cfc` de qb mapea `QueryBuilder@qb` con `.initArg( name = "defaultOptions", value = settings.defaultOptions )` dentro de `onLoad()` - lo cual se lee como correcto, y es por lo que el `ChatDb.query()` generado originalmente no nombraba ningún datasource. No funcionó: cada query fallaba con `No default datasource defined in the application or globally or in the query options. Registered datasources are: [<name>]`, es decir, el datasource estaba registrado y el builder aún tenía opciones vacías. Por qué el ajuste del módulo no llegaba no se investigó a fondo - dejó de importar, porque nombrar el datasource explícitamente es mejor que depender de ese cableado de todos modos. `ChatDb.query()` ahora llama a `.mergeDefaultOptions( { datasource : static.DATASOURCE } )` en cada builder, reflejando lo que `schemaOptions()` ya tenía que hacer para `SchemaBuilder` (al cual qb nunca le pasa `defaultOptions` en absoluto). Si añades un nuevo camino de llamada de qb al código generado, nombra el datasource en él; no asumas que el ajuste del módulo te cubre.

**Una app generada ahora declara `this.defaultDatasource`, no solo `this.datasources`.** Con un solo datasource, cualquier cosa que ejecute una query sin nombrar uno - ColdBox mismo, otro módulo, el propio código de un proyecto - debería alcanzarlo en lugar de fallar. Nada afirmaba antes sobre el bloque de datasource emitido, que es cómo se envió sin un valor por defecto; `ColdBoxAppGeneratorSpec` ahora cubre tanto el caso con web UI como el caso sin web UI (donde ninguna de las dos líneas debería aparecer).

**El `models/ChatDb.bx` generado no compilaba, y ningún spec unitario lo notó.** `WebUiGenerator` construye código fuente de BoxLang a partir de literales de cadena de BoxLang, donde una comilla doble literal se escribe como una comilla duplicada - así que un literal de cadena vacía necesita CUATRO caracteres de comilla, y escribir las dos de apariencia natural emite una comilla suelta que abre una cadena y engulle el resto de la línea. Dos líneas de plantilla que llevaban un elvis a cadena vacía (`?: ""`) emitían `?: "`, así que cada proyecto de web UI generado enviaba un `ChatDb.bx` que fallaba al analizarse. Cada caso de `WebUiGeneratorSpec` afirmaba sobre subcadenas del texto generado, que todas seguían coincidiendo; solo un arranque real de ColdBox intentaba jamás *compilarlo*, y ese camino estaba a su vez roto durante varios ciclos por los bugs del arnés descritos abajo, así que el bug real permaneció oculto detrás de ellos. Ahora hay un caso de `WebUiGeneratorSpec` que afirma que cada línea del código fuente generado tiene un número par de comillas dobles - un invariante que atrapa a toda la clase en lugar de a las dos líneas que resultaron estar mal. La lección más amplia: un spec de generador que solo hace grep de su propia salida no prueba que la salida sea código válido.

**`GET /chat/api/health` prueba mucho menos de lo que parece.** La acción `health()` generada renderiza un literal `{ status: "ok", success: true }` y no toca nada más - así que un 200 prueba que ColdBox arrancó y el enrutamiento alcanza el `ChatUi` handler generado, y no dice nada sobre WireBox, `ChatDb` o SQLite. El comentario en `runColdBoxIntegrationTests.bxs` solía afirmar que ejercitaba "ColdBox routing -> ChatUi -> WireBox -> ChatDb", lo cual es falso y daba una confianza falsa real: una ejecución de CI devolvió una sonda verde mientras `models/ChatDb.bx` no podía compilarse en absoluto. Corregido en el lugar; trata los specs de integración, no la sonda, como la cobertura del almacén.

**El `JSONReporter` de TestBox no puede reportar sobre specs que tocan singletons de framework en vivo.** Los specs de integración de ColdBox resuelven objetos reales de `controller`/WireBox/qb, y esas referencias de objeto terminan en el memento de resultado de TestBox. `JSONReporter` serializa el memento por completo, así que BoxLang reflexiona sobre los objetos vivos (`StructUtil.objectToStruct` -> `DynamicInteropService.getMethodNames`) y sigue sus referencias cíclicas hasta que la JVM lanza `StackOverflowError`. Los specs mismos ya habían corrido en verde cuando esto se disparó, lo cual es exactamente lo que lo hizo confuso: una suite que pasaba se reportó como una ejecución fallida. `tests/runner-coldbox.bxm` por lo tanto ensambla su propio reporte en su lugar, forzando cada valor a través de un helper `toScalar()` para que ningún objeto pueda llegar al serializador sin importar lo que un spec deje atrás, y registrando los conteos de aprobado/fallido en los marcadores de progreso *antes* de que se construya el reporte - así que un fallo de serialización nunca puede confundirse de nuevo con un fallo de prueba.

**En una máscara de tiempo de BoxLang, `nn` son NANOSEGUNDOS, no minutos - usa `mm`.** `dateTimeFormat( now(), "HH:nn:ss" )` producía silenciosamente timestamps como `02:777491298:32`, que se leen como salida corrupta en lugar de una máscara incorrecta. (Nótese la trampa inversa en una máscara de fecha, donde `mm` es el mes: `yyyy-mm-dd` produce el minuto, no el día.)

**Derivar una identidad del lado del servidor no es lo mismo que autorizar contra ella.** El `handlers/ChatUi.bx` generado tuvo cuidado de derivar `userId` de la sesión en cada acción y nunca del cuerpo del request, y dos acciones aún tenían un agujero de autorización, porque se direccionaban por `threadId` en lugar de por el llamador. `/pending` y `/resume` cargaban un checkpoint por id y actuaban sobre él; `/resume` incluso derivaba `decidedBy` de la sesión, lo cual se lee como una comprobación de scope pero solo gobierna *la etiqueta de la decisión*, no *de quién es la ejecución que se está decidiendo*. Un visitante con el `threadId` de otra persona podía leer sus tool calls pendientes y responder sus aprobaciones. Ambas ahora comparan al llamador contra el `userId` que el agente checkpointeó en las opciones de la ejecución. La regla general que vale la pena recordar de esto: cuando una ruta está indexada por un id opaco en lugar de por el llamador, derivar identidad te da atribución, no control de acceso - las dos necesitan pensamiento separado, y un valor derivado en el servidor sentado en la misma función puede confundirse fácilmente con ambas cosas.

**Las comprobaciones de ruta de `ProjectValidator` deben considerar `..`, no solo un separador inicial.** `database.path` se inserta en una llamada `expandPath()` en el `Application.bx` generado, así que se comprobaba que fuera absoluta - y `../../var/lib/chat.db` escapa del directorio de la app exactamente con la misma efectividad mientras pasa esa comprobación limpiamente. Ahora se rechaza en ambos separadores, coincidiendo segmentos de ruta completos para que nombres legítimos como `..hidden` o `a..b` sigan pasando. Cualquier validador futuro que proteja una ruta generada necesita ambas mitades de esta comprobación.

**Ejecutar la suite de pruebas localmente necesita bx-ai superpuesto desde el código fuente, exactamente como lo hace CI.** `./gradlew downloadModules` obtiene el snapshot *publicado* de bx-ai, que se retrasa respecto a su propia rama de desarrollo - sobrescribe la copia vendida bajo `src/test/resources/modules/bxai` y la suite entonces falla ~40 specs con `Method 'isRunning' not found` y `The method aiGatewayRegistry does not exist`, ambas APIs que existen upstream pero no en el build publicado. Eso no es una regresión, y es fácil malinterpretarlo como una. Reconstruye y superpón antes de ejecutar `testBx`: `( cd <bx-ai checkout> && ./gradlew createModuleStructure )` luego `cp -R <bx-ai>/build/module/. src/test/resources/modules/bxai/`. `tests/coldbox/`, `tests/testbox/` y `tests/qb/` vienen de `box install` (ejecútalo en `tests/`), y los tres están en gitignore.

**`chr()` no existe en BoxLang - el BIF es `char()`.** Esto no depende del contexto, a pesar de un comentario en `ModuleConfig.bx` que solía afirmar que no estaba disponible solo "en este contexto de ejecución de CLI de módulo" (ahora corregido). Verificado directamente contra el runtime: `char( 10 )` devuelve el salto de línea, `chr( 10 )` lanza `Function [chr] not found`, tanto en un script CLI simple como en una plantilla servida por igual. Vale la pena saberlo porque el fallo es uno de *tiempo de ejecución* - `chr()` se analiza bien y sobrevive la revisión, luego lanza en cualquier camino de código que primero lo alcance. Costó varios ciclos de CI aquí: `tests/runner-coldbox.bxm` llamaba a `chr( 10 )` en su primer marcador de progreso, así que cada request devolvía 500 al entrar, y el bucle de reintento descrito abajo convirtió ese error simple de una línea en un opaco "HTTP 408, never responded".

Una cuarta lección del mismo arnés de CI, y la que costó más ciclos: **la instrumentación que un reintento puede sobrescribir no registra nada.** `tests/runner-coldbox.bxm` comenzaba truncando su propio archivo de progreso (`fileWrite( progressFile, "" )`), y `runColdBoxIntegrationTests.bxs` reintentaba el request del corredor una vez por segundo hasta una fecha límite. Así que cada reintento borraba los marcadores que el primer intento colgado había escrito, y los diagnósticos de fallo imprimían fielmente un archivo vacío - lo cual se leía como "la página nunca llegó a ninguna parte" cuando la verdad era "se eliminó la evidencia". Peor aún, los reintentos eran activamente dañinos independientemente de los diagnósticos: la página del corredor no es ni barata ni idempotente (arranca TestBox y ejecuta cada spec de integración), así que reintentarla apilaba ejecuciones de pruebas completas concurrentes sobre un pool de trabajadores de MiniServer de solo un puñado de hilos - una forma de causar un colgado, no de recuperarse de uno. Ambos están corregidos: el orquestador limpia el archivo de marcadores una vez y hace exactamente un request (la sonda de health check antes de él ya ha probado que el servidor está arriba, así que no hay nada que un reintento pueda esperar), y la página solo añade en adelante, etiquetando cada línea con un UUID por request para que los intentos superpuestos permanezcan distinguibles. La regla general: un registro de progreso debe ser de solo-adición y debe sobrevivir a lo que está instrumentando, y cualquier cosa costosa o con estado no debería estar detrás de un bucle de reintento en absoluto.

Una peculiaridad relacionada, separada, de BoxLang encontrada mientras se diagnosticaba esto: **`request` es un nombre de scope incorporado reservado.** Una variable local/de bucle llamada `request` puede sombrearlo silenciosamente - `for ( var request in someArray ) { request.someKey }` iteró el número correcto de veces, pero cada acceso `request.someKey` dentro de él leía silenciosamente el scope incorporado vacío en lugar de la variable de bucle, sin ningún error en absoluto. Corregido renombrando a `recordedRequest` en los matchers de `BaseAgentSpec.bx` - vale la pena recordarlo para cualquier código futuro de BoxLang en este proyecto que itere sobre algo razonablemente llamado `request`.

## La búsqueda del miniserver de `serve` es solo por PATH

`serve` busca `boxlang-miniserver` solo en `PATH` (`MiniServerLauncher.findExecutable()`). No hay respaldo a una ruta configurada o a un binario incluido - si no está instalado y en `PATH`, `serve` falla con un error claro y accionable, pero no hay forma alternativa de apuntarlo a uno. `invoke --server` reutiliza `serve` internamente, así que hereda la misma búsqueda solo por PATH - su propia prueba de `InvokeSpec.bx` para el round-trip HTTP real comprueba `MiniServerLauncher.findExecutable()` primero y se omite (en lugar de fallar) cuando no hay ningún binario real en `PATH`, el mismo idioma que `RuntimeStartupSmokeSpec.bx` ya usa para su propia comprobación de presencia del jar. Haz al menos una ejecución manual de `bxAgents invoke --message=... --server` en una máquina con `boxlang-miniserver` instalado antes de depender del camino HTTP real en producción - el mismo enfoque honesto ya usado en otro lugar de esta página para vacíos que esta suite no puede cerrar por sí misma en cada entorno.

## El home de runtime de BoxLang delimitado alcanza a `serve`/`invoke --server` incondicionalmente; los verbos en proceso solo si tu instalación de BoxLang carga `.env`

`serve` delimita el propio home de runtime de BoxLang del miniserver a `.build/runtime` vía `serverHome` (un campo real y confirmado de `MiniServerConfig` - el propio texto de ayuda de CLI de `MiniServer` de `boxlang-web`: `-s, --serverHome <PATH>  BoxLang server home directory (default: ~/.boxlang)`), así que la caché de clases compiladas de cada proyecto y cualquier override de configuración están aislados por proyecto en lugar de compartidos globalmente en `~/.boxlang`. `invoke --server` hereda esto ya que reutiliza `serve` internamente. Esta parte es incondicional - nosotros mismos escribimos la configuración de lanzamiento para ese proceso.

`chat`, `build`, `test`, e `invoke` en proceso por defecto son diferentes: se ejecutan dentro del proceso `bxAgents` ya iniciado, cuyo propio runtime de BoxLang - y por lo tanto su home - se resolvió antes de que cualquiera de nuestro código de BoxLang, incluyendo el propio `main()` de `ModuleConfig.bx`, tuviera oportunidad de ejecutarse (el motor tiene que existir para interpretar ese código en absoluto). `BoxRuntime` es un singleton a nivel de JVM; su home está fijado en la primera inicialización, así que nada que una clase de verbo haga desde dentro de ese proceso ya en ejecución puede cambiarlo retroactivamente.

SÍ hay una palanca real para esto, sin embargo: `BoxRunner` (el propio punto de entrada de CLI de núcleo de BoxLang, no solo el miniserver) lee una variable de entorno `BOXLANG_HOME` genuina antes de que `BoxRuntime` se inicialice - confirmado directamente contra el jar de runtime real (no solo la documentación): ejecutarlo con `BOXLANG_HOME=<path>` configurado como una variable de entorno de SO real (las rutas relativas se resuelven contra el CWD, igual que una ruta absoluta) puebla la estructura de home completa en esa ruta en lugar de `~/.boxlang`, cada vez. `new` genera un `.env` que declara `BOXLANG_HOME=.build/runtime` - la misma ruta que usa `serve` - exactamente por esta razón (reflejando la propia convención `.env`/`BOXLANG_HOME` de `ortus-boxlang/bx-ai-intro`), en la **raíz del proyecto** - es decir, el directorio desde el que un usuario realmente ejecuta `bxAgents <verb>` para los verbos en proceso, que es el lugar correcto para que un cargador de `.env` basado en CWD lo encuentre.

**Lo que confirmamos, y lo que no pudimos.** `boxlang-miniserver` (lo que lanza `serve`) sí tiene carga automática de `.env` real e incorporada - confirmado descompilando y luego realmente ejecutando `ortus.boxlang.web.MiniServer` directamente: con ningún `envFile` configurado, resuelve `.env` en relación a el **webRoot** del servidor (no la raíz del proyecto) y, si se encuentra, lo carga como Java `Properties`, aplicando cada clave vía `System.setProperty()` - los valores reales cargados de esta manera SÍ son visibles para el código de BoxLang vía `getSystemSetting()`. El jar de runtime de núcleo crudo (`BoxRunner`, usado por `chat`/`build`/`test`/`invoke` por defecto) no tiene lógica equivalente en ninguna parte de él (confirmado haciendo grep en cada clase del jar en busca de `.env`) - así que esos verbos solo captan `.env` si la propia CLI `boxlang` que tienes instalada (un lanzador nativo provisto por BVM, no este jar crudo) hace su propia carga de `.env` antes de que la JVM arranque, de la manera en que `ortus-boxlang/bx-ai-intro` depende de ello. Eso es plausible y consistente con el uso en el mundo real de ese proyecto, pero no es algo que este sandbox tenga el binario real para verificar directamente.

Una peculiaridad específica y confirmada incluso donde sí ocurre la carga de `.env`: **`BOXLANG_HOME` mismo no tiene efecto a menos que sea una variable de entorno de SO real** - no una propiedad de sistema de la JVM, y no un flag `-D`. Verificado dos veces directamente contra los jars reales: (1) ejecutar `boxlang-miniserver` contra un webRoot cuyo `.env` declaraba `BOXLANG_HOME=customhome` cargó el archivo (confirmado vía su propia línea de log "Loaded environment variables from:" y vía `getSystemSetting()` devolviendo correctamente otros valores de `.env`), sin embargo el servidor aún registraba `Logs Directory: /root/.boxlang/logs` - el home por defecto, no `customhome`; (2) lanzar `BoxRunner` directamente con `-DBOXLANG_HOME=<path>` (una propiedad de sistema de la JVM, sin `.env` involucrado) tampoco tuvo efecto en el home resuelto, aunque `getSystemSetting( "BOXLANG_HOME" )` devolvía felizmente el valor del flag desde código BoxLang. Así que la resolución de `BOXLANG_HOME` específicamente solo lee la variable de entorno de SO verdadera - a diferencia de la mayoría de los ajustes, que `getSystemSetting()` devuelva un valor para ella no significa que el home del runtime realmente se haya movido. Si tu CLI de `boxlang` carga `.env` de la misma manera en que lo hace internamente MiniServer (`System.setProperty` después de que la JVM ya ha arrancado) en lugar de exportar una variable de entorno real antes de lanzar la JVM, `BOXLANG_HOME` en `.env` no alcanzará el home del runtime aunque `.env` mismo se cargue con éxito - todo lo demás en él seguirá funcionando. Si tu instalación no te da esto en absoluto, cárgalo tú mismo antes de ejecutar comandos (por ejemplo, `set -a; source .env; set +a`) para obtener el mismo aislamiento que `serve` ya obtiene incondicionalmente.

## El paso de conveniencia `box install` de `new` no está ejercitado por una prueba automatizada

`new` ejecuta `box install` dentro de la carpeta `tests/` generada por defecto, así que `bxAgents test` funciona de inmediato (ver [Referencia de CLI](cli-reference.md)). Esta es una operación de red real (CommandBox resuelve `testbox` contra ForgeBox) - confirmada de tomar ~25 segundos y fallar con un error de certificado en este sandbox de desarrollo específicamente, la misma restricción de ForgeBox-inalcanzable ya señalada en otro lugar del propio tooling de este proyecto. `NewSpec.bx` solo ejercita el camino rápido y determinista `--skipInstall` (y las invocaciones de `new` de `ModuleCliProcessTest.java` de proceso de SO / `ModuleConfigCliSpec.bx` en proceso también pasan `--skipInstall` por la misma razón) - el propio camino de intento de instalación por defecto se verifica solo por pruebas manuales, no por CI.

## `chat` necesita una TTY real

`chat` usa el propio `MiniConsole` de BoxLang, que invoca `stty` para configurar el modo raw de terminal - solo puede ejecutarse contra una terminal interactiva genuina. No funciona canalizado, redirigido, ni desde un proceso no interactivo (un job de CI, un script). No hay modo de respaldo no interactivo.

## Corregido: `chat` e `invoke` por defecto (sin `--server`) solían fallar para un `Agent.bx` basado en clase

Anteriormente, tanto `chat` como `invoke` por defecto lanzaban `The requested class [agent.classes.agentClass] has not been located in any class resolver.` antes de siquiera alcanzar al agente. Causa raíz: ambos verbos cargan el `GeneratedAgentFactory.bx` generado en proceso vía `DynamicClassLoader.instantiate()` (una llamada cruda de `RunnableLoader` contra una ruta absoluta, sin contenedor ColdBox involucrado), y la factory generada solía instanciar un `Agent.bx` basado en clase vía una búsqueda de ruta con puntos **relativa**, `new "agent.classes.agentClass"()` - que solo se resuelve una vez que algo ha registrado un mapeo que hace resoluble la raíz de la app, y nada lo hacía fuera de un arranque real de ColdBox.

Registrar uno a mitad de script (`Configuration.registerMapping( "/", appDir )` justo antes de `DynamicClassLoader.instantiate()`) tampoco lo corrige - confirmado empíricamente reproduciendo a mano la misma secuencia exacta en un script `.bxs` independiente. La misma clase de limitación ya documentada arriba para el descubrimiento de TestBox de `TestRunnerLauncher`: un mapeo registrado vía `Configuration.registerMapping()` a mitad de script no se propaga de forma confiable a las búsquedas de ruta relativa propias de una clase hechas dentro de ese mismo proceso.

**Corrección:** `ColdBoxAppGenerator.copyAgentClass()` ahora devuelve la propia ruta de archivo absoluta de la clase copiada (en lugar de una ruta de componente con puntos), y `renderClassBasedAgentStatement()` la instancia vía `DynamicClassLoader.instantiate( absolutePath, context )` - la misma primitiva exacta que `chat`/`invoke` ya usan para cargar `GeneratedAgentFactory.bx` mismo - en lugar de un `new "..."()` relativo. Esto evita la resolución de mapeo por completo, así que ahora funciona idénticamente sin importar si un contenedor ColdBox real está arrancado. Confirmado contra `examples/class-based-agent/`: `chat`, `invoke` por defecto, `invoke --server`, y `serve` ahora construyen y ejecutan correctamente el mismo agente.

## Ninguna prueba de humo de instalación del `executable` de box.json

`box.json` declara `"boxlang": { "executable": "bxAgents" }` así que una instalación real de módulo produce un comando `bxAgents` nativo (ver [Instalación](getting-started/installation.md)). Este cableado en sí no está ejercitado por una prueba automatizada - depende del propio comportamiento documentado del instalador de módulos de BoxLang para generar wrappers ejecutables, verificado leyendo su código fuente, no por una prueba de instalar-y-ejecutar en el propio CI de este repositorio.

## `schedules/Scheduler.bx` no se valida en tiempo de build

Ya que es código real, escrito a mano, de ColdBox pasado sin tocar (ver [schedules/](conventions/schedules.md)), `build` no puede comprobarlo de manera significativa de la forma en que solía comprobarse la configuración `{ cron, action }` - un error de sintaxis, un typo en una llamada `getInstance( "..." )`, o referenciar un nombre de agente que no existe, todo pasa `build` limpiamente y solo sale a la superficie cuando la app generada realmente arranca (`serve`), igual que cualquier otra clase real de BoxLang cuyo contenido este proyecto no posee. `build` sí atrapa un error más estrecho y adyacente: dos agentes (raíz o subagente, a cualquier profundidad) declarando el mismo `name` - eso es una colisión de binding de `config/WireBox.bx` que este proyecto genera él mismo, así que se comprueba en tiempo de validación igual que todo lo demás que genera.

## Un bug real encontrado al construir el verbo `test`: resolver "el jar actual de BoxLang" es ambiguo una vez que `boxlang-miniserver` está en el classpath

`TestRunnerService.bx` genera un proceso hijo nuevo para ejecutar los `tests/specs` de un proyecto, y necesita saber con qué jar lanzarlo. La primera implementación usó el truco estándar de "de qué jar se cargó esta clase" (`BoxRuntime.class.getProtectionDomain().getCodeSource().getLocation()`) - esto funcionó en pruebas manuales aisladas, pero falló impredeciblemente una vez ejecutado como parte de la propia suite completa `testBx` de este proyecto, que también necesita `boxlang-miniserver-*.jar` en el classpath (para los specs relacionados con `serve`/`MiniServerLauncher`). Confirmado vía inspección directa: **`boxlang-miniserver-*.jar` es un fat jar que empaqueta su propia copia de `ortus.boxlang.runtime.BoxRuntime`** - con ambos jars en el classpath, el classloader puede resolver `BoxRuntime.class` al jar del miniserver en lugar del jar de runtime real, lanzando silenciosamente `MiniServer.main` (que rechaza `--bx-config` y sale con 1 inmediatamente, antes de que jamás se produzca ningún script BoxLang o reporte de prueba) en lugar de `BoxRunner.main`. Esto se manifestó como los casos de proceso real de `tests/specs/cli/TestSpec.bx` fallando con `exitCode=1` y un reporte vacío - reproducible solo cuando se ejecuta a través de la suite completa, no de forma aislada, lo cual es lo que hizo fácil pasarlo por alto.

Corregido resolviendo el jar desde `java.class.path` en su lugar - escaneando en busca de una entrada `boxlang-*.jar` que **no** contenga `miniserver`, recayendo en el viejo truco de codeSource solo si no se encuentra tal entrada.

## Los destinos `ssh`/`docker`/`digitalocean` de `deploy` no están ejercitados por una prueba automatizada de proceso real

`SshTargetSpec.bx`/`DockerTargetSpec.bx` afirman sobre el comando `scp`/`ssh`/`docker` exacto que construye cada destino (un array de argumentos real de `ProcessBuilder`) sin invocar jamás el binario real - el mismo enfoque de "capturar, no ejecutar" usado en otra parte de esta suite para cualquier cosa que necesite un binario que podría no estar en `PATH` en CI (ver los saltos de `assumeTrue` de `MiniServerLauncherTest`). La lógica de copia de `local` SÍ está ejercitada de verdad (`LocalTargetSpec.bx`), incluyendo una prueba de regresión para un bug latente real que este refactor corrigió: el `Deploy.bx` original elegía el `.bxa` "más nuevo" vía un ordenamiento léxico de nombre de archivo, que silenciosamente elige mal una vez que un proyecto alcanza versiones de dos dígitos (`v9.0.0` ordena después de `v10.0.0`) - corregido ordenando por el tiempo de modificación de archivo real (`DistArtifactLocator`).

`DigitalOceanTargetSpec.bx` de forma similar solo hace pruebas unitarias de la lógica pura de `buildAppSpec()`/`findExistingAppId()` - las llamadas reales `GET/POST /v2/apps` nunca se ejercitan en CI, ya que eso necesita una cuenta real de DigitalOcean y un token de API. Haz al menos un `deploy --name=<ssh-entry>` manual contra una VM desechable real, y un `deploy --name=<digitalocean-entry>` contra una cuenta real de DO con una app desechable, antes de depender de ninguno de los dos en producción - el mismo enfoque honesto ya usado arriba para el vacío de pruebas solo-con-proveedor-mock.

## Los destinos `ftp`/`sftp` de `deploy`: el manejo de conexión real está probado, una carga exitosa real no

A diferencia de `ssh`/`docker` (que invocan binarios externos y así solo pueden tener su *construcción de comando* probada sin un servidor real), `ftp`/`sftp` llaman al componente `bx:ftp` real del módulo [`bx-ftp`](https://github.com/ortus-boxlang/bx-ftp) **en proceso** - no hay opción de "capturar el comando, no ejecutar". `BaseFtpTargetSpec.bx` en su lugar hace un intento de conexión genuino contra `127.0.0.1` en un puerto en el que nada escucha, y afirma que el error real de conexión rechazada se captura y se relanza como un `BxAgents.DeployFailed` claro (confirmado vía el propio código fuente de bx-ftp que cada acción lanza en caso de fallo en lugar de devolver un `succeeded: false` suave). Esto prueba el camino real de conectar/envolver-error/limpiar de extremo a extremo - lo que no puede probar es una carga exitosa real.

Ese vacío es específicamente porque **este sandbox de desarrollo no tiene salida TCP cruda alguna** - solo HTTPS a través del proxy del entorno (confirmado directamente: tanto `curl ftp://test.rebex.net` como una conexión cruda `/dev/tcp` a un host FTP público colgaron y expiraron por tiempo de espera, y `docker info` no muestra ningún daemon corriendo, así que los propios servidores de prueba FTP/SFTP de Docker que trae bx-ftp tampoco pudieron iniciarse aquí). Esta es una restricción del sandbox, no una limitación de código - haz al menos un `deploy --name=<ftp-entry>` y un `deploy --name=<sftp-entry>` manuales contra un servidor real alcanzable (o los propios servidores de prueba `docker-compose up` de bx-ftp, desde una máquina que tenga acceso a Docker/red) antes de depender de ninguno de los dos en producción, el mismo enfoque honesto ya usado arriba para `ssh`/`digitalocean`.

## `build` no revierte un `.build/app` parcialmente escrito ante un fallo a mitad de generación

`BuildPipeline.build()` elimina y recrea `.build/app` de antemano, luego ejecuta los generadores de la Fase 5 en secuencia. Cada entrada que la Fase 3 (`ProjectValidator`) puede comprobar se comprueba antes de que nada de eso ocurra, así que un fallo genuino a mitad de generación debería ser raro en la práctica - pero si aun así ocurre uno (por ejemplo, una entrada de `models/`/`schedules/`/`mcp/` que falla al cargar, o un problema de entorno/sistema de archivos), `.build/app` queda en disco en un estado parcialmente escrito en lugar de restaurarse a su contenido previo o limpiarse. Un `build` exitoso posterior lo sobrescribe limpiamente, así que esto no es pegajoso, pero cualquier cosa que inspeccione `.build/app` entre un build fallido y el siguiente (un paso de CI, un reintento manual de `package`) puede ver una app rota, medio generada. Todavía no hay un paso de rollback/directorio-temporal-luego-intercambio.

## Se observó un `StackOverflowError` intermitente en `testBx`, no relacionado con el propio código de este proyecto

Mientras se investigaba este hito, `./gradlew testBx` ocasionalmente (no en cada ejecución) estrellaba toda la JVM con un `StackOverflowError` dentro de la propia serialización JSON genérica de objetos del motor de BoxLang (`DynamicObjectSerializer`/`BoxStructSerializer` llamándose mutuamente, alternando, hasta que se agota la pila - confirmado que sigue ocurriendo con `-Xss16m`, así que es un ciclo genuino, no meramente una estructura profunda pero finita). Aislado vía `git stash` (el fallo se reprodujo idénticamente sin ninguno de los cambios de esta sesión aplicados, contra el mismo commit exacto ya en `development`) y vía bisección de `tests/specs/**` en cada subdirectorio tanto individualmente como combinados (cada subconjunto, e incluso cada subconjunto-menos-uno, corrió limpio) - solo la única ejecución completa lo reproduce ocasionalmente, y volver a ejecutar la misma suite completa inmediatamente después de un fallo a veces pasa limpio. Esto apunta a una condición de carrera dependiente del tiempo (interactuando plausiblemente con la propia tarea `everySecond()` en segundo plano real de `ExampleScheduler`, que imprime desde su propio pool de hilos concurrentemente con cualquier spec que esté ejecutándose en ese momento) más que a un bug en algún spec particular o en el propio código generado de este proyecto. Si `testBx` falla con un `StackOverflowError` y ninguna otra explicación, reinténtalo antes de asumir una regresión real - actualmente no es reproducible a voluntad, así que no existe ninguna prueba de regresión automatizada para ello, y llevarlo hasta una única causa raíz estaba fuera del alcance de esta ronda.

## Gateways de estilo push (Telegram) probados solo contra un seam mockeado de API/scheduler - ninguna integración de plataforma en vivo corre en CI

`TelegramGatewaySpec.bx` ejercita la propia lógica de `TelegramGateway` (normalización entrante, fragmentación saliente en el límite de 4096 caracteres, construcción de teclado inline HITL, registro/eliminación de tarea de scheduler) enteramente contra un seam de prueba inyectable `apiCaller`/`setScheduler()` - nunca una llamada real a la API de bots de Telegram, y nunca un arranque real de scheduler de ColdBox. Esto prueba que el propio código del gateway es correcto, no que realmente funciona de extremo a extremo contra la API real de Telegram o un scheduler real en ejecución. Haz al menos un `bxAgents serve` manual contra un proyecto con un bot de Telegram real respaldado por `botTokenEnvVar` antes de depender de él en producción - el mismo enfoque honesto ya usado arriba para los vacíos de pruebas solo-con-proveedor-mock/sin-conexión-en-vivo en otras partes de este archivo. La misma advertencia se aplicará a cada futuro gateway de estilo push (Slack, Discord, Email, WhatsApp) construido de la misma manera.

`SlackGatewaySpec.bx` lleva el mismo vacío, un nivel más profundo: la conexión websocket persistente de `SlackGateway` se prueba vía un seam inyectable `setSocketOpener()` (un objeto falso que sustituye al `java.net.http.WebSocket` real), así que cada aserción de manejo de frame/lógica de reconexión en el spec corre con cero E/S de red real. Lo que sí se verificó genuinamente de forma directa (no mockeado): una prueba de humo independiente instanció el `SlackSocketListener(gateway)` real (que `implements="java:java.net.http.WebSocket$Listener"` directamente - BoxLang lo compila como un implementador de JVM genuino, sin necesidad de proxy) y llamó al `HttpClient.newWebSocketBuilder().buildAsync(...)` real contra una dirección inalcanzable, confirmando que la propia interoperabilidad de BoxLang a Java funciona correctamente hasta la frontera de red (falló con un simple `java.net.ConnectException`, no un error de casting/interoperabilidad) - pero ninguna prueba aquí ha completado jamás un handshake real de Socket Mode contra los servidores reales de Slack. Haz al menos un `bxAgents serve` manual contra un proyecto con credenciales reales de app de Slack respaldadas por `botTokenEnvVar`/`appTokenEnvVar` antes de depender de él en producción.

`DiscordGatewaySpec.bx` lleva el vacío idéntico, por la razón idéntica: la lógica de manejo de frame/latido/reconexión de `DiscordGateway` se ejercita enteramente contra seams inyectables `setApiCaller()`/`setSocketOpener()`, cero E/S de red real. La misma disciplina de prueba de humo independiente se aplicó aquí también - `gateway.onConnect()` conducido contra una llamada real de `HttpClient.newWebSocketBuilder().buildAsync(...)` a una dirección inalcanzable falló con un simple `java.net.ConnectException`, no un error de casting/interoperabilidad, confirmando que la cadena de interoperabilidad funciona. Lo que NO se verificó: un handshake real de Gateway (`Hello` → `Identify` → `READY`) contra los servidores reales de Discord, temporización real de latido bajo la propia tolerancia de Discord, o que el valor por defecto de `intents` (`GUILDS`+`GUILD_MESSAGES`+`DIRECT_MESSAGES`+`MESSAGE_CONTENT` = `37377`) sea realmente suficiente para recibir contenido de mensaje una vez que `MESSAGE_CONTENT` está habilitado/aprobado para un bot real en el Discord Developer Portal. Haz al menos un `bxAgents serve` manual contra un proyecto con un bot real de Discord respaldado por `botTokenEnvVar` (con `MESSAGE_CONTENT` habilitado) antes de depender de él en producción.

`EmailGatewaySpec.bx` lleva una versión mayor del mismo vacío. El IMAP entrante se prueba enteramente vía un seam inyectable `setImapPoller()` (structs de mensaje normalizados enlatados, sin buzón real), y el saliente se prueba enteramente vía un seam inyectable `setMailService()` (`FakeMailService`/`FakeMail`, sustituyendo a `MailService@cbmailservices` - `EmailGateway` nunca pasa por WireBox real en absoluto en estos specs, ya que no hay arranque real de ColdBox). Lo que sí se verificó genuinamente de forma directa esta sesión (no mockeado, no asumido): la superficie real de API `jakarta.mail` de la que depende `fetchInboundMessages()` (`Session.getDefaultInstance()`, `Flags`/`Flags.Flag`/`FlagTerm`, `Store.getStore("imaps")`, `Folder.READ_WRITE`, `MimeMultipart`, `InternetAddress`) contra los jars reales de `jakarta.mail-api`/Angus Mail (descargados de forma independiente para esto - no están vendidos en el propio classpath de prueba de este repositorio), confirmando que cada nombre de clase/método usado realmente existe y se resuelve; un `Store.connect()` real contra una dirección inalcanzable, conducido a través del propio `EmailGateway.pollInbox()` (no un helper omitido), falló con un simple error de tiempo de espera de conexión, no un error de interoperabilidad/casting, confirmando que la cadena de interoperabilidad alcanza correctamente la frontera de red real, la misma disciplina que las pruebas de humo de websocket de Slack/Discord. Lo que explícitamente NO se verificó, y es un vacío estrictamente mayor que el de los gateways de plataforma de chat: ningún handshake IMAP real contra un buzón real, ningún módulo real `cbmailservices`/`bx-mail` instalado en ninguna parte de este repositorio o su arnés de pruebas (ninguno está vendido de la forma en que lo están `bx-ai`/TestBox - ver la entrada de retraso de snapshot abajo para ese mismo workaround aplicado a un módulo diferente), así que el camino de resolución de WireBox (`MailService@cbmailservices` realmente existiendo, el `bx:mail` de `BXMail` realmente enviando) nunca se ha ejercitado en absoluto, mockeado o real, en este código base. Haz al menos un `bxAgents serve` manual contra un proyecto con credenciales IMAP reales Y una instalación real de `cbmailservices`/`bx-mail` (confirma que `box install` tuvo éxito y que `moduleSettings.cbmailservices` se resuelve) antes de depender de este gateway en producción - este es el menos verificado de los cuatro gateways de estilo push enviados hasta ahora.

`WhatsAppCloudGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el camino de verificación de firma se ejercita con firmas HMAC-SHA256 genuinamente computadas (`javax.crypto.Mac`/`SecretKeySpec`, verificadas de forma cruzada e independiente esta sesión tanto contra `openssl dgst -hmac` como contra el propio módulo `hmac` de Python antes de confiar en el cómputo de BoxLang - se atrapó una discrepancia real de vector de referencia que resultó ser un typo en el valor esperado copiado a mano, no un bug, pero solo la verificación cruzada lo atrapó), el handshake de verificación, el despacho/deduplicación de webhook, el envío saliente, y el renderizado de botón/lista interactivo se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada HTTP saliente a la API de Graph siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: las propias llamadas de contexto de request de ColdBox del `handlers/WhatsAppCloud.bx` generado (`event.getHTTPContent()`/`event.getHTTPHeader()`/`event.renderData()`, el acceso a parámetros de query de clave con puntos fusionada en el scope URL de `rc` para el handshake GET) contra un arranque real de ColdBox - estos SÍ son los idiomas documentados y estándar de handler REST de ColdBox (confirmados contra la propia receta de documentación "Building REST APIs" de ColdBox, no adivinados), un punto de partida significativamente más confiable que la suposición de clave `aiGatewayRegistry()` no documentada que resultó estar equivocada en otra parte de este archivo - pero "documentado" no es lo mismo que "probado que funciona en este contexto generado" - se difirió en lugar de arriesgar ese radio de impacto para lo que es un vacío de menor confianza-de-estar-realmente-equivocado que el bug de clave de registro. Haz al menos un `bxAgents serve` real + una prueba real de webhook de Meta (o `curl`) contra `/webhooks/whatsapp-cloud` antes de depender de esta ruta en producción. Tampoco se ha hecho jamás una llamada real a la API de Graph - la capa HTTP de `deliver()`/`requestHumanInteraction()` se ejercita solo vía el seam de prueba `apiCaller`.

`TeamsGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: la verificación de JWT se ejercita contra un par de claves RSA de 2048 bits genuinamente generado (`java.security.KeyPairGenerator`) y JWTs de prueba firmados a mano construidos enteramente dentro del spec (sin fixtures precomputadas, sin dependencia externa de `openssl` en tiempo de prueba) - una firma válida se acepta y despacha, mientras que una firma manipulada, un `aud` incorrecto, un `iss` incorrecto, y un `exp` expirado se confirman de forma independiente cada uno como rechazados con 401. El camino de actividad de invocación (clic de botón de Adaptive Card), el despacho de mensajes, el filtrado exclusivo de scope personal, el enhebrado vía `replyToId`, la fragmentación, y el renderizado de Adaptive Card se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada REST de Connector saliente siendo stubbeada (`setApiCaller()`) y las obtenciones de JWKS/token OAuth2 siendo stubbeadas (`setJwksFetcher()`/`setTokenFetcher()`). Lo que NO se verificó: las propias llamadas de contexto de request de ColdBox del `handlers/Teams.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que el propio handler de WhatsApp Cloud, y diferido por la razón idéntica - ver esa entrada arriba); nunca se ha hecho una obtención real de token OAuth2 ni una llamada REST de Connector real contra los endpoints reales de Microsoft; y la compensación de JWKS-cacheado-por-la-vida-de-la-instancia (ver la sección de Teams de `docs/conventions/gateways.md`) significa que un escenario real de rotación de claves tampoco se ha ejercitado jamás. Haz al menos un `bxAgents serve` real + un registro real de app de Teams (App ID/contraseña del portal Azure/Bot Framework) + un cliente real de Teams enviando un DM antes de depender de este gateway en producción.

`TwilioGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el camino de verificación `X-Twilio-Signature` HMAC-SHA1/base64 se ejercita con una firma genuinamente computada construida en línea en el spec, verificada de forma cruzada e independiente esta sesión contra los propios módulos `hmac`/`hashlib` de Python antes de confiar en la implementación de BoxLang (la misma disciplina que la propia verificación cruzada HMAC-SHA256 de WhatsApp Cloud) - una firma de referencia real se computó en Python para una combinación conocida de token de autenticación/URL/parámetros y se confirmó que coincide exactamente con la salida de BoxLang. El análisis de cuerpo con codificación de formulario (incluyendo un `+` literal viajando correctamente de ida y vuelta a través de la codificación de porcentaje `%2B`), el override de `publicUrl` para despliegues de proxy inverso/túnel, el modelo dual de TwiML-ack-luego-respuesta-REST-asíncrona, la fragmentación de salida, y la correlación de respuesta HITL indexada por número de teléfono se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada HTTP saliente a la API de Messages siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: la propia llamada `event.getUrl()` del `handlers/Twilio.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que los propios handlers de WhatsApp Cloud/Teams, diferido por la razón idéntica) - `event.getUrl()` es un método documentado de Routable/Request Context de ColdBox (confirmado vía el MCP de documentación de ColdBox), no una suposición, pero "documentado" no es "probado en este contexto generado". Tampoco se ha hecho jamás una llamada real a la API de Messages de Twilio. Haz al menos un `bxAgents serve` real + una prueba real de webhook de número de teléfono de Twilio antes de depender de esta ruta en producción - y nótese que la correlación HITL indexada por número de teléfono (ver la sección de Twilio de `docs/conventions/gateways.md`) tiene una limitación real y documentada: una segunda solicitud HITL al mismo número de teléfono antes de que se responda la primera sobrescribiría la entrada `pendingApprovals` de la primera, huerfanándola silenciosamente. Tampoco hay lista blanca/limitación de tasa construida para SMS entrante - a diferencia de Eve, que documenta (pero no aplica en código) que su propia configuración `allowFrom` es "obligatoria", este puerto no tiene ninguna compuerta equivalente en absoluto; cualquier número de teléfono puede enviar mensajes a un número desplegado de Twilio y alcanzar al agente.

`GitHubGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada, y la lógica central de este gateway se condujo además a través de una prueba de humo independiente de BoxLang real durante el desarrollo (no solo el spec permanente) - así fue como se atrapó un bug genuino antes de que jamás llegara a la suite de pruebas: la lógica de subcadena del helper de extracción de mención llamaba a `left( body, 0 )` cada vez que la `@mention` ocurría justo al comienzo de un comentario (un caso muy común), y el `left()` de BoxLang lanza `"Count cannot be zero"` en lugar de devolver una cadena vacía para un conteo de cero - confirmado disparándolo con un cuerpo de comentario real, luego corregido ramificando explícitamente alrededor del caso de longitud cero en lugar de asumir que `left()`/`mid()` toleran eso. La verificación de `X-Hub-Signature-256`, la compuerta de regex-lookahead de `@mention` (confirmada vía una prueba de humo dedicada de que un bot llamado `mybot` NO se dispara en `@mybot2`), las protecciones contra bucle de bot, la deduplicación de id de entrega, la identidad de conversación de issue-vs-hilo-de-review, `deliver()`, y la correlación HITL de `@mention`-a-respuesta se conducen todos a través de los métodos públicos reales del gateway con solo la propia llamada REST de GitHub saliente siendo stubbeada (`setApiCaller()`). Lo que NO se verificó: el `handlers/GitHub.bx` generado contra un arranque real de ColdBox (la misma categoría de vacío que el propio handler de cada otro gateway de webhook en este proyecto, diferido por la razón idéntica). Nunca se ha hecho una llamada real a la API de GitHub, y nunca se ha usado una GitHub App/PAT real contra un repositorio real - haz al menos un `bxAgents serve` real + un webhook real de GitHub configurado contra un repositorio de prueba antes de depender de este gateway en producción.

`SignalGatewaySpec.bx` cubre la propia lógica del gateway de forma exhaustiva y real, no mockeada: el análisis JSON-RPC/SSE de `handleSseEvent()` (manejo de línea en blanco/JSON inválido, filtrado de mensaje de grupo, enhebrado de cita, respaldo a `sourceUuid` cuando falta un nombre para mostrar), la forma de envío y fragmentación de `deliver()`, y la correlación/coincidencia de decisión HITL se conducen todos a través de los métodos públicos reales del gateway con solo las llamadas de E/S salientes `rpcCaller`/`connector` siendo stubbeadas, la misma disciplina de prueba de seam que cada otro gateway en este proyecto. Dos hallazgos de nivel BoxLang salieron a la superficie durante el desarrollo, ambos resueltos y vale la pena registrarlos como peligros generales en lugar de bugs específicos del gateway: (1) un script de prueba de humo desechable que nombró una variable local `request` estaba interactuando silenciosamente con el propio scope reservado `request` de BoxLang en lugar de crear una variable simple, produciendo errores engañosos de "método no encontrado"/"discrepancia de tipo de argumento" desde `HttpClient.send()` que se veían exactamente como una limitación genuina de interoperabilidad Java pero desaparecían por completo una vez que se renombró la variable - `SignalGateway.bx` mismo nunca tuvo un bug; (2) un `try/catch` colocado directamente en el nivel superior de un script de prueba de humo `.bxs` independiente (no dentro de una función) disparó un `java.lang.VerifyError: Inconsistent stackmap frames`, una limitación real de verificación de bytecode del compilador de scripts de nivel superior de BoxLang, rastreada vía el stack trace hasta la propia clase generada del script de prueba, no `SignalGateway.bx` - corregido envolviendo el try/catch dentro de una función nombrada en su lugar. Lo que NO se verificó, y es el vacío más grande entre todos los gateways de estilo push enviados hasta ahora: nunca ha habido un daemon real de `signal-cli` disponible en este entorno, así que todo el ciclo de vida de conexión SSE asíncrona - abrir el stream vía `HttpClient.sendAsync()`+`BodyHandlers.ofLines()`, el bucle de retroceso exponencial de reconexión contra una conexión genuinamente inestable, el watchdog de inactividad de 30s/120s forzando una reconexión, y un round-trip JSON-RPC en vivo - nunca se ha ejercitado de extremo a extremo, solo probado por humo a nivel de plomería de interoperabilidad (una prueba independiente alcanzó un `java.net.ConnectException` real contra una dirección inalcanzable, probando que la cadena es sólida, no que funciona contra un daemon en vivo). Haz al menos un `bxAgents serve` manual contra un proyecto con un daemon real y en ejecución de `signal-cli` y una cuenta real vinculada de Signal antes de depender de este gateway en producción - esta es una arquitectura de transporte genuinamente nueva en este código base (el único gateway basado en SSE entre Telegram/Slack/Discord/Email/Signal), no solo una nueva plataforma sobre una forma de transporte ya probada.

## WhatsApp Personal (puente no oficial de cuenta personal) - investigado, no construido

El plan original (igualando la propia arquitectura de Hermes Agent) pedía un `WhatsAppPersonalGateway` construido generando un subproceso de Node.js ejecutando `@whiskeysockets/baileys` (el cliente de protocolo multi-dispositivo de WhatsApp Web que el propio Hermes usa - licenciado MIT, su `bridge.js` completo se leyó directamente del código fuente real de Hermes, no resumido). Ese enfoque se dejó de lado a mitad de sesión por instrucción directa de preferir una integración nativa BoxLang/JVM sobre un puente de subproceso, y de usar una biblioteca Java nativa solo si es de código abierto y ni GPL ni LGPL.

Esa búsqueda encontró **Cobalt** (`com.github.auties00:cobalt`, antes WhatsappWeb4j) - una implementación Java real, licenciada MIT, mantenida activamente (900+ estrellas) del protocolo de "cliente vinculado" multi-dispositivo de WhatsApp, con una API fluida documentada (`WhatsAppClient.builder().linkedApi().webClient()...`, `addNewMessageListener()`, `sendMessage()`) que coercionaría limpiamente desde un closure de BoxLang vía la propia coerción SAM-de-Java documentada de BoxLang (confirmado vía el MCP de documentación de BoxLang - no se necesita `createDynamicProxy()` para un listener de método único abstracto). Dos bloqueos reales salieron a la superficie durante la verificación, no adivinados:

1. **La primera lectura de pom.xml (rama `master` de Cobalt, una reescritura multi-módulo en progreso) requiere Java 25** - dos versiones mayores por delante de la propia línea base documentada de BoxLang (Java 21+, confirmado vía el MCP de documentación de BoxLang y coincidiendo con el propio JDK `21.0.10` de este proyecto). Al volver a comprobar contra el artefacto realmente publicado en Maven Central (`cobalt:0.0.10`, lo real que una `<dependency>` resolvería hoy, no la reescritura no publicada) se mostró `<java.version>21</java.version>` - así que el hallazgo de Java-25 fue una falsa alarma causada por leer la rama equivocada, no un bloqueo real. Vale la pena registrarlo como precaución: el `pom.xml` de rama por defecto de un repositorio de GitHub no es necesariamente lo que está en Maven Central.
2. **El `cobalt:0.0.10` real y publicado trae `com.aspose:aspose-words` como una dependencia dura de tiempo de compilación** (usada internamente para generar miniaturas de vista previa de enlaces a partir de documentos Word) - Aspose.Words for Java es de licencia comercial/propietaria, no de código abierto, así que empaquetarlo violaría la misma restricción de licencia que Cobalt mismo fue elegido para satisfacer. El grafo de dependencias completo (~15 jars: zxing, qr-terminal, curve25519, protobuf-base, jackson o fastjson2 dependiendo de la versión, libphonenumber, dd-plist, apk-parser, link-preview, jaffree, ez-vcard, slf4j, más Aspose) todos necesitarían descargarse y empaquetarse manualmente bajo la carpeta `libs/` de este módulo - los módulos de BoxLang no tienen resolución de dependencias de estilo Maven propia (confirmado vía el MCP de documentación de BoxLang: los jars de terceros se empaquetan directamente en la carpeta `libs/` de un módulo, cargados por un classloader por módulo - no hay clave `javaLibraries` en `box.json` que resolvería un árbol de dependencias automáticamente).

Dada la contaminación de licencia vía Aspose y el esfuerzo de ensamblaje manual de fat-jar sin herramientas de resolución de dependencias disponibles para verificar que el resultado realmente carga, **WhatsApp Personal fue descartado del alcance** en lugar de enviarse como un gateway real o un stub. `validGatewayTypes` de `ProjectValidator` y `TYPE_CLASS_MAP` de `GatewayGenerator` no incluyen una entrada `whatsapp-personal` - un proyecto que intenta declarar uno obtiene el error de validación existente de "tipo de gateway desconocido", igual que cualquier otro tipo no soportado, en lugar de un stub medio construido engañoso. Revisitar esto es razonable si Cobalt alguna vez elimina la dependencia de Aspose (es una funcionalidad estrecha - miniaturas de vista previa de enlaces de documentos Word - no central para la mensajería), o si una sesión futura decide que el enfoque de puente de subproceso Node/Baileys (rechazado en esta ronda por preferencia de arquitectura, no por un bloqueo técnico) es preferible después de todo.

## `GatewaySession` es de todo el proyecto y solo de agente raíz (v1)

Un proyecto con al menos una entrada de gateway de estilo push obtiene exactamente un `GatewaySession` generado, agrupando cada gateway de estilo push y siempre vinculado al agente raíz del proyecto - coincidiendo con el precedente existente de que la exposición HTTP `exposes: "agent"` también es siempre solo-de-agente-raíz (ver [gateways/](conventions/gateways.md#3-push-style-gateways-type-telegram--slack--discord--email--whatsapp-cloud--teams--twilio--github--signal-and-friends)). Un proyecto con subagentes todavía no puede enrutar diferentes gateways a diferentes subagentes (por ejemplo, "Telegram habla con SupportBot, Slack habla con ResearchBot"). Una futura clave `targetAgent: "SubagentName"` por gateway, consumida por un `GatewaySession` por-nodo-de-agente en lugar de una sesión de todo el proyecto, es el punto de extensión natural - todavía no construido.

## Corregido: `GatewaySessionBootstrap.bx` buscaba gateways por la clave incorrecta de `aiGatewayRegistry()` (enviado roto en los cuatro gateways de estilo push, atrapado durante el pase de investigación de WhatsApp)

Un bug real, previamente enviado: la llamada `aiGatewayRegistry().get(...)` del interceptor generado usaba el propio nombre de archivo de la entrada `gateways/*` descubierta (por ejemplo, `"telegramChannel"`, de `gateways/telegramChannel.bx`), pero el `GatewayRegistry.register()` real de bx-ai siempre indexa por el propio `getName()` fijo de la CLASE del gateway (por ejemplo, `"telegram"`, configurado una vez en `TelegramGateway.init()`) - nunca nada suministrado por el llamador. Confirmado tanto leyendo el código fuente de bx-ai directamente como empíricamente (registrando un gateway real, luego llamando a `.get()` con el nombre de su entrada descubierta, lanzaba `"No item found in registry"`). Esto significaba que la construcción de `GatewaySession` lanzaría una excepción en el arranque de ColdBox (`afterConfigurationLoad`) para **cada** proyecto generado con un gateway de estilo push - Telegram, Slack, Discord, y Email todos se enviaron con este bug; pasó desapercibido porque la única cobertura de pruebas previa afirmaba sobre el CONTENIDO DE CADENA crudo del archivo generado, nunca contra un registro en vivo.

Corregido en `GatewayGenerator.generate()`: el interceptor ahora busca gateways por su cadena de TIPO (que siempre es idéntica al nombre registrado, para cada gateway de estilo push construido hasta ahora), deduplicada. `GatewayGeneratorSpec.bx` obtuvo una prueba de regresión permanente que registra una instancia REAL de gateway y prueba que la clave EXACTA que el generador acaba de emitir la resuelve vía un `aiGatewayRegistry()` en vivo - cerrando el vacío exacto que permitió que esto se enviara sin detectarse la primera vez.

**Una consecuencia real y permanente que esta corrección expone (no un comportamiento nuevo, solo ahora correctamente alcanzable)**: porque el registro está indexado por tipo, no por entrada, **dos entradas `gateways/*` del mismo tipo de estilo push colisionan en el mismo slot de registro a nivel de todo el proyecto** - por ejemplo, dos entradas `type: "telegram"` (dos tokens de bot diferentes) silenciosamente harían que el segundo registro sobrescriba al primero, y `GatewaySession` solo vería alguna vez uno de ellos. No hay un alias/override de nombre de registro por entrada hoy. Una instancia por tipo de estilo push, por proyecto, es el verdadero techo de v1 - no documentado como tal antes de que esta corrección lo hiciera visible.

## `./gradlew downloadModules` puede obtener un snapshot de bx-ai que está temporalmente rezagado respecto al codegen de `aiGatewayRegistry()` de `GatewayGenerator`

bx-ai renombró `gatewayRegistry()` a `aiGatewayRegistry()` en su rama `development` (confirmado directamente - `bifs/gatewayRegistry.bx` se eliminó por completo, sin alias de retrocompatibilidad) - `GatewayGenerator` se actualizó para coincidir, ya que bx-ai todavía no ha cortado un release y la propia instrucción de este proyecto era seguirlo directamente, no hacer un shim alrededor de ello. La trampa: `downloadModules` obtiene un artefacto de snapshot fijo, continuamente republicado (`bx-ai@3.4.0-snapshot`) desde `downloads.ortussolutions.com`, y ese zip publicado puede rezagarse respecto al HEAD de git `development` propio de bx-ai por alguna cantidad de tiempo (confirmado directamente esta sesión: justo después de que este renombramiento aterrizó upstream, el snapshot descargable todavía tenía el viejo `gatewayRegistry.bx`). Si un `downloadModules` reciente extrae un snapshot que predata este renombramiento, cualquier proyecto con una entrada `gateways/*` de channel-adapter fallará al arrancar con `Function 'aiGatewayRegistry' not found`, ya que el código generado ahora llama al nombre nuevo pero el módulo obtenido todavía solo tiene el viejo. Esto no es solucionable desde el lado de BX Agents - se resuelve por sí mismo una vez que ForgeBox republica el snapshot desde el `development` actual de bx-ai. Se verificó que el propio código generado de este proyecto es correcto contra el verdadero HEAD de bx-ai construyendo una estructura de módulo local directamente desde su código fuente de git (`ortus-boxlang/bx-ai`) en lugar de depender del zip descargado posiblemente obsoleto.

Se reconfirmó el mismo retraso de forma independiente mientras se construía el trabajo de gateway de estilo push/`GatewaySession`: el `bx-ai@3.4.0-snapshot` descargado en ese momento todavía no tenía `GatewaySession.bx`, ningún BIF `aiGatewaySession()`/`aiGatewayRegistry()`, y un `BaseGateway.bx`/`IGateway.bx` sin `onMessage()`/`onError()` en absoluto - un snapshot de mucho antes de este mismo renombramiento. `testBx` se ejecutó en este estado reemplazando `bifs/`/`models/`/`public/`/`ModuleConfig.bx` de `src/test/resources/modules/bxai` con una copia fresca del propio código fuente de git de bx-ai (su `libs/`/`box.json` se dejaron sin tocar), el mismo workaround que arriba - no algo que los usuarios finales de `build`/`serve` necesiten hacer ellos mismos una vez que ForgeBox se ponga al día, pero necesario para la propia verificación sin CI de esta sesión.

**CI ahora resuelve esto automáticamente, porque tuvo que hacerlo.** La primera ejecución real de GitHub Actions de esta suite probó que el retraso no es cosmético: el `bx-ai@3.4.0-snapshot` publicado todavía envía `bifs/gatewayRegistry.bx` (renombrado a `aiGatewayRegistry` upstream hace mucho tiempo) y no contiene **ningún `models/gateway/BaseGateway.bx` en absoluto** - la clase que los nueve gateways de estilo push en este módulo extienden. Probado contra él, 41 specs fallan por razones no relacionadas con el propio código de este módulo (`The method aiGatewayRegistry does not exist`, luego cada spec de gateway en cascada desde una clase base que no está ahí). `.github/workflows/tests.yml` por lo tanto clona la rama `development` de bx-ai, ejecuta su `createModuleStructure`, y superpone el resultado sobre lo que sea que `downloadModules` haya obtenido - el mismo workaround manual descrito arriba, automatizado. `bx-ftp` y `bx-sqlite` son releases estables y todavía vienen de `downloadModules` sin tocar. Elimina el paso de superposición una vez que un snapshot publicado se ponga al día; hasta entonces, ten en cuenta que CI está probando contra el HEAD de rama de bx-ai en lugar de un artefacto fijado, así que una ruptura upstream sale a la superficie aquí como un fallo de bx-agents.

Golpeado una tercera vez al conectar `/compact`: bx-ai delimitó `IAiMemory.summarize()` por `userId`/`conversationId` en `development` (commit `f9ac7bd`), pero un `downloadModules` reciente todavía obtenía un snapshot que llevaba el viejo `summarize( struct config = {} )` de un solo argumento. Mismo workaround - el módulo se reconstruyó desde el propio código fuente de git de bx-ai (`./gradlew createModuleStructure` en ese repositorio, copiado sobre `src/test/resources/modules/bxai`) y el comportamiento delimitado se verificó directamente contra ese build. A diferencia de los dos anteriores, este tiene una consecuencia real de tiempo de ejecución que un usuario final puede encontrar: `/compact` llama a `mem.summarize( config, userId, conversationId )`, y en un bx-ai anterior a ese commit los argumentos extra simplemente se ignoran, así que la compactación resumiría el scope *por defecto* de la instancia de memoria en lugar del de la conversación del llamador. Una app generada por lo tanto necesita bx-ai en o después de `f9ac7bd` para `/compact` específicamente; cada otra ruta no se ve afectada. Nada en la propia suite de pruebas de este repositorio regresa en un snapshot más antiguo tampoco, ya que los specs de la interfaz web afirman sobre el texto fuente generado en lugar de ejecutarlo.

## La interfaz web de chat v1 (`exposes: "webui"`) - qué es real, y qué solo pudo comprobarse contra documentación, no contra un servidor en vivo

`WebUiGeneratorSpec.bx` y una prueba de extremo a extremo de `BuildPipelineSpec.bx` ambas conducen a las clases reales `WebUiGenerator`/`BuildPipeline` contra fixtures reales: el shell estático `<path>/index.html` se confirma que se escribe y se plantilla correctamente (los placeholders `__API_BASE__`/`__APP_TITLE__` sustituidos, nunca dejados en la salida), el opcional `interceptors/WebUiAuthGate.bx` se confirma que se genera solo cuando `apiKeyEnvVar` está configurado, que protege exactamente `<path>/api/*` y nunca el shell `<path>` desnudo mismo, y que se registra correctamente en la lista `interceptors:[...]` de `config/ColdBox.bx` de extremo a extremo a través del `BuildPipeline` real. El interceptor generado también se confirmó esta sesión que compila e instancia limpiamente vía una prueba de humo independiente (cargado a través de la misma primitiva `DynamicClassLoader` que el propio pipeline de build usa).

Lo que NO se verificó - y no pudo, en este entorno de desarrollo: una prueba real de navegador de un `bxAgents serve` real de la página realmente cargando, transmitiendo una respuesta, y la compuerta `X-API-Key` realmente rechazando/aceptando requests sobre HTTP real. El propio arnés `runColdBoxIntegrationTests.bxs`/`tests/coldbox` de este proyecto (el mismo que probó que la ruta `/invoke` de `toAi()` funciona de extremo a extremo para `http-gateway-agent`) requiere que `tests/coldbox` esté presente vía un `box install` real dentro de `tests/` - el acceso a red de CommandBox y ForgeBox ambos estuvieron indisponibles en el sandbox de esta sesión, así que ese arnés no pudo ejercitarse para la interfaz web (ni reconfirmarse para nada más) esta sesión. Dos consecuencias directas:

- La forma exacta de respuesta JSON de `/invoke` usada por las propias páginas/documentación generadas de este proyecto (`{"input": "..."}` de entrada, una respuesta conteniendo `"success": true`) está confirmada empíricamente **solo** vía la propia aserción previa, ya pasando, de `runColdBoxIntegrationTests.bxs` (una comprobación de subcadena, no una aserción de forma completa) - no reverificada esta sesión.
- El formato de cable SSE de `/stream` que analiza el propio JS de la interfaz web (líneas `data: {"token":"..."}`, terminadas por `data: [DONE]`) se toma directamente de la propia documentación oficial "AI Routing" de ColdBox, no reconfirmada de forma independiente contra un servidor en vivo esta sesión - a diferencia de casi cualquier otra afirmación de formato de cable en la documentación de este proyecto, que fueron verificadas cruzadamente contra código real en ejecución dondequiera que fue posible (ver, por ejemplo, las verificaciones cruzadas independientes de Python/openssl de los esquemas de firma HMAC-SHA256/SHA1 en otras partes de este archivo).

Haz al menos un `bxAgents serve` real + una prueba real de navegador (mensaje enviado, respuesta transmitida, la compuerta `X-API-Key` realmente devuelve 401 a un request que carece de la clave) antes de depender de esta funcionalidad en producción - el mismo consejo permanente ya dado para el vacío de no-probado-contra-un-arranque-real de cada handler de webhook generado en otras partes de este archivo.

## El almacén SQLite de la interfaz web - verificado a nivel de biblioteca, no a través de un arranque real de ColdBox

La pila qb + bx-sqlite subyacente a `models/ChatDb.bx` se verificó directamente esta sesión contra los jars reales, no inferida de la documentación: los propios fuentes `.cfc` de qb 13.1.0 compilan y corren nativamente en BoxLang 1.16 sin `bx-compat-cfml`; `SQLiteGrammar` + `SchemaBuilder` realmente crean las tablas e índices v1 contra un archivo SQLite real; un segundo pase de migración es un no-op limpio; `QueryBuilder` hace round-trip de inserciones, lecturas filtradas/ordenadas y eliminaciones; y la clave primaria compuesta de `preferences` realmente rechaza un `(userId, prefKey)` duplicado. Se encontraron dos restricciones de esta manera en lugar de asumirse - qb requiere un datasource **nombrado** (su propio `appendSqlComments()` tipa ese argumento como `string`, así que un struct inline lanza antes de que corra cualquier SQL) - y `SchemaBuilder@qb` se mapea solo con su argumento `grammar`, nunca recibiendo `moduleSettings.qb.defaultOptions` - y el código generado está moldeado en torno a ambos.

Lo que NO se verificó, por la misma razón que el resto de la interfaz web: nada de esto se ha ejecutado dentro de un arranque real de ColdBox. `tests/coldbox` requiere un `box install` real, y el acceso a red de CommandBox/ForgeBox estuvo indisponible en el sandbox de esta sesión. Así que la lógica de migración está probada, pero tres suposiciones de cableado no se ejercitan de extremo a extremo: que `getInstance( "ChatDb" )` se resuelve a través del propio WireBox de la app generada, que `SchemaBuilder@qb`/`QueryBuilder@qb` se resuelven una vez que qb está instalado como un módulo real de ColdBox (qb es una dependencia de `box.json`, no vendida aquí - el mismo vacío honesto que ya lleva `cbmailservices`), y que `this.datasources` en el `Application.bx` generado se capta como se espera. Haz un `bxAgents serve` contra un proyecto webui con `qb` y `bx-sqlite` genuinamente instalados antes de confiar en el almacén en producción; el modo de fallo si alguno de ellos falta es ruidoso en el arranque (un mapeo de WireBox no resoluble o un driver JDBC desconocido), no silencioso.

## La interfaz web sobre HTTP real: probada por la sonda del corredor de integración

`runColdBoxIntegrationTests.bxs` obtiene `GET /chat/api/health` desde fuera del servidor, contra la app fixture generada arrancada por un `boxlang-miniserver` real, y falla el build si no es un 200. En CI devuelve:

```
+ App probe GET /chat/api/health -> status=200
  body: {"success":true,"status":"ok"}
```

Ese único request es la prueba de extremo a extremo para el lado del servidor de la interfaz web: el enrutamiento de ColdBox alcanzando `handlers/ChatUi.bx`, WireBox resolviendo el handler y `ChatDb`, `this.datasources` dándole a bx-sqlite un archivo SQLite usable, y qb siendo un módulo de ColdBox genuinamente activado - nada de lo cual puede establecer ninguna aserción de texto fuente. `WebUiRuntimeSpec.bx` luego cubre el comportamiento del almacén (migraciones, CRUD de conversación con scope, la protección entre usuarios, upserts de preferencia) leyendo objetos reales de WireBox dentro de ese mismo arranque.

**Lo que aún no está cubierto:** las demás rutas de webui sobre HTTP. Solo `/health` se obtiene desde fuera del proceso. Conducir el resto desde `WebUiRuntimeSpec` no es posible tal como está estructurado, porque ese spec se ejecuta *dentro* de un request servido por el mismo miniserver único, así que una llamada de loopback competiría por el pool de trabajadores que el propio corredor ocupa. Cubrirlas necesita ya sea un segundo proceso de servidor o un pool de hilos de miniserver más grande, y es un seguimiento natural en lugar de algo que fingir.

Nótese que este párrafo previamente citó `runner-coldbox.bxm never responded: HTTP 408` como evidencia de esa inanición. **Esa atribución era incorrecta**, y vale la pena registrarlo porque engañó a este proyecto durante varios ciclos de CI: el 408 no tenía nada que ver con llamadas de loopback. La página del corredor llamaba a `chr( 10 )` - un BIF que no existe - así que devolvía 500 al entrar en cada request, y el bucle de reintento del orquestador se abrió paso a través de 90 segundos de esos 500 antes de reportar el tiempo de espera final. Ambos están corregidos. La preocupación de inanición-de-loopback arriba es una razón estructural genuina para no conducir más rutas desde dentro de ese spec, pero sigue siendo una expectativa bien fundada en lugar de algo que realmente se haya observado fallando aquí.

## El propio front-end de la interfaz web: conducido en un navegador real, pero contra una API mockeada

El JavaScript de la página enviada se ejercitó de verdad esta sesión, no solo se afirmó como texto fuente: el `index.html` generado se cargó en Chromium sin cabeza y se condujo de extremo a extremo con cada ruta `<path>/api/*` interceptada y respondida con payloads realistas. Confirmado funcionando de esa manera - la barra lateral de conversación renderizando desde `GET /conversations` y cambiando/renombrando/eliminando a través de sus propias rutas; `GET /info` moldeando la barra de herramientas (Compact apareciendo solo cuando `capabilities.compact` es true, el nombre del modelo aterrizando en el encabezado); el tema llegando desde `preferences` del lado del servidor y aplicándose; `/history` rehidratando el transcript; un turno de SSE real transmitiendo contenido, razonamiento y chips de tool-call desde el envelope de bx-ai; renderizado de markdown; y **New chat** creando una conversación del lado del servidor y abriéndola. Cero errores de JavaScript, cero placeholders `__TOKEN__` sin reemplazar, y el layout de pantalla estrecha se capturó en 390px.

Lo que eso NO prueba: la API era un mock de Playwright, no el `handlers/ChatUi.bx` generado corriendo bajo un arranque real de ColdBox contra un almacén SQLite real. Las formas de request y respuesta se tomaron del propio código fuente del handler generado, así que una desviación entre los dos no sería atrapada por esto. Todo en la entrada de limitaciones conocidas de arriba sobre el almacén se aplica aquí también - un `bxAgents serve` manual contra un proyecto webui con `qb` y `bx-sqlite` genuinamente instalados sigue siendo la puerta honesta antes del uso en producción.
