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.
make check # generador al día, auditoría 1:1, vet, tests unitariosmake e2e-up # levanta el entorno Docker-in-Docker de Arcanemake test-e2e # batería en vivo en cada superficie y, después, el informe de coberturamake e2e-down # elimina el entorno, volúmenes incluidosTodo 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.
Tests unitarios
Sección titulada «Tests unitarios»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
confirmse 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.
Comprobaciones de paridad
Sección titulada «Comprobaciones de paridad»| Comando | Falla 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 extremo a extremo
Sección titulada «La batería de extremo a extremo»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.
El entorno
Sección titulada «El entorno»| Servicio | Papel |
|---|---|
docker | Daemon Docker-in-Docker principal |
docker-swarm | Segundo daemon, inicializado como gestor Swarm de un solo nodo |
arcane-a | Gestor Arcane principal (127.0.0.1:13552); es el dueño de todos los entornos de agente |
arcane-b | Segundo gestor independiente (127.0.0.1:13553), con su propia base de datos, usuarios y claves |
arcane-agent | Agente directo sobre docker |
arcane-edge | Agente edge sobre docker, conectado mediante el túnel edge de Arcane |
swarm-agent | Agente 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.
Cada escenario en cada superficie
Sección titulada «Cada escenario en cada superficie»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.
El registro y audit_e2e_gaps
Sección titulada «El registro y audit_e2e_gaps»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 exercisedUna 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.
El registro de cobertura
Sección titulada «El registro de cobertura»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.
A través del binario real
Sección titulada «A través del binario real»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.
Tests de transporte
Sección titulada «Tests de transporte»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.
| Orden | Módulo | Qué comprueba |
|---|---|---|
make test-e2e-stdio | test/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-http | test/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.
make coverage-conditions PKG=./internal/toolutilmake coverage-mutants PKG=./internal/toolutil CHANGED=auto MUTANT_REPORT=mutants.txtmake coverage-mutants-hand PKG=./internal/toolutil REPORT=mutants.txtCHANGED=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.
El registro
Sección titulada «El registro»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).
| Paquete | Condiciones antes | Condiciones después | Mutantes antes | Mutantes después (gremlins) | Lo que queda tras la comprobación a mano |
|---|---|---|---|---|---|
internal/toolutil | 1709/1828 | 1931/1952 | no ejecutado entero | 956 / 50 / 139 / 6 | 19 vivos y 2 sin cubrir, todos equivalentes |
internal/wiring | 750/788 | 803/814 | no ejecutado | 353 / 34 / 33 / 18 | 17 vivos y 1 sin cubrir equivalentes; 1 vivo sin eliminar |
internal/config | 430/460 | 458/462 | 189 / 30 / 69 / 0 | 218 / 4 / 68 / 0 | 3 vivos equivalentes; 1 vivo pendiente |
internal/completions | 160/176 | 176/176 | 68 / 9 / 4 / 0 | 76 / 1 / 4 / 0 | 1 vivo equivalente |
internal/prompts | 92/94 | 93/94 | 51 / 6 / 0 / 0 | 56 / 1 / 0 / 0 | ninguno |
internal/resources | 117/124 | 123/124 | 82 / 2 / 5 / 0 | 84 / 1 / 4 / 0 | 1 vivo equivalente |
cmd/audit_workflow_pins | 381/420 | 530/562 | no ejecutado | 215 / 10 / 29 / 3 | 6 vivos, todos equivalentes |
internal/toolutil (safe_preview.go, W20 H10) | 1999/2022 | 1999/2022 | no ejecutado | 35 / 0 / 2 / 0 | ninguno |
internal/prompts (workflows.go, W20 H10) | 93/94 | 93/94 | no ejecutado | 0 / 0 / 0 / 0 | ninguno |
cmd/reproduce_release (nuevo, W20 H8) | — | 131/146 | — | 47 / 5 / 38 / 0, y main.go tras la revisión 19 / 2 / 30 / 0 | 1 vivo, equivalente |
cmd/audit_doc_names (W20 H12) | 69/90 | 123/134 | no ejecutado | 70 / 18 / 0 / 0 | ninguno |
cmd/audit_site_headers (W20 H11, nuevo) | ninguno | 205/206 | ninguno | 121 / 2 / 1 / 0 | ninguno |
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
caseni 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 quego vetrechaza; los 33 de wiring salvo 1 equivalente y 4 que no compilan; los 68 de config salvo 2 quego vetrechaza (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)) entoolutil(tres) yresources; 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 formaif a == b { a = b }:t < FetchTimeoutencompletions,ActionTimeout < Timeoutenconfig, eltotalde una segunda etapa y el ajustetotal < currentdel progreso detoolutil,seconds > MaxStreamSeconds; comparaciones cuyo resultado no puede cambiar: las dos guardas desameValue, 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 porquefitsTypeva antes; elerr != nildenetworkOf(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](, uncase s[i] < 0x20que el bucle anterior hace inalcanzable en 0x20, el espacio deinSequence, una línea de longitud 0 queacceptignora; una negación que solo afecta a un registro de depuración (el error deNotifyProgress); un intervalo de progreso 0, queNewProgressnunca conserva, y el instante igual al intervalo;r > unicode.MaxASCIIfrente a>=, donde DEL se rechaza en ambos casos; el contador de un/48pasado 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) > 0frente a>= 0enClient.URLdearcane, 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 defilepath.Separatorjunto 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 buclei+1 < len(n.Content)frente a<=sobre mapas de YAML (el bloque de permisos enwritePermission, los servicios enjobImages, los trabajos enworkflowFromNode,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 dewritePermissionse alcanzaba con unpermissions: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); ylen(parts) < 2frente a<=enstep.action, donde una referencia de dos partes daowner/repoen ambos casos. - En
config,len(entries) == 0 || len(known) == 0frente 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, els.writer != nilantes deflushed()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; unLookupque 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 delcaseque 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 paquetejsonrpcacaba 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;
serveSDKyallowRatesolo 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.goes la del transporte HTTP sin estado, medida eninternal/wiring(http_stateless_elicit_test.go), no en los tests propios detoolutil. gobco da la rama de pánico deSchemaForcomo nunca tomada, aunqueTestSchemaFor_UnsupportedType_Panicsla 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.
Fuzzing
Sección titulada «Fuzzing»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.
Evaluación con modelos
Sección titulada «Evaluación con modelos»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.
En la CI
Sección titulada «En la CI»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).
Las cabeceras del sitio de documentación
Sección titulada «Las cabeceras del sitio de documentación»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.
| Cabecera | Valor que fija la regla | Se rechaza |
|---|---|---|
Content-Security-Policy | default-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-report | Sin 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-Security | max-age=31536000; includeSubDomains; preload | Sin max-age legible, o de menos de un año |
X-Content-Type-Options | nosniff | Cualquier otro valor |
X-Frame-Options | DENY | Un valor que no rechaza ningún marco (ALLOW-FROM o ALLOWALL solos) |
Referrer-Policy | strict-origin-when-cross-origin | unsafe-url, no-referrer-when-downgrade, o ninguna política que un navegador conozca |
Permissions-Policy | camera=(), 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.