---
name: calidad-mutation-testing
description: >
  Mutation testing: medir la calidad real de una suite de tests inyectando
  mutantes (cambios sintácticos pequeños) en el código y verificando que los
  tests los maten. Cubre herramientas por stack (Stryker, mutmut, cargo-mutants,
  PIT, Stryker.NET, Infection), interpretación de mutantes sobrevivientes,
  modo incremental para que el costo sea viable, y uso del mutation score como
  métrica Verify/Guard en loops de autoresearch o como gate opcional de
  tdd-qa-swl. Cargar cuando la cobertura de líneas es alta pero se sospecha de
  asserts débiles, al endurecer la suite de un módulo crítico, o al configurar
  el gate de mutación en CI.
version: "1.0.0"
herramientasPermitidas: [Read, Bash, Grep, Glob]
exclusiones:
  - "No cargar si la suite no está verde y estable — el mutation testing presupone tests deterministas que pasan; con tests flaky el score es ruido."
  - "No cargar para subir cobertura de líneas — eso es tdd-workflow; la mutación mide calidad de asserts, no cantidad de tests."
  - "No cargar en proyectos sin tests — primero tdd-qa-swl construye la suite; mutar sin tests produce 100% de sobrevivientes sin información."
evolvable: true
---
# Mutation Testing — La Suite que Vigila a la Suite

La cobertura de líneas responde "¿qué código ejecutan los tests?". El mutation
testing responde la pregunta importante: **"¿los tests detectarían un bug?"**.
Una suite con 90% de cobertura y asserts débiles pasa el gate de `pruebas.md`
sin proteger nada — los mutantes sobrevivientes lo exponen.

**Principio**: un test que no mata mutantes documenta ejecución, no comportamiento.

## Cuándo cargar este skill

- Módulo crítico (pagos, auth, cálculo) con cobertura alta pero bugs que se
  filtran a producción — sospecha de asserts débiles.
- Cerrar el ciclo TDD estilo Uncle Bob: spec → tests → código → revisión →
  **mutación** como verificación final de la suite.
- Configurar mutation score como métrica de un loop `/swl:autoresearch --codigo`
  o como gate opcional en CI.
- Auditar la suite que entrega `tdd-qa-swl` antes de declarar una fase verde.

## Cómo funciona

1. La herramienta genera **mutantes**: copias del código con un cambio mínimo
   (`>` → `>=`, `+` → `-`, borrar una llamada, `true` → `false`).
2. Corre la suite contra cada mutante.
3. Clasifica: **muerto** (algún test falló — bien), **sobreviviente** (la suite
   pasó con el bug inyectado — gap real), **timeout** (cuenta como muerto),
   **no cubierto** (ningún test lo ejecuta — gap de cobertura clásico).

```
mutation score = mutantes muertos / (mutantes totales − equivalentes) × 100
```

## Herramientas por stack

Antes de instalar, verificar versión vigente con Context7
(regla `usar-context7.md`) — los nombres de paquete cambian entre majors.

| Stack | Herramienta | Comando típico | Score en JSON |
|-------|------------|----------------|---------------|
| JS/TS | Stryker (`@stryker-mutator/core`) | `npx stryker run` | `reports/mutation/mutation.json` → `.thresholds` / score en summary |
| Python | `mutmut` | `mutmut run && mutmut results` | `mutmut junitxml` o parsear `mutmut results` |
| Python (alterno) | `cosmic-ray` | `cosmic-ray init/exec/dump` | `cr-report --json` |
| Rust | `cargo-mutants` | `cargo mutants` | `mutants.out/outcomes.json` |
| Java/Kotlin | PIT (`pitest`) | `mvn org.pitest:pitest-maven:mutationCoverage` | `target/pit-reports/mutations.xml` |
| C#/.NET | Stryker.NET (`dotnet-stryker`) | `dotnet stryker` | `StrykerOutput/**/mutation-report.json` |
| PHP | Infection | `vendor/bin/infection` | `infection-log.json` → MSI |
| Go | `gremlins` | `gremlins unleash` | salida estructurada con `--output` |

## Hacer viable el costo — modo incremental SIEMPRE

Mutar el proyecto completo es O(mutantes × duración de suite). En un repo
mediano son horas. Reglas para que sea operable:

- **Mutar solo lo que cambió**: Stryker `--since`/modo incremental, `mutmut`
  con `--paths-to-mutate`, `cargo mutants --in-diff <(git diff main)`. El gate
  de PR muta el diff, no el repo.
- **Acotar al módulo crítico**: configurar `mutate:` solo sobre
  `src/pagos/**`, no sobre todo `src/`. El score global de un repo grande es
  una métrica vanidosa; el score del módulo de dinero es accionable.
- **Suite rápida primero**: si la suite tarda >2 min, mutar solo con los tests
  unitarios del módulo (los runners permiten filtrar la suite que ejecutan).
- **Paralelizar**: todos los runners soportan concurrencia
  (`--concurrency`, `--jobs`); default razonable: núcleos − 1.

## Interpretar mutantes sobrevivientes

Cada sobreviviente es una de tres cosas — diagnosticar antes de actuar:

| Diagnóstico | Señal | Acción |
|-------------|-------|--------|
| **Assert débil** | el test ejecuta la línea pero no verifica el resultado mutado | Endurecer el assert (caso típico: verifica que no lanza, no el valor) |
| **Test faltante** | ningún test cubre el comportamiento de esa rama | Escribir test de frontera dirigido al mutante (`tdd-qa-swl`) |
| **Mutante equivalente** | el mutante no cambia el comportamiento observable (ej: optimización interna) | Excluirlo/anotarlo — NO escribir un test artificial para matarlo |

Anti-patrón crítico: escribir tests que asertan detalles de implementación
solo para matar mutantes — eso acopla la suite y degrada el refactor. El
mutante manda la pregunta; el test responde al **comportamiento**.

## Umbrales recomendados

| Contexto | Score objetivo | Justificación |
|----------|---------------|---------------|
| Módulo crítico (dinero, auth, cálculo regulatorio) | ≥ 85% | Un bug aquí cuesta más que el CI lento |
| Lógica de negocio estándar | ≥ 70% | Equilibrio costo/valor |
| Glue code, configs, controllers delgados | sin gate | El esfuerzo no paga; cubrir con tests de integración |

No imponer un score global de repo: produce esfuerzo uniforme sobre código de
valor desigual. Gates por módulo, declarados en la config del runner.

## Uso como métrica en loops SWL

El mutation score es la métrica ideal para `/swl:autoresearch --codigo` porque
es numérica, determinista y su Guard natural es la propia suite:

```
Goal: subir mutation score de src/pagos/ de 62% a 85%
Scope: tests/pagos/**
Metric: mutation score (higher_is_better)
Verify: npx stryker run --mutate "src/pagos/**" --incremental && <extraer score del JSON>
Guard: npm test (la suite completa sigue verde)
```

Cada iteración: agregar/endurecer UN test → correr Verify → keep si el score
sube y el Guard pasa. Registrar con `hooks/lib/loop-telemetry.js`.

**Gate opcional en tdd-qa-swl**: tras alcanzar cobertura ≥80%, correr mutación
incremental sobre el diff de la fase; sobrevivientes con diagnóstico "assert
débil" o "test faltante" se atienden antes del cierre (regla
`arreglar-al-detectar.md`). Es opt-in: requiere runner instalado y suite <2 min.

## Cuándo NO cargar

- Suite roja o flaky — primero estabilizar (`pruebas.md § deterministas`); el
  mutation testing amplifica el ruido de tests no deterministas.
- Prototipo o spike de descarte — el costo del setup no se recupera.
- Presupuesto de CI ya saturado — correr mutación local/nightly, no por PR,
  hasta resolver el presupuesto.

## Gotchas / Errores comunes no obvios

- **El score baja al agregar código nuevo bien testeado**: el denominador
  crece con mutantes del código nuevo; si el módulo viejo tenía deuda, el
  score agregado oscila. Causa: medir score global en vez de por módulo/diff.
  Solución: gates incrementales (`--since`, `--in-diff`) — el código nuevo se
  evalúa contra su propio diff.
- **Timeouts contados como éxito inflan el score**: un mutante que cuelga la
  suite cuenta como "muerto" aunque ningún assert lo detectó. Si el módulo
  tiene loops sensibles, revisar el desglose `timeout` vs `killed` antes de
  celebrar — un ratio de timeouts >10% amerita bajar el timeout factor.
- **Mutación sobre código generado**: mutar archivos generados (protobuf,
  cliente OpenAPI, migraciones) quema horas sin información. Excluirlos
  explícitamente en la config del runner desde el día uno.
- **mutmut y el cache stale**: `mutmut` cachea resultados en `.mutmut-cache`;
  tras un refactor grande el cache puede reportar resultados de código que ya
  no existe. Ante números inverosímiles: borrar el cache y re-correr.
- **Stryker incremental tras rebase**: el archivo `.stryker-tmp`/incremental
  referencia commits que el rebase reescribió — el modo incremental se
  degrada a corrida completa sin avisar. Presupuestar la primera corrida
  post-rebase como completa.

## Anti-patrones

- **Tests escritos para matar mutantes, no para verificar comportamiento** —
  acoplan la suite a la implementación.
- **Score global de repo como KPI de equipo** — métrica vanidosa; gates por
  módulo crítico.
- **Correr mutación completa en cada PR** — CI de horas; incremental por diff
  en PR, completa nightly.
- **Ignorar el desglose y mirar solo el porcentaje** — los sobrevivientes
  individuales son la información; el score es solo el resumen.
