Ir al contenido

Pruebas

arcane-mcp afirma ofrecer todas las capacidades de Arcane en tres superficies. Tres niveles de pruebas respaldan esa afirmación: tests unitarios que no necesitan nada en marcha, comprobaciones de paridad sobre el catálogo y una batería en vivo que maneja instancias reales de Arcane.

Ventana de terminal
make check # generador al día, auditoría 1:1, vet, tests unitarios
make e2e-up # levanta el entorno Docker-in-Docker de Arcane
make test-e2e # batería en vivo en cada superficie y, después, el informe de cobertura
make e2e-down # elimina el entorno, volúmenes incluidos

Todo requiere Go 1.27.3 (GOTOOLCHAIN=go1.27.3 funciona con un Go anterior en el equipo); la batería en vivo necesita además Docker.

make test (o go test ./...) ejecuta los tests unitarios. Arcane se simula con servidores httptest; los tests se basan en tablas y se llaman TestUnit_Escenario_Esperado. make cover mide la cobertura de internal/... y cmd/..., y la CI falla por debajo del 90 %.

Además del comportamiento de cada paquete —asociación de argumentos, codificación multipart, forma de las respuestas, resúmenes de progreso, recogida de mensajes WebSocket, la delimitación del terminal de container.exec—, los tests unitarios comprueban el catálogo en su conjunto:

  • cada acción del inventario está en el catálogo y es válida (forma del ID, operationId, título y descripción, un esquema de entrada de tipo objeto, destructiva implica modificadora), y cada acción relacionada que nombra existe;
  • cada acción está registrada en las superficies meta e individual y la resuelve el execute dinámico, y cada superficie registra el número de herramientas esperado;
  • la misma llamada devuelve el mismo resultado en las tres superficies, una llamada destructiva sin confirm se rechaza en las tres y el modo seguro muestra la vista previa de una modificación;
  • el modo de solo lectura no deja ninguna acción que modifique, las exclusiones por dominio y por nombre de herramienta retiran lo que nombran, y una acción retirada explica el motivo con el nombre que tenga en cualquier superficie.
ComandoFalla cuando
go run ./cmd/gen_actions -check (make check-generated)Las declaraciones generadas difieren de lo que producen la especificación incorporada y api/*.yaml
go run ./cmd/audit_1to1 (make audit-1to1)Una operación OpenAPI o una ruta adicional de Arcane no tiene acción ni entrada en la lista de excepciones, una acción no cubre ninguna ruta o un nombre de herramienta supera los 64 caracteres
go run ./cmd/gen_tool_reference -check (make check-tool-reference)La referencia de herramientas de este sitio está desactualizada
go run ./cmd/audit_doc_names (make audit-doc-names)Un ID de acción o un nombre de herramienta escrito en los textos que sirve el servidor, el README, docs/ o una página del sitio no está en el catálogo, o una cifra del catálogo escrita a mano (N acciones, N dominios, N operaciones, N meta-tools, una herramienta por acción (N), en inglés o en español) en esas páginas o en un listado de registro o de marketplace (server.json, plugin.json, lhm.plugin.json, los README de los paquetes) no coincide con el catálogo; las páginas que escribe un generador quedan para su propia comprobación, y una cifra que habla de otra cosa figura con su motivo en cmd/audit_doc_names/figures.go

make inventory escribe docs/capabilities.md, la tabla completa de rutas de Arcane con sus acciones y sus herramientas.

La batería de test/e2e (etiqueta de compilación e2e) se ejecuta contra un entorno Docker Compose desechable de instancias reales de Arcane v2.17.2. Nada de ella toca el daemon Docker del equipo: cada instancia de Arcane gestiona un daemon Docker-in-Docker propio del entorno.

ServicioPapel
dockerDaemon Docker-in-Docker principal
docker-swarmSegundo daemon, inicializado como gestor Swarm de un solo nodo
arcane-aGestor Arcane principal (127.0.0.1:13552); es el dueño de todos los entornos de agente
arcane-bSegundo gestor independiente (127.0.0.1:13553), con su propia base de datos, usuarios y claves
arcane-agentAgente directo sobre docker
arcane-edgeAgente edge sobre docker, conectado mediante el túnel edge de Arcane
swarm-agentAgente directo sobre docker-swarm

Así, el gestor a tiene cuatro entornos —local, agent, edge y swarm— que cubren todos los caminos que tiene Arcane hasta un daemon Docker: su propio cliente, HTTP hacia un agente directo, el túnel edge y un gestor Swarm. El gestor b existe para los escenarios entre gestores.

make e2e-up (test/e2e/scripts/up.sh) pasa de la nada a un entorno aprovisionado en aproximadamente medio minuto con las imágenes en caché: arranca los daemons y los gestores, inicializa el Swarm, inicia sesión en cada gestor y crea una clave de API, registra los tres entornos de agente, arranca los agentes con sus claves de emparejamiento y espera a que cada entorno esté en línea y responda a un listado de contenedores. El resultado es test/e2e/.estate.json (ignorado por git, con permisos 0600 porque contiene claves reales). Los puertos solo escuchan en loopback.

El README del entorno documenta cómo funciona el aprovisionamiento y cada detalle medido contra v2.16.1, v2.17.0, v2.17.1 y v2.17.2: los agentes directos que se quedan en pending hasta una prueba de conexión, los errores reenviados desde los agentes que no son problemas RFC 9457, por qué se llega a los daemons por TCP, cómo se consigue que resuelvan los montajes de los proyectos, y más.

Los escenarios se escriben una sola vez sobre internal/surfacecall, que maneja un servidor arcane-mcp en el mismo proceso a través de cualquier superficie con una única forma de llamada, y cada uno se ejecuta como tres subtests: /dynamic, /meta e /individual. Cubren contenedores e imágenes, proyectos Compose y su ciclo de vida, repositorios Git y GitOps, plantillas y variables, redes y volúmenes, análisis de vulnerabilidades, entornos y agentes, registros, copias de seguridad, tareas, webhooks, el canal de eventos y el diagnóstico, usuarios, roles, claves de API, ajustes de OIDC y passkeys, y el clúster Swarm con sus nodos, servicios, stacks, configs y secrets, tanto en los entornos remotos como en el local.

Cada acción que llama un escenario queda anotada, por superficie, en test/e2e/.ledger.json. Tras la batería, make test-e2e ejecuta go run ./cmd/audit_e2e_gaps, que compara ese registro con el catálogo:

e2e coverage: N/461 actions on all three surfaces, P partial, U untestable (listed), M not exercised

Una acción cuenta como cubierta cuando al menos una llamada ha tenido éxito en cada una de las tres superficies. Las acciones que el entorno no puede ejercitar figuran con su motivo en test/e2e/untested.yaml —por ejemplo, una autoactualización que sustituiría al servidor bajo prueba, o una función que necesita un almacén de imágenes containerd que los daemons del entorno no tienen—, y la auditoría las acepta. Cualquier otra acción parcial o sin ejercitar se notifica, y la orden termina con código 1. go run ./cmd/audit_e2e_gaps -list muestra el estado de cada acción.

make e2e-coverage-record vuelca el registro de una ejecución completa y sin filtros en el fichero versionado test/e2e/coverage-record.json: por acción y superficie, si tuvo éxito, qué tests la ejercitaron y si se comprobó también un camino de error. make check-e2e-coverage no necesita el entorno: busca en los escenarios las acciones que llaman, las compara con el catálogo, untested.yaml y el registro, y comprueba la página de cobertura e2e generada. Una acción que solo se usa en una limpieza no cuenta como cubierta.

Por defecto, la batería construye el servidor dentro del propio proceso de test. Con E2E_TRANSPORT=stdio, cada escenario pasa en cambio por el binario arcane-mcp compilado, hablándole por su entrada y su salida estándar; con E2E_TRANSPORT=http, por arcane-mcp --http, con una sesión Streamable HTTP por escenario y superficie: make test-e2e-live-stdio y make test-e2e-live-http. Los escenarios, el registro y la auditoría no cambian.

Dos módulos manejan el binario real igual que lo haría un cliente MCP. No necesitan Arcane, ni credenciales, ni Docker: las llamadas que llegarían a Arcane las atiende una imitación que levanta el propio test.

OrdenMóduloQué comprueba
make test-e2e-stdiotest/e2e/stdio (etiqueta stdioe2e)El saludo inicial, los datos del servidor y las versiones del protocolo; la lista de herramientas de cada superficie frente al catálogo (solo lectura, exclusiones); una llamada en cada superficie; los recursos arcane://tools, y los recursos de datos y las guías leídos a través del Arcane falso (Recursos); que por stdout solo salga JSON-RPC y los logs vayan a stderr; la configuración desde .env, el entorno y las opciones, por ese orden de precedencia; que una configuración incorrecta impida arrancar con un mensaje y un código distinto de cero; la confirmación, el modo de solo lectura y el modo seguro; las llamadas destructivas confirmadas mediante elicitación (rechazo y aceptación) con sus registros de auditoría; la renovación de la sesión tras un 401; una salida limpia al cerrarse stdin (tras responder una llamada aún en curso) y con SIGTERM y SIGINT
make test-e2e-httptest/e2e/http (etiqueta httpe2e)El ciclo de vida de la sesión (Mcp-Session-Id, llamadas, DELETE); el token bearer opcional (ARCANE_MCP_HTTP_TOKEN) y las claves por cliente (X-Arcane-API-Key); la comprobación de Host en localhost; cómo se responde a peticiones mal formadas y a métodos incorrectos; el aislamiento entre sesiones simultáneas; /healthz; el apagado con SIGTERM, en reposo, con un cliente conectado y con una llamada en curso, a la que se deja terminar

Cada objetivo compila el binario una sola vez en un directorio temporal y se lo pasa a los tests en E2E_SERVER_BINARY.

Cobertura de condiciones y pruebas de mutación

Sección titulada «Cobertura de condiciones y pruebas de mutación»

Además del umbral de cobertura de sentencias, make coverage-conditions PKG=./internal/toolutil (gobco) señala cada condición booleana que los tests de un paquete nunca evalúan en ambos sentidos, y make coverage-mutants PKG=./internal/toolutil (gremlins) muta el paquete e informa de los mutantes que sus tests dejan vivos. Ambas se ejecutan bajo demanda, en local o con el flujo conditions.

El flujo conditions se ejecuta además cada lunes sobre una lista fija de paquetes, solo condiciones y solo como informe (sin umbral ni pruebas de mutación); lánzalo a mano con mutants=true para las pruebas de mutación.

Los supervivientes de gremlins se vuelven a comprobar a mano, porque gremlins v0.6.0 nunca ejecuta un mutante en una expresión case ni en el inicializador de una constante o variable de paquete (el perfil de cobertura de Go no tiene ahí ningún bloque, así que aparece como NOT COVERED), y un mutante que vive merece confirmarse en una ejecución propia. make coverage-mutants-hand PKG=./internal/config REPORT=gremlins.txt, con la salida de make coverage-mutants guardada en gremlins.txt (también sirve su informe JSON), aplica cada mutante LIVED y NOT COVERED solo, en una copia de trabajo del módulo, ejecuta sobre él los tests del paquete (go test -failfast, que ejecuta go vet) e imprime KILLED, LIVED, NOT VIABLE (no compila, o go vet lo rechaza) o NOT APPLIED para cada uno. Un mutante se aplica como lo aplica gremlins, a través del AST y go/printer, que añade los paréntesis que pide la nueva precedencia: el primer && de a && b && c convertido en || es (a || b) && c, no a || b && c; MUTANT_WORKERS fija cuántos se ejecutan a la vez.

Un cambio que toca un paquete (decisión D48) mantiene sus condiciones evaluadas en ambos sentidos igual o mejor, ejecuta gremlins sobre los ficheros que cambió y clasifica cada superviviente: eliminado por un test nuevo, o registrado como equivalente con su motivo en el registro de abajo. No es un cero literal, por los motivos de arriba.

Ventana de terminal
make coverage-conditions PKG=./internal/toolutil
make coverage-mutants PKG=./internal/toolutil CHANGED=auto MUTANT_REPORT=mutants.txt
make coverage-mutants-hand PKG=./internal/toolutil REPORT=mutants.txt

CHANGED=auto toma los ficheros del paquete cambiados respecto a origin/main (si no, main; CHANGED=<ref> nombra otra referencia), FILES="a.go b.go" los nombra, y MUTANT_BLOCK_FILES=3 los reparte, o el paquete entero, en bloques. En este modo por bloques gremlins ejecuta un bloque cada vez, cada uno con una caché de compilación propia que se borra al terminar el bloque (cada mutante se compila en una copia nueva del módulo, así que un paquete entero llegó a añadir unos 20 GB a la caché compartida), ningún bloque empieza con menos de 6 GB libres, y un solo informe cubre todos los bloques, por fichero y en total.

Medido el 2026-10-27 con gobco v1.3.4 y gremlins v0.6.0 (INVERT_LOGICAL activado, dos workers), antes y después de los tests de la revisión W19. «Después» es la rama g7-mutants con main en f3660da fusionada. Las condiciones son las evaluadas en ambos sentidos sobre el total; los mutantes, eliminados / vivos / sin cubrir / agotados por tiempo tal como los da gremlins, y cada mutante vivo o sin cubrir se volvió a aplicar a mano después (ver abajo).

PaqueteCondiciones antesCondiciones despuésMutantes antesMutantes después (gremlins)Lo que queda tras la comprobación a mano
internal/toolutil1709/18281931/1952no ejecutado entero956 / 50 / 139 / 619 vivos y 2 sin cubrir, todos equivalentes
internal/wiring750/788803/814no ejecutado353 / 34 / 33 / 1817 vivos y 1 sin cubrir equivalentes; 1 vivo sin eliminar
internal/config430/460458/462189 / 30 / 69 / 0218 / 4 / 68 / 03 vivos equivalentes; 1 vivo pendiente
internal/completions160/176176/17668 / 9 / 4 / 076 / 1 / 4 / 01 vivo equivalente
internal/prompts92/9493/9451 / 6 / 0 / 056 / 1 / 0 / 0ninguno
internal/resources117/124123/12482 / 2 / 5 / 084 / 1 / 4 / 01 vivo equivalente
cmd/audit_workflow_pins381/420530/562no ejecutado215 / 10 / 29 / 36 vivos, todos equivalentes
internal/toolutil (safe_preview.go, W20 H10)1999/20221999/2022no ejecutado35 / 0 / 2 / 0ninguno
internal/prompts (workflows.go, W20 H10)93/9493/94no ejecutado0 / 0 / 0 / 0ninguno
cmd/reproduce_release (nuevo, W20 H8)—131/146—47 / 5 / 38 / 0, y main.go tras la revisión 19 / 2 / 30 / 01 vivo, equivalente
cmd/audit_doc_names (W20 H12)69/90123/134no ejecutado70 / 18 / 0 / 0ninguno
cmd/audit_site_headers (W20 H11, nuevo)ninguno205/206ninguno121 / 2 / 1 / 0ninguno

Las ejecuciones de toolutil y wiring anteriores a la revisión no cupieron en la caché de compilación de la máquina donde corrieron (la de toolutil se detuvo tras 243 de sus mutantes), así que solo sus cifras de condiciones tienen un «antes». El «después» de toolutil son dos ejecuciones: los ficheros que cambió la fusión de main (action.go, confirmnote.go, dispatch.go, elicit.go, field_errors.go, overlay.go, proxyclose.go, render_hints.go, render_list.go, render_registry.go, safe_preview.go) se ejecutaron sobre el código fusionado, y los demás sobre el código justo anterior a la fusión, que comparten. Las ejecuciones de completions, prompts y resources son anteriores a la fusión, que no cambió su código; el único mutante vivo de prompts lo elimina un test añadido después de su ejecución. Los paquetes centrales que midió W13 (internal/arcane, internal/openapi, internal/tools/dynamic, internal/tools/livestream, internal/tools/actioncatalog) están en 255/266, 373/408, 157/158, 153/156 y 32/32 condiciones; sus cifras de mutación son las de W13.

cmd/audit_workflow_pins se midió después, el 2026-11-01 para W20 H6 (el apéndice de D44), con las mismas herramientas: «antes» es main en 9f24130, «después» la rama w20-h6 con las correcciones de la revisión. Los mutantes son los de los ficheros que cambió la rama (CHANGED=auto: credentials.go, downloads.go, images.go, main.go, shape.go, workflow.go). A mano, 10 de los 29 sin cubrir no compilan o go vet los rechaza y los otros 19 se eliminan; los 3 agotados por tiempo convierten el i++ de un bucle en i-- y no terminan, lo que cuenta como detectado; 4 de los 10 vivos los eliminan tests añadidos después de la ejecución (la profundidad de los alias, las claves de fusión tras la primera clave, un flujo sin jobs), y los 6 que quedan son equivalentes (abajo). Una ejecución anterior en la misma rama había dejado 14 vivos en código de W19 G9 que ningún test fijaba (el segundo secreto de un valor, dos scripts en una línea, las líneas de los hallazgos tras la primera de un bloque run, los bordes del separador de líneas, las formas locales de una instalación global de npm); los tests eliminan 12, y los otros 2 están entre los 6.

Las dos filas de W20 H10 solo cambiaron texto (la nota y la indicación de reinicio del modo seguro, y un paso de un prompt), así que sus condiciones son las mismas antes y después por construcción; la cifra de toolutil es el paquete entero en main en c0a8bf9 más el cambio. Los mutantes son los del fichero tocado solamente (FILES=): los 2 sin cubrir de safe_preview.go se eliminan a mano, y workflows.go son datos sin operadores que mutar.

cmd/reproduce_release es nuevo (W20 H8). Sus tests compilan de forma cruzada un fixture para seis plataformas, lo que una caché de compilación nueva por mutante haría lento y grande, así que gremlins se ejecutó con GOFLAGS=-short (esos tests se omiten) sobre main.go y recipe.go, y cada mutante vivo o sin cubrir se aplicó después a mano con todos los tests: 3 no compilan y el resto se eliminan, 12 de ellos con tests añadidos tras la ejecución (el directorio -out, un fichero recompilado que no es un binario de Go, la línea de una suma mal formada, los límites de la cabecera fat y la disposición del binario universal comprobada con desplazamientos calculados en el test y no por assembleFat). La revisión cambió cómo se obtiene el commit abreviado (abbreviate), así que main.go se ejecutó de nuevo del mismo modo: 2 no compilan, 28 se eliminan a mano, uno más con un test añadido después (un sello de 16 caracteres), y uno vive: i > 0 frente a >= en el bucle que reutiliza el binario del primer objetivo, que solo compila ese objetivo una segunda vez, con los mismos bytes. Las 15 condiciones nunca evaluadas en ambos sentidos son errores de git, del sistema de ficheros, de una compilación posterior a la primera y de la codificación YAML una vez leídas las entradas.

La comprobación a mano. Cada mutante vivo y cada mutante sin cubrir se aplicó solo, en una copia de trabajo del módulo, ejecutando sobre él los tests del paquete (make coverage-mutants-hand):

  • Todo mutante que gremlins dio como vivo vive también a mano, salvo que lo elimine un test añadido después de su ejecución.
  • Sin cubrir no es sin probar. gremlins solo ejecuta un mutante donde el perfil de cobertura de Go tiene un bloque, y el perfil no tiene ninguno para las expresiones de una cláusula case ni para los inicializadores de constantes y variables de paquete. A mano se eliminaron los 139 sin cubrir de toolutil salvo 2 equivalentes y unos pocos que go vet rechaza; los 33 de wiring salvo 1 equivalente y 4 que no compilan; los 68 de config salvo 2 que go vet rechaza (x == a && x == b); y los 4 de completions y los 4 de resources.
  • Los mutantes que vivieron a mano se eliminaron con tests nuevos cuando no eran equivalentes: más de 40 en toolutil, 16 en wiring y unos pocos en cada uno de los demás paquetes.

W20 H1 (rechazo de segmentos de punto, D51), 2026-11-01. Mismas herramientas y ajustes, en modo por bloques sobre los ficheros que tocó el cambio. Condiciones: internal/toolutil 2017/2040 (main en ese momento: 1999/2022, con las mismas 23 pendientes; cada condición nueva se evalúa en los dos sentidos), internal/arcane 280/290 (una más que antes, por un caso nuevo de SameOriginRedirect), internal/config 484/490 (main: 479/486). Mutantes: dispatch.go, path_args.go, audit.go y execute.go de toolutil 124 / 1 / 7 / 1, y dispatch.go otra vez tras la revisión 50 / 0 / 6 / 0, todos los sin cubrir eliminados a mano y el único vivo retirado junto con la guarda en la que vivía; client.go, ws.go y pathguard.go de arcane 115 / 2 / 0 / 0, uno eliminado con un test nuevo y el límite de la consulta de Client.URL equivalente (abajo), pathguard.go tras la revisión 8 / 0 / 0 / 0; config.go de config 207 / 5 / 68 / 0, a mano 66 sin cubrir eliminados y 2 que go vet rechaza, un vivo (la comprobación de proxies de confianza sin proxies) eliminado con un test nuevo, y los otros cuatro son los que este registro ya da como equivalentes o pendientes.

W20 H12 (cifras del catálogo en audit_doc_names), 2026-11-01. Mismas herramientas y ajustes. Condiciones: 69/90 en main en d399e34, 123/134 después (11 pendientes, 10 menos, todas en la búsqueda de nombres que ya existía: ramas de error a las que ningún repositorio llega, una nota de uso que nunca está vacía y un nombre con punto seguido de -). Mutantes de los ficheros que tocó el cambio (CHANGED=auto: figures.go y main.go): 70 / 18 / 0 / 0. Todos los de figures.go se eliminan; los 18 que vivieron están en la búsqueda de nombres de main.go, anterior al cambio (las marcas de patrón de los nombres de herramienta, los rangos de isWordByte, la nota de uso vacía y los mensajes de los prompts), y los tests nuevos eliminan los 18 a mano.

W20 H11 (cmd/audit_site_headers, un paquete nuevo), 2026-11-02. Mismas herramientas y ajustes, todos los ficheros del paquete (CHANGED=auto). La única condición pendiente es el error de Close del fichero de resumen, que un test no puede provocar en un fichero que acaba de abrir para añadir. De los mutantes, el sin cubrir convierte el + de una URL constante en -, que no compila; un límite vivo (una z después del primer carácter del nombre de una función de Permissions-Policy) lo elimina un test nuevo, y el otro (un contador de miembros que pasa a negativo, leído solo como cero o no) desapareció con el contador, ahora un indicador.

W20 H3 y H2 (ajustes solo HTTP en stdio, HTTP sin cifrar en la red, D52), 2026-11-02. Mismas herramientas y ajustes, en modo por bloques sobre los ficheros que tocaron los dos elementos. Condiciones: internal/config 514/520 (main: 484/490, con las mismas 6 pendientes), cmd/arcane-mcp 394/438 (main: 368/414; los tests nuevos cubren dos de las 46 de main, y unas pocas de ready.go y tls.go van y vienen entre ejecuciones según los tiempos). Mutantes: config.go y httponly.go de config 231 / 5 / 68 / 0; a mano 66 sin cubrir eliminados y 2 que go vet rechaza, un vivo (la guarda de nil de plaintextFlag.String) eliminado con un test nuevo, y los otros cuatro son los que este registro ya da como equivalentes o pendientes. main.go y plaintext.go de cmd/arcane-mcp 74 / 11 / 11 / 7; a mano todos los vivos y sin cubrir eliminados con tests nuevos (los plazos del servidor, los atributos de la línea de arranque, ninguna línea de vaciado sin retardo, un inicio de sesión que atiende a clientes sin clave, la comprobación del Host de loopback sin lista de hosts, una parada que espera a una petición y luego cierra el puerto, un comodín que se juzga antes de cualquier resolución), salvo uno en socket_windows.go (errors.Is(err, wsaeConnRefused) || … invertido), que no se puede medir aquí: el fichero solo se compila para Windows, así que ningún test en Linux, donde se hacen estas ejecuciones, lo compila. Tras la revisión (los errores con --http en una sola ejecución, el rechazo de un nombre que no resuelve), con main fusionado: condiciones 510/516 en internal/config (las mismas 6 pendientes) y 404/448 en cmd/arcane-mcp; config.go y httponly.go 229 / 4 / 68 / 0, a mano 66 sin cubrir eliminados y 2 que go vet rechaza, y los 4 vivos son los ya registrados arriba; plaintext.go 18 / 0 / 2 / 0, los dos sin cubrir eliminados a mano.

Lo que queda, y por qué.

  • Mutantes equivalentes. Sugerencias de capacidad (make(…, n±1)) en toolutil (tres) y resources; cortes de una lista a la longitud que ya tiene (MaxListRows, columnSample, los 8192 bytes de cabecera de una descarga) y un relleno al que se piden cero columnas; límites de la forma if a == b { a = b }: t < FetchTimeout en completions, ActionTimeout < Timeout en config, el total de una segunda etapa y el ajuste total < current del progreso de toolutil, seconds > MaxStreamSeconds; comparaciones cuyo resultado no puede cambiar: las dos guardas de sameValue, y su (aIsString || bIsString) && EqualFold(…) y (aNum || bNum) && af == bf, que solo difieren con una cadena en blanco o un 0 frente a un valor de otro tipo, que nunca se comparan porque fitsType va antes; el err != nil de networkOf (una clave inválida no tiene ni dirección IPv6 ni 64 bits), una guarda de CutSuffix que solo añade un candidato repetido, un [ que no puede estar en el índice de ](, un case s[i] < 0x20 que el bucle anterior hace inalcanzable en 0x20, el espacio de inSequence, una línea de longitud 0 que accept ignora; una negación que solo afecta a un registro de depuración (el error de NotifyProgress); un intervalo de progreso 0, que NewProgress nunca conserva, y el instante igual al intervalo; r > unicode.MaxASCII frente a >=, donde DEL se rechaza en ambos casos; el contador de un /48 pasado su límite, que no cambia nada una vez bloqueado; los empates del cubo más lleno del limitador de peticiones; la lectura anticipada de espacios del límite de peticiones, que cambia cuánto se lee y nunca la cuenta; la captura del cuerpo que sigue con un búfer truncado o añade cero bytes, y un texto de error que añade cero bytes; len(query) > 0 frente a >= 0 en Client.URL de arcane, porque una consulta vacía se codifica como una cadena de consulta vacía; el conjunto de lotes del escritor de stdio, siempre presente, y sus mensajes, nunca vacíos; el vaciado al final de stdin sin tiempo de espera, que vuelve en ambos casos; una comprobación de filepath.Separator junto a la de /, el mismo byte en Linux; el tic del barrido de inactividad, un cuarto del tiempo de inactividad, que solo cambia cuán pronto se recupera un servidor inactivo.
  • En cmd/audit_workflow_pins: cinco límites de bucle i+1 < len(n.Content) frente a <= sobre mapas de YAML (el bloque de permisos en writePermission, los servicios en jobImages, los trabajos en workflowFromNode, mapGet, scalarMap), cada uno tras comprobar que el nodo es un mapa, cuyo contenido son pares clave/valor y nunca tiene longitud impar (la revisión de H6 vio que el de writePermission se alcanzaba con un permissions: en forma de lista, que hacía fallar al mutante; un bloque así se considera ahora capaz de escribir y se informa antes del bucle); y len(parts) < 2 frente a <= en step.action, donde una referencia de dos partes da owner/repo en ambos casos.
  • En config, len(entries) == 0 || len(known) == 0 frente a && en las listas por instancia solo difiere con un valor hecho solo de comas (ARCANE_API_KEY=,), que ninguna configuración quiere decir; se deja sin fijar.
  • En wiring, el s.writer != nil antes de flushed() al final de stdin protege contra una escritura que aún tiene el cerrojo de stdout cuando se olvida la última respuesta; un test que lo elimine tendría que detener el planificador en ese instante, así que se deja, como hueco conocido.
  • Los agotados por tiempo (6 en toolutil, 18 en wiring) cuentan como detectados: esos mutantes hacen que un test entre en bucle o espere hasta su plazo, sobre todo al final de stdin en stdio.go.
  • Condiciones a las que no llega ninguna entrada: validHeaderName("") tras comprobar que la cabecera está definida; una lista de instancias donde una entrada sin nombre solo puede ser la única (len(entries) > 1, name != ""); una acción destructiva pero no mutante (las marcas de modo seguro de los prompts y de los siguientes pasos), que la validación del catálogo rechaza; un Lookup que encuentra un nombre pero no devuelve acción; una frase de siguientes pasos que nombra una acción no servida, descartada antes de calcular su marca de modo seguro; una descarga de texto sin más campo que su contenido, cuando su tipo y tamaño siempre están; una ubicación de error de campo vacía con su argumento no vacío, y un enlace de cuerpo después del case que recoge toda ubicación de cuerpo; parámetros de modo seguro que siempre son un objeto.
  • Errores que no pueden ocurrir: escribir un cuerpo multipart en un bytes.Buffer; codificar un valor JSON recién decodificado, o un esquema recién reflejado; volver a codificar un id de petición que el paquete jsonrpc acaba de decodificar; netip.Addr.Prefix(64), que no falla con ninguna dirección IPv6; una caché de esquemas que solo guarda esquemas compilados; una captura de cuerpo cuya lectura de prueba no termina ni en EOF ni pasado su límite.
  • Ramas defensivas que producción nunca toma: el escritor de stdio siempre tiene sus conjuntos de lotes y de respuestas debidas, y el SDK nunca escribe un mensaje vacío; serveSDK y allowRate solo reciben un cuerpo nulo de una petición construida a mano; la clave de quien llama en modo apikey nunca está vacía una vez que la puerta la ha leído; un proxy unix de confianza sin cabecera, que la configuración rechaza.
  • La sesión sin parámetros de inicialización de elicit.go es la del transporte HTTP sin estado, medida en internal/wiring (http_stateless_elicit_test.go), no en los tests propios de toolutil. gobco da la rama de pánico de SchemaFor como nunca tomada, aunque TestSchemaFor_UnsupportedType_Panics la toma: gobco no registra esa evaluación, en una función genérica que entra en pánico justo después.

Sincronización con Arcane v2.17.2 (W20, 2026-11-01). internal/toolutil perdió proxyclose.go: 1985/2008 condiciones evaluadas en ambos sentidos frente a 1999/2022 en main, con las mismas 23 sin evaluar en ambos sentidos (las 14 que se fueron estaban todas cubiertas). gremlins sobre los archivos que cambió (execute.go, request.go): 57 eliminados, 0 vivos, 1 sin cubrir (case hasText && hasB64 en decodeFileObject, eliminado a mano), 1 agotó el tiempo (detectado). Las notas de uso cambiadas en once paquetes internal/tools/* y un comentario en internal/openapi dejan sus condiciones como estaban. gremlins sobre esos archivos: openapi 3 eliminados, tools/webhook 2 eliminados, tools/build 5 eliminados y 1 vivo también a mano, equivalente: err != nil || res == nil convertido en && en fileGet solo difiere cuando ExecuteREST devuelve a la vez resultado y error, o ninguno de los dos, cosa que nunca hace. Los otros nueve paquetes no tienen archivos de prueba propios (sus acciones se ejercitan con las pruebas del catálogo) y el modo por bloques se niega a medirlos.

Lo que cuesta una ejecución. Con dos workers en cuatro núcleos compartidos: alrededor de 1 hora y 5 minutos para toolutil (1151 mutantes) y 1 hora y 12 minutos para wiring (438, frenada por 18 mutantes de stdio que agotan cada uno el presupuesto de 300 segundos); de 1 a 3 minutos para cada uno de los otros cuatro. Cada mutante se compila en una copia temporal del módulo, así que los mutantes no comparten nada de la caché de compilación: unos 18 MB de caché por mutante, unos 20 GB para toolutil y 8 GB para wiring. La comprobación a mano tarda de 5 a 20 segundos por mutante.

El código que lee entradas no fiables tiene objetivos de fuzzing nativos de Go, y cada uno comprueba una propiedad, no solo que no haya pánico: el filtro de stdio (cada línea se pasa entera o se responde, y el SDK nunca termina la sesión), la recuperación del id de petición en los rechazos HTTP, el renderizador Markdown sobre JSON arbitrario (no sobrevive ningún byte de control, secuencia ANSI, etiqueta ni enlace, las tablas conservan su forma y el contenido estructurado queda intacto), la derivación de esquemas desde OpenAPI, la vinculación de argumentos a ruta, query, cabeceras, cookies y cuerpo (se decodifican de vuelta en los argumentos), el resumen de los flujos de progreso y las tramas de los flujos en vivo (eventos SSE, líneas de log, secuencias de terminal, el enmarcado de container.exec). make fuzz ejecuta cada objetivo durante 30 segundos (FUZZTIME=10m para más tiempo, FUZZ=<nombre> para algunos); un go test normal solo reproduce sus semillas y las entradas guardadas en testdata/fuzz.

Lo bien que los modelos de lenguaje usan estas herramientas se mide con un corpus de tareas en lenguaje natural; consulta Evaluación con modelos.

El flujo de CI ejecuta los tests unitarios con el detector de condiciones de carrera y el umbral de cobertura, comprueba go mod tidy, compila la batería e2e (go vet -tags e2e), ejecuta la comprobación del generador, la auditoría 1:1 y la comprobación de la referencia de herramientas, compila de forma cruzada para Linux, macOS y Windows en amd64 y arm64, pasa golangci-lint y govulncheck, y construye este sitio de documentación, y comprueba sin conexión el registro de cobertura e2e y el corpus y el registro de la evaluación con modelos. El flujo fuzz ejecuta cada objetivo de fuzzing durante 30 segundos en cada push y durante 20 minutos una vez por semana. El flujo semanal e2e levanta el entorno Docker-in-Docker en un runner de GitHub, ejecuta la batería en vivo y sube el registro de cobertura resultante.

Ningún test recupera un fallo de hardware (una desreferencia de nil capturada con recover): en Windows, la recuperación de uno por parte de Go puede sobrescribir memoria del heap junto a la pila de la goroutine en equipos Intel con AMX (golang/go#81238), lo que una vez hizo fallar la ejecución de Windows con texto de esquema corrupto en un test sin relación. Los tests que comprueban la recuperación de pánicos provocan el pánico por software (panic(...), o un transporte cuyo RoundTrip entra en pánico).

Este sitio es una compilación de GitHub Pages servida a través de Cloudflare. Pages no deja que un sitio fije ninguna cabecera de respuesta propia, y la compilación tampoco fija ninguna, así que todas las cabeceras de seguridad vienen de la regla de cabeceras de respuesta de Cloudflare para docs.jmrp.io, que solo el propietario puede cambiar. Tras cada despliegue, el trabajo headers del flujo pages ejecuta go run ./cmd/audit_site_headers sobre la raíz publicada y sobre una página interior (es/reference/tools/), solo con el token de lectura del flujo. Una cabecera que falta o es permisiva hace fallar el trabajo, con el valor que hay que fijar como error en la ejecución. Para entonces el sitio ya está publicado, así que un fallo es una regresión que hay que corregir en Cloudflare, no un despliegue que deshacer. Una página que no responde se reintenta mientras el despliegue se propaga.

CabeceraValor que fija la reglaSe rechaza
Content-Security-Policydefault-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://img.shields.io https://cursor.com https://kiro.dev https://files.lmstudio.ai; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; report-uri https://jmrp.io/csp-reportSin directiva de scripts, scripts desde cualquier host o 'unsafe-eval', plugins que no sean 'none', sin frame-ancestors o con uno que admita cualquier host
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadSin max-age legible, o de menos de un año
X-Content-Type-OptionsnosniffCualquier otro valor
X-Frame-OptionsDENYUn valor que no rechaza ningún marco (ALLOW-FROM o ALLOWALL solos)
Referrer-Policystrict-origin-when-cross-originunsafe-url, no-referrer-when-downgrade, o ninguna política que un navegador conozca
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=(), usb=()Vacía, un miembro que no se puede leer, o una función concedida a cualquier origen

La raíz, las páginas interiores y la página 404 servían las seis el 2026-11-02, así que las seis son obligatorias. go run ./cmd/audit_site_headers sin opciones comprueba el sitio publicado desde cualquier sitio.