Limitaciones conocidas

Los vacíos honestos - qué está probado contra una aplicación real en ejecución y qué no.

On this page

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 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). 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). 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/), 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 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 (HelloIdentifyREADY) 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/). 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.

Edit this page Download Markdown Last updated Aug 21, 2026, 6:33:28 PM