Complemento de
docs/plan-lab.md.Estado a jul 24 2026: Fases 0-4 completas (tests base, CI/CD + rollback, pasarela de pagos, chaos engineering, SLO/error budget), Fase 6 completa (SAST el 17 jul, DAST el 23 jul, a11y el 17 jul) y Fase 7 completa (mutation testing con score real 87.2 % + contratos Zod, 17 jul). Queda solo la Fase 5 (load testing con k6), bloqueada por la falta de un target de preview/staging estable →
VERCEL_TOKEN.Este documento detalla las Fases 5, 6 y 7 con alcance, archivos concretos, criterios de aceptación y notas de costo/seguridad.
Convenciones ya establecidas que estas fases reutilizan:
- Ingesta de artefactos de CI:
POST /api/lab/ingestconAuthorization: Bearer LAB_INGEST_TOKEN(timingSafeEqual). Hoy solo aceptakind: 'ci_run'; se amplía por fase.- Páginas admin: bajo
/admin/lab/*, protegidas porsrc/middleware.ts. Datos siempre escapados conesc()antes deinnerHTML.- APIs de lectura admin:
/api/admin/lab/*(solo GET, tras el guard).- Migraciones Drizzle: solo aditivas (
npx drizzle-kit generate+migratecon Node 22).- Tests: Vitest; lógica pura sin tocar BD, o BD libsql en archivo temporal (no
:memory:, por las transacciones).
Objetivo: mostrar latencia y throughput bajo carga (100 / 500 / 1000 usuarios concurrentes) con gráficas p50/p95/p99, y contar la historia de "cómo se comporta el sistema cuando lo aprietan". Casi nadie a nivel tecnólogo lo hace.
La carga NUNCA va contra codebymike.tech (producción):
- Vercel factura por invocación + CPU activa; 1000 VUs contra prod = costo real + posible bloqueo del WAF/BotID.
- Turso tiene límite de filas leídas/escritas en el plan gratuito.
La carga va contra un preview deployment desechable o un target de staging.
El job de k6 es manual (workflow_dispatch), nunca en cada push.
- Scripts k6 en
lab/k6/:home.js— GET a la home pública (renderizado SSR).api-read.js— GET a un endpoint público de lectura (p. ej./api/health).checkout.js— POST a/api/payments/checkouten modo mock (idempotency key única por VU/iteración) para medir la ruta de escritura + BD bajo carga.- Escenario con etapas (
stages): ramp-up a 100 → 500 → 1000 VUs y ramp-down, conthresholds(p. ej.http_req_duration: p(95)<800).
- Tabla
load_test_runs(migración nueva):id, tool ('k6'), scenario, vus, durationS, p50, p95, p99, rps, errorRate, checksPassed, checksFailed, rawJson, target, createdAt - Ingesta: ampliar
POST /api/lab/ingestpara aceptarkind: 'load_test'. El job parsea elsummary.jsonde k6 (handleSummary→ métricas) y lo postea. - Endpoint admin
GET /api/admin/lab/load— últimas corridas. - Página
/admin/lab/load:- Tarjetas por corrida con p50/p95/p99, RPS y tasa de error.
- Gráfica de latencia por nivel de carga (SVG inline o
<canvas>sin libs externas; recordar CSP de artifacts si se exporta). - Comparativa 100 vs 500 vs 1000 VUs.
- Sidebar: link "Load testing" en el grupo LAB.
- Workflow
.github/workflows/load-test.yml:workflow_dispatchcon inputstarget_urlymax_vus.- Instala k6 (
grafana/setup-k6-actiono binario), corre los scripts, sube el summary a la ingesta. - Guard: rechaza si
target_urlcontienecodebymike.tech.
Opción A (recomendada, sin gastar en CI): correr k6 localmente contra un
preview deploy (k6 run lab/k6/home.js -e TARGET=https://<preview>.vercel.app),
el script postea el summary a la ingesta y la tarjeta aparece en /admin/lab/load.
Opción B: disparar el workflow_dispatch desde la pestaña Actions en vivo.
-
lab/k6/*.jscorren local con k6 y generan summary. -
load_test_runspoblada vía ingesta autenticada. -
/admin/lab/loadmuestra p50/p95/p99 y gráfica por nivel de VUs. - Ningún script apunta a producción por defecto; guard en el workflow.
- Tests: parser del summary de k6 (función pura) + validación del payload de ingesta
load_test.
Objetivo: correr análisis de seguridad automáticos y de accesibilidad, y mostrar hallazgos → estado (abierto / resuelto / aceptado). La narrativa "encontré X y lo resolví" vale más ante el jurado que un scan vacío.
- Job de CI
security.yml(en PRs y push a main):npm audit --json→ parse de vulnerabilidades de dependencias.- Semgrep (
returntocorp/semgrep-action) o Snyk (free tier) con reglas para JS/TS. - CodeQL de GitHub (gratis en repos públicos como
dev-portfolio) como opción nativa.
- Resultados →
POST /api/lab/ingestconkind: 'security_finding'.
- OWASP ZAP baseline (
zaproxy/action-baseline) contra el preview deployment de cada PR (.github/workflows/dast.yml), nunca contra producción. - La URL del preview se obtiene sondeando la API de Deployments de GitHub
(
actions/github-script+GITHUB_TOKEN), sin depender deVERCEL_TOKEN— a diferencia de la Fase 5, este job no está bloqueado por ese secret. - Decisión de diseño:
spider.parseRobotsTxt=falsepara que ZAP se comporte como el "crawler legítimo" quepublic/robots.txtle pide y nunca trate las rutas honeypot (/wp-login.php,/admin.php,/admin,/api) como objetivos. Si las pisara, el rate limiter durable bloquearía la IP del runner y el resto del scan perdería cobertura — no sería una vulnerabilidad real, sería un autogol de configuración. - Salida JSON (
report_json.json) →scripts/zap-ingest.mjs→ ingesta consource: 'zap',autoResolve: true. Cada alerta × instancia es un hallazgo, acotado a 15 instancias por alerta. - Pendiente de verificar en un PR real (no se ha corrido aún contra un
preview vivo): confirmar tiempos de espera del deployment y que
zaproxy/action-baselineno necesite ajustes decmd_optionsadicionales.
@axe-core/playwrighto Lighthouse CI sobre las páginas públicas (/,/projects,/contact,/pay).- Verifica contraste, roles ARIA, navegación por teclado, alt text.
- Score + violaciones → ingesta.
- Tabla
security_findings(migración):id, source ('npm-audit'|'semgrep'|'snyk'|'zap'|'codeql'|'axe'|'lighthouse'), severity ('critical'|'high'|'medium'|'low'|'info'), title, description, route, ruleId, status ('open'|'resolved'|'accepted'), resolvedAt, firstSeenAt, createdAt- Dedup por
(source, ruleId, route)para no duplicar el mismo hallazgo entre corridas.
- Dedup por
- Ampliar ingesta con
kind: 'security_finding'(acepta lote). - Endpoint admin
GET /api/admin/lab/security+PATCHpara marcar resuelto/aceptado (con nota). - Página
/admin/lab/security:- Resumen por severidad (contadores tipo semáforo).
- Lista filtrable por estado/fuente; acción "marcar resuelto".
- Tarjeta de a11y con score de Lighthouse/axe.
- Sidebar: link "Seguridad".
- Workflows:
security.yml(SAST), extensión DAST enci.ymlo job aparte,a11y.yml.
-
npm audit+ un SAST (Semgrep/CodeQL) reportan asecurity_findings. - ZAP baseline corre contra preview, nunca contra prod.
- a11y con score visible en el panel.
- Hallazgos se pueden marcar resueltos y persisten (dedup funciona).
- Tests: parser de SARIF/JSON, dedup de findings, transición de estado.
Objetivo: demostrar calidad de las pruebas mismas, no solo cobertura. Casi nadie a nivel tecnólogo conoce mutation testing; sorprende.
- Stryker con
@stryker-mutator/vitest-runnersobresrc/lib/**(donde vive la lógica pura: money, pnl, payments, chaos, slo, domains…). - Config
stryker.config.json:mutate: ['src/lib/**/*.ts'],testRunner: 'vitest',reporters: ['json', 'clear-text'], thresholds (high 80 / low 60 / break 50). - Es lento → job manual/semanal (
workflow_dispatchoschedulecron), nunca en cada push. - El
mutation-report.json→ parse delmutationScore→ ingestakind: 'mutation_run'. - Mostrar junto a cobertura en
/admin/lab/quality(o/admin/lab/pipeline): "87% cobertura, 74% mutation score — y sé explicar la diferencia".- La diferencia es el gancho: cobertura dice "esta línea se ejecutó"; mutation dice "si rompo esta línea, ¿algún test se da cuenta?". Un mutation score bajo con cobertura alta = tests que no verifican de verdad.
- Front y API viven en el mismo repo Astro → Pact (consumer/provider entre
repos separados) es sobredimensionado. Versión honesta y defendible:
- Esquemas Zod compartidos para las respuestas de
/api/*clave (health, payments checkout, monitors, slo). - Tests de contrato: snapshot del shape de cada respuesta; si un endpoint cambia su forma sin actualizar el esquema, el test falla → "el contrato se rompió".
- Documentar que Pact aplicaría a SlideHub/microservicios separados como extensión.
- Esquemas Zod compartidos para las respuestas de
- Dependencias dev:
@stryker-mutator/core,@stryker-mutator/vitest-runner,zod. stryker.config.json+ scriptnpm run test:mutation.- Tabla
mutation_runs(o reutilizarci_runscon columnamutationScoreya existente — decidir: haycoveragePctymutationScoreenci_runs, quizá basta ampliar el ingest deci_run). - Esquemas Zod en
src/lib/contracts.ts+ teststests/contracts.test.ts. - Workflow
mutation.yml(workflow_dispatch+schedulesemanal). - UI: tarjeta de mutation score en el panel (junto a cobertura del pipeline).
-
npm run test:mutationcorre Stryker sobresrc/liby produce score. - Mutation score visible en el panel, reportado vía ingesta.
- Esquemas Zod cubren las respuestas de al menos 4 endpoints
/api/*. - Un cambio de shape en un endpoint rompe su test de contrato.
- Job de mutation es manual/semanal (no en cada push).
| Fase | Estado | Entrega clave | Tablas nuevas | Workflows |
|---|---|---|---|---|
| 5 Load testing | ⏳ pendiente | Gráficas p50/p95/p99 bajo 100-1000 VUs | load_test_runs |
load-test.yml (manual) |
| 6 SAST/DAST + a11y | ✅ 17 y 23 jul | Hallazgos → resueltos, violaciones WCAG reales | security_findings |
security.yml, a11y.yml, dast.yml |
| 7 Mutation + contratos | ✅ 17 jul | Mutation score 87.2 % + contratos Zod | (reusa ci_runs) |
mutation.yml (semanal) |
El orden real fue 6 → 7 → 5, no el 5 → 7 → 6 que se recomendaba aquí. El motivo: la 6 reaprovechaba Playwright recién instalado por los e2e y salió casi gratis, y la 5 resultó ser la más dependiente de infra externa (no la 6), porque necesita un target de preview estable que sigue sin existir.
- VERCEL_TOKEN en GitHub Secrets: sigue faltando (
gh secret listno lo muestra). Sin él, el rollback de la Fase 1 solo avisa, no revierte. Subirlo congh secret set VERCEL_TOKEN --repo mikerb95/dev-portfolio. Llaves Wompi reales (Fase 2): ✅WOMPI_PUBLIC_KEY,WOMPI_INTEGRITY_SECRETyWOMPI_EVENTS_SECRETestán en Vercel (Production) desde el 16 jul 2026. Falta confirmar el registro del webhookhttps://codebymike.tech/api/payments/webhooken el panel de Wompi y hacer un cobro real de punta a punta.- Dashboard
/adminconsolidado: tarjetas resumen que ya tienen datos (badge CI, cobertura, SLO/error budget, último mutation score) en la portada del panel para el arranque de la sustentación. - Guion de sustentación (ya esbozado en
plan-lab.md): ensayar la secuencia push-con-bug → rollback → chaos → payments-lab → k6/ZAP.