Pruebas de Regresión con IA
Patrones de prueba diseñados específicamente para el desarrollo asistido por IA, donde el mismo modelo escribe y revisa código, creando puntos ciegos sistemáticos que solo las pruebas automatizadas pueden detectar.
Cuándo Activarlo
- Un agente de IA (Claude Code, Cursor, Codex) ha modificado rutas de API o lógica de backend
- Se encontró y corrigió un error, y se necesita prevenir su reintroducción
- El proyecto tiene un modo sandbox/mock que se puede aprovechar para pruebas sin base de datos
- Se ejecuta
/bug-checko comandos de revisión similares después de cambios de código - Existen múltiples rutas de código (sandbox vs producción, feature flags, etc.)
El Problema Fundamental
Cuando una IA escribe código y luego revisa su propio trabajo, arrastra los mismos supuestos a ambos pasos. Esto crea un patrón de fallo predecible:
IA escribe corrección → IA revisa corrección → IA dice "parece correcto" → El error sigue existiendo
Ejemplo real (observado en producción):
Corrección 1: Se añadió notification_settings a la respuesta de la API
→ Se olvidó añadirlo a la consulta SELECT
→ La IA lo revisó y no lo detectó (mismo punto ciego)
Corrección 2: Se añadió a la consulta SELECT
→ Error de compilación TypeScript (columna no está en los tipos generados)
→ La IA revisó la Corrección 1 pero no detectó el problema del SELECT
Corrección 3: Se cambió a SELECT *
→ Se corrigió la ruta de producción, se olvidó la ruta sandbox
→ La IA lo revisó y lo volvió a pasar por alto (cuarta aparición)
Corrección 4: La prueba lo detectó al instante en la primera ejecución PASÓ:
El patrón: la inconsistencia entre la ruta sandbox y producción es la regresión número 1 introducida por IA.
Pruebas de API en Modo Sandbox
La mayoría de los proyectos con arquitectura amigable para IA tienen un modo sandbox/mock. Esta es la clave para pruebas de API rápidas y sin base de datos.
Configuración (Vitest + Next.js App Router)
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
// __tests__/setup.ts
// Forzar modo sandbox — no se necesita base de datos
process.env.SANDBOX_MODE = "true";
process.env.NEXT_PUBLIC_SUPABASE_URL = "";
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "";
Helper de Prueba para Rutas de API de Next.js
// __tests__/helpers.ts
import { NextRequest } from "next/server";
export function createTestRequest(
url: string,
options?: {
method?: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
sandboxUserId?: string;
},
): NextRequest {
const { method = "GET", body, headers = {}, sandboxUserId } = options || {};
const fullUrl = url.startsWith("http") ? url : `http://localhost:3000${url}`;
const reqHeaders: Record<string, string> = { ...headers };
if (sandboxUserId) {
reqHeaders["x-sandbox-user-id"] = sandboxUserId;
}
const init: { method: string; headers: Record<string, string>; body?: string } = {
method,
headers: reqHeaders,
};
if (body) {
init.body = JSON.stringify(body);
reqHeaders["content-type"] = "application/json";
}
return new NextRequest(fullUrl, init);
}
export async function parseResponse(response: Response) {
const json = await response.json();
return { status: response.status, json };
}
Escribir Pruebas de Regresión
El principio clave: escribe pruebas para los errores que se encontraron, no para el código que funciona.
// __tests__/api/user/profile.test.ts
import { describe, it, expect } from "vitest";
import { createTestRequest, parseResponse } from "../../helpers";
import { GET, PATCH } from "@/app/api/user/profile/route";
// Definir el contrato — qué campos DEBEN estar en la respuesta
const REQUIRED_FIELDS = [
"id",
"email",
"full_name",
"phone",
"role",
"created_at",
"avatar_url",
"notification_settings", // ← Añadido después de que un error lo encontrara faltante
];
describe("GET /api/user/profile", () => {
it("devuelve todos los campos requeridos", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { status, json } = await parseResponse(res);
expect(status).toBe(200);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
// Prueba de regresión — este error exacto fue introducido por IA 4 veces
it("notification_settings no es undefined (regresión BUG-R1)", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { json } = await parseResponse(res);
expect("notification_settings" in json.data).toBe(true);
const ns = json.data.notification_settings;
expect(ns === null || typeof ns === "object").toBe(true);
});
});
Pruebas de Paridad Sandbox/Producción
La regresión más común de IA: corregir la ruta de producción pero olvidar la ruta sandbox (o viceversa).
// Prueba que las respuestas sandbox coincidan con el contrato esperado
describe("GET /api/user/messages (lista de conversaciones)", () => {
it("incluye partner_name en modo sandbox", async () => {
const req = createTestRequest("/api/user/messages", {
sandboxUserId: "user-001",
});
const res = await GET(req);
const { json } = await parseResponse(res);
// Esto detectó un error donde se añadió partner_name
// a la ruta de producción pero no a la ruta sandbox
if (json.data.length > 0) {
for (const conv of json.data) {
expect("partner_name" in conv).toBe(true);
}
}
});
});
Integrar Pruebas en el Flujo de Verificación de Errores
Definición de Comando Personalizado
<!-- .claude/commands/bug-check.md -->
# Verificación de Errores (Bug Check)
## Paso 1: Pruebas Automatizadas (obligatorio, no se puede saltar)
Ejecuta estos comandos PRIMERO antes de cualquier revisión de código:
npm run test # Suite de pruebas Vitest
npm run build # Verificación de tipos TypeScript + compilación
- Si las pruebas fallan → reporta como error de máxima prioridad
- Si la compilación falla → reporta errores de tipo como máxima prioridad
- Solo continúa al Paso 2 si ambos pasan
## Paso 2: Revisión de Código (revisión por IA)
1. Consistencia entre rutas sandbox / producción
2. La forma de la respuesta de la API coincide con las expectativas del frontend
3. Integridad de la cláusula SELECT
4. Manejo de errores con retroceso (rollback)
5. Condiciones de carrera en actualizaciones optimistas
## Paso 3: Para cada error corregido, propón una prueba de regresión
El Flujo de Trabajo
Usuario: "バグチェックして" (o "/bug-check")
│
├─ Paso 1: npm run test
│ ├─ FALLA → Error encontrado mecánicamente (sin necesidad de juicio de IA)
│ └─ PASA → Continuar
│
├─ Paso 2: npm run build
│ ├─ FALLA → Error de tipo encontrado mecánicamente
│ └─ PASA → Continuar
│
├─ Paso 3: Revisión de código por IA (teniendo en cuenta los puntos ciegos conocidos)
│ └─ Hallazgos reportados
│
└─ Paso 4: Para cada corrección, escribir una prueba de regresión
└─ La próxima verificación de errores detectará si la corrección se rompe
Patrones Comunes de Regresión de IA
Patrón 1: Discrepancia entre Rutas Sandbox/Producción
Frecuencia: Muy común (observado en 3 de cada 4 regresiones)
// FALLA: La IA añade campo solo a la ruta de producción
if (isSandboxMode()) {
return { data: { id, email, name } }; // Falta el nuevo campo
}
// Ruta de producción
return { data: { id, email, name, notification_settings } };
// PASA: Ambas rutas deben devolver la misma forma
if (isSandboxMode()) {
return { data: { id, email, name, notification_settings: null } };
}
return { data: { id, email, name, notification_settings } };
Prueba para detectarlo:
it("sandbox y producción devuelven los mismos campos", async () => {
// En el entorno de prueba, el modo sandbox está forzado a ON
const res = await GET(createTestRequest("/api/user/profile"));
const { json } = await parseResponse(res);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
Patrón 2: Omisión en la Cláusula SELECT
Frecuencia: Común con Supabase/Prisma al añadir nuevas columnas
// FALLA: Nueva columna añadida a la respuesta pero no al SELECT
const { data } = await supabase
.from("users")
.select("id, email, name") // notification_settings no está aquí
.single();
return { data: { ...data, notification_settings: data.notification_settings } };
// → notification_settings siempre es undefined
// PASA: Usar SELECT * o incluir explícitamente las nuevas columnas
const { data } = await supabase
.from("users")
.select("*")
.single();
Patrón 3: Fuga del Estado de Error
Frecuencia: Moderada — al añadir manejo de errores a componentes existentes
// FALLA: Estado de error establecido pero datos antiguos no se limpian
catch (err) {
setError("No se pudo cargar");
// ¡reservations todavía muestra datos de la pestaña anterior!
}
// PASA: Limpiar el estado relacionado al ocurrir un error
catch (err) {
setReservations([]); // Limpiar datos obsoletos
setError("No se pudo cargar");
}
Patrón 4: Actualización Optimista Sin Retroceso Apropiado
// FALLA: Sin retroceso en caso de fallo
const handleRemove = async (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
await fetch(`/api/items/${id}`, { method: "DELETE" });
// Si la API falla, el elemento desaparece de la UI pero sigue en la BD
};
// PASA: Capturar el estado anterior y retroceder en caso de fallo
const handleRemove = async (id: string) => {
const prevItems = [...items];
setItems(prev => prev.filter(i => i.id !== id));
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Error de API");
} catch {
setItems(prevItems); // Retroceder
alert("Error al eliminar");
}
};
Estrategia: Prueba Donde Se Encontraron Errores
No apuntes a una cobertura del 100%. En su lugar:
Error encontrado en /api/user/profile → Escribir prueba para la API de perfil
Error encontrado en /api/user/messages → Escribir prueba para la API de mensajes
Error encontrado en /api/user/favorites → Escribir prueba para la API de favoritos
Sin error en /api/user/notifications → No escribir prueba (aún)
Por qué funciona con el desarrollo de IA:
- La IA tiende a cometer el mismo tipo de error repetidamente
- Los errores se agrupan en áreas complejas (autenticación, lógica de múltiples rutas, gestión de estado)
- Una vez probado, esa regresión exacta no puede volver a ocurrir
- El número de pruebas crece orgánicamente con las correcciones de errores — sin esfuerzo desperdiciado
Referencia Rápida
| Patrón de Regresión de IA | Estrategia de Prueba | Prioridad |
|---|---|---|
| Discrepancia sandbox/producción | Afirmar la misma forma de respuesta en modo sandbox | Alta |
| Omisión en cláusula SELECT | Afirmar todos los campos requeridos en la respuesta | Alta |
| Fuga del estado de error | Afirmar limpieza de estado al ocurrir un error | Media |
| Falta de retroceso | Afirmar que el estado se restaura al fallar la API | Media |
| Conversión de tipo que enmascara null | Afirmar que el campo no es undefined | Media |
HACER / NO HACER
HACER:
- Escribir pruebas inmediatamente después de encontrar un error (antes de corregirlo si es posible)
- Probar la forma de la respuesta de la API, no la implementación
- Ejecutar pruebas como primer paso de cada verificación de errores
- Mantener las pruebas rápidas (< 1 segundo en total con modo sandbox)
- Nombrar las pruebas según el error que previenen (por ejemplo, "regresión BUG-R1")
NO HACER:
- Escribir pruebas para código que nunca ha tenido un error
- Confiar en la autorevisión de la IA como sustituto de las pruebas automatizadas
- Saltar las pruebas de la ruta sandbox porque "son solo datos simulados"
- Escribir pruebas de integración cuando las pruebas unitarias son suficientes
- Apuntar a un porcentaje de cobertura — apunta a la prevención de regresiones