Un formateador convierte build:cache:hit en una nota que termina en cache. Otra entrada deja espacios dobles en la salida. No hay pruebas que indiquen si esos comportamientos eran intencionales. Antes de pedir a la IA que simplifique el código, necesitas detectar si esos resultados cambian.
Supongamos que el formateador escribe notas fechadas a partir de cadenas label:detail. Descarga el ejemplo completo de pruebas de caracterización y ejecútalo con Node.js 22 o posterior. La ejecución registrada usó 22.22.1, sin dependencias externas.
Observa varias entradas antes de proponer una implementación mejor
Descomprime la descarga, entra en characterization-tests-before-ai-refactoring y ejecuta:
node observe.mjs original
El comando imprime doce observaciones en JSON, con cada entrada, un instante proporcionado, un entorno proporcionado y la salida. Empieza con una cadena corriente y cambia un detalle cada vez. Estas son dos entradas de esa ejecución, mostrando solo entrada y salida:
[
{
"text": "build:cache:hit",
"output": "2026-10-13 | BUILD | cache"
},
{
"text": " build : ready ",
"output": "2026-10-13 | BUILD | ready"
}
]
La segunda salida tiene dos espacios antes y después del último |. Mantenlos visibles mientras investigas. Las otras observaciones muestran que build y build: terminan en -; una etiqueta vacía usa el NOTE_LABEL proporcionado, o note cuando falta o está vacío.
Abre ahora original.mjs:
export function formatNote(text, runtime = systemRuntime) {
const fields = text.trim().split(':');
const fallback = runtime.readEnv('NOTE_LABEL') || 'note';
const label = (fields[0] || fallback).toUpperCase();
const detail = fields[1] || '-';
const day = runtime.now().toISOString().slice(0, 10);
return `${day} | ${label} | ${detail}`;
}
fields[1] explica la pérdida de hit: nunca se usan los campos posteriores. El primer trim() actúa sobre toda la entrada, por lo que conserva el espacio después de build y antes de ready. El renderizado añade sus propios espacios separadores junto a ellos.
Una observación te da algo que investigar antes de elegir una corrección. Michael Feathers describe una progresión similar, desde llamar al código existente y examinar su respuesta hasta guardar interacciones útiles como pruebas, en Unit Conversations.
Haz explícitos la fecha y el entorno
Una salida guardada con la fecha de hoy fallará mañana. El NOTE_LABEL de un desarrollador también puede cambiar la salida de una etiqueta vacía. Esos valores deben formar parte de la entrada de la prueba.
El ejemplo ya ofrece un pequeño punto de sustitución: el argumento opcional runtime. Por defecto lee el reloj y la variable de entorno del proceso. Todas las observaciones y pruebas documentadas proporcionan, en cambio, esta implementación de runtime.mjs:
export function fixedRuntime({ at, env }) {
return {
now: () => new Date(at),
readEnv: (name) => env[name],
};
}
La mayoría de los casos usa 2026-10-13T12:00:00.000Z y { NOTE_LABEL: 'daily' }. Uno usa 2026-10-13T23:30:00-07:00; su salida comienza por 2026-10-14 porque toISOString() expresa la fecha en UTC. Hay casos específicos para variables ausentes y vacías.
Este punto de sustitución suele llamarse seam. Legacy Seam, de Fowler, explica cómo separar una dependencia para que las pruebas elijan su comportamiento. La descarga ya dispone de ese parámetro. Si el código real oculta la dependencia en su interior, introduce el punto de sustitución mínimo adecuado en un cambio separado y revisado antes de reorganizar el analizador; conserva cómo obtiene sus valores el recorrido por defecto.
Ninguna prueba cambia aquí el reloj global ni el entorno del proceso. Las comprobaciones también pasaron con el proceso hijo configurado con TZ=Pacific/Kiritimati y otro NOTE_LABEL, lo que confirma que estas observaciones dependen de las entradas proporcionadas. La interacción del adaptador por defecto con una aplicación real aún necesita una comprobación de integración.
Guarda lo ocurrido, incluidas las partes que cuestionas
cases.mjs contiene las entradas exploratorias. Tras ejecutar y examinar los resultados originales, los guardamos en characterized.json. Registra cadenas exactas, incluidos los espacios inesperados y los campos descartados.
Una prueba de caracterización pregunta si cambió el comportamiento seleccionado. No demuestra que el comportamiento registrado sea correcto. Por ejemplo, conservar cache en este fixture deja abierta la cuestión de si la aplicación debería conservar cache:hit.
run.mjs lee el JSON guardado y compara con él el resultado de cada implementación:
for (const item of fixtures) {
test(item.name, () => {
const actual = versions[mode](item.text, fixedRuntime(item));
assert.equal(actual, item.output);
});
}
El ejecutor nunca llama al formateador original para generar el resultado esperado durante la aserción. Por tanto, modificar la implementación original no puede redefinir silenciosamente la respuesta guardada. observe.mjs solo imprime; tampoco sobrescribe el fixture.
Ejecuta el original frente a ese registro:
node run.mjs original
Pasan las doce comprobaciones. Cubren entradas normales, detalles ausentes y vacíos, campos posteriores, espacios internos, entradas vacías, etiquetas alternativas presentes y ausentes, y la fecha UTC. Son entradas de cadena seleccionadas. No se caracterizaron aquí mayúsculas Unicode, fechas no válidas, argumentos que no sean cadenas, orden de efectos secundarios ni llamadores reales de la aplicación.
Refactoriza conservando los resultados guardados
La versión que conserva el comportamiento extrae el análisis y el renderizado en funciones con nombre. Su analizador mantiene deliberadamente la selección original de campos:
function parseFields(text) {
const [label, detail] = text.trim().split(':');
return { label, detail };
}
function render(day, label, detail) {
return `${day} | ${label} | ${detail}`;
}
refactored.mjs usa esas funciones y conserva las reglas de valores alternativos, la conversión a mayúsculas y la fecha UTC. Ejecuta:
node run.mjs refactored
Pasan las doce observaciones guardadas y la salida completa de observación coincide byte a byte con el original. Eso respalda esta refactorización para las entradas registradas y mantiene visibles los casos sin probar.
Si pides ayuda a la IA en este punto, proporciona el código y los casos de caracterización. Pídele conservar las cadenas existentes e identificar cualquier cambio que considere que alteraría el comportamiento. «Limpia este código» deja mucho más margen para decidir sin avisar sobre espacios o delimitadores.
Haz que una limpieza atractiva falle de forma visible
La limpieza hipotética de accidental.mjs usa un analizador que puede parecer más cómodo:
function parseFields(text) {
const [label, ...rest] = text
.trim()
.split(':')
.map((field) => field.trim());
return { label, detail: rest.join(':') };
}
Recorta cada campo y vuelve a unir el resto. Ejecuta las tres versiones por separado:
node run.mjs original
node run.mjs refactored
node run.mjs accidental
| Versión | Pasan | Fallan | Código de salida |
|---|---|---|---|
original |
12 | 0 | 0 |
refactored |
12 | 0 | 0 |
accidental |
10 | 2 | 1, fallo esperado |
Los casos fallidos son exactamente las dos sorpresas iniciales. build:cache:hit ahora termina en cache:hit, y la entrada con espacios produce 2026-10-13 | BUILD | ready. Ningún cambio se ocultó regenerando el fixture.
Usa node observe.mjs accidental para examinar las nuevas salidas. Ejecuta los comandos por separado si quieres continuar después del fallo esperado; unirlos con && detiene la ejecución al recibir el código 1. Por defecto, node --test run.mjs solo comprueba original.
El resultado rojo significa que la limpieza cambió el comportamiento registrado. Decidir si alguna salida nueva es preferible es el siguiente paso.
Decide por separado un cambio de comportamiento
DECISION.md propone un cambio para el caso de los dos puntos adicionales: conservar cache:hit en lugar de cache. Registra la salida antigua y la propuesta, y deja esta última fuera de la refactorización que conserva el comportamiento.
En una aplicación real, examina entradas representativas y consumidores de esta cadena antes de aceptar el cambio. La observación guardada no indica si el usuario espera la parte descartada o depende de la forma corta. Si se acepta la nueva regla, impleméntala en otro parche, actualiza la expectativa correspondiente con su motivo y añade casos para el comportamiento acordado. Conserva las reglas de espacios y fecha en ese parche.
Aquí resulta útil el análisis del impacto de cambios de código: la observación identifica una diferencia; los llamadores y sus pruebas ayudan a evaluar las consecuencias. Si un parche ya incluye pruebas, revisa las aserciones que cambian con él para comprobar si aún verifican el comportamiento previsto.
Usa las observaciones para guiar la siguiente lectura
Si todavía no encuentras el origen del valor formateado, empieza siguiendo un valor por el código desconocido. Una vez localizado el formateador, mantén juntos la implementación actual, los casos registrados y la refactorización propuesta.
Una pregunta opcional para la IA:
Explica cómo produce este formateador cada salida registrada. Señala el código que descarta campos, conserva espacios, aplica etiquetas alternativas y calcula la fecha. Sugiere una refactorización que conserve estas observaciones y enumera por separado cualquier cambio de comportamiento propuesto. No supongas que una salida guardada es un requisito.