Um formatador transforma build:cache:hit em uma nota terminada em cache. Outra entrada deixa espaços duplicados na saída. Não há testes indicando se esses comportamentos são intencionais. Antes de pedir à IA para simplificá-lo, você precisa detectar se esses resultados mudarem.
Suponha que o formatador escreva notas datadas a partir de strings label:detail. Baixe o exemplo completo de testes de caracterização e execute com Node.js 22 ou posterior. A execução registrada usou 22.22.1, sem dependências externas.
Observe algumas entradas antes de propor uma implementação melhor
Descompacte o download, entre em characterization-tests-before-ai-refactoring e execute:
node observe.mjs original
O comando imprime doze observações em JSON, incluindo entrada, instante fornecido, ambiente fornecido e saída. Comece com uma string comum e mude um detalhe por vez. Estas são duas entradas da execução, mostrando apenas entrada e saída:
[
{
"text": "build:cache:hit",
"output": "2026-10-13 | BUILD | cache"
},
{
"text": " build : ready ",
"output": "2026-10-13 | BUILD | ready"
}
]
A segunda saída tem dois espaços antes e depois do último |. Mantenha-os visíveis durante a investigação. As outras observações mostram que build e build: terminam em -; um rótulo vazio usa o NOTE_LABEL fornecido ou note quando o valor falta ou está vazio.
Agora abra 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 a perda de hit: os campos seguintes nunca são usados. O primeiro trim() atua sobre a entrada inteira, preservando o espaço após build e antes de ready. A renderização adiciona seus próprios espaços junto aos separadores.
Uma observação oferece algo para investigar antes de escolher a correção. Michael Feathers descreve uma progressão semelhante — chamar código existente, examinar sua resposta e salvar interações úteis como testes — em Unit Conversations.
Torne a data e o ambiente explícitos
Uma saída salva com a data de hoje falha amanhã. O NOTE_LABEL de um desenvolvedor também pode mudar a saída de um rótulo vazio. Esses valores pertencem à entrada do teste.
O exemplo já fornece um pequeno ponto de substituição: o argumento opcional runtime. Seu padrão lê o relógio e a variável de ambiente nomeada. Todas as observações e comandos documentados fornecem, em vez disso, esta implementação de runtime.mjs:
export function fixedRuntime({ at, env }) {
return {
now: () => new Date(at),
readEnv: (name) => env[name],
};
}
A maioria dos casos usa 2026-10-13T12:00:00.000Z e { NOTE_LABEL: 'daily' }. Um usa 2026-10-13T23:30:00-07:00; sua saída começa com 2026-10-14, porque toISOString() formata em UTC. Valores de ambiente ausentes e vazios têm casos próprios.
Esse ponto de substituição costuma ser chamado de seam. Legacy Seam, de Fowler, discute separar uma dependência para que os testes escolham seu comportamento. O download já oferece esse parâmetro. Se a dependência estiver embutida no código real, introduza o menor ponto adequado em uma mudança separada e revisada antes de reorganizar o parser; preserve a obtenção dos valores pelo caminho padrão.
Aqui, nenhum teste altera o relógio global ou o ambiente do processo. As verificações também passaram com o processo filho em TZ=Pacific/Kiritimati e outro NOTE_LABEL, confirmando que as entradas fornecidas determinam as observações. A interação do adaptador padrão com uma aplicação real ainda exige sua própria verificação de integração.
Salve o que aconteceu, inclusive o que parece estranho
cases.mjs contém as entradas exploratórias. Depois de executar e examinar os resultados originais, nós os salvamos em characterized.json. Ele registra strings exatas, incluindo espaços inesperados e campos descartados.
Um teste de caracterização pergunta se o comportamento selecionado mudou. Não prova que o comportamento registrado está correto. Por exemplo, preservar cache nesta fixture deixa em aberto se a aplicação deveria manter cache:hit.
run.mjs lê o JSON salvo e compara o resultado de cada implementação:
for (const item of fixtures) {
test(item.name, () => {
const actual = versions[mode](item.text, fixedRuntime(item));
assert.equal(actual, item.output);
});
}
O executor nunca chama o formatador original para gerar o resultado esperado durante a asserção. Assim, mudar o original não redefine silenciosamente a resposta salva. observe.mjs apenas imprime; também não sobrescreve a fixture.
Execute o original contra esse registro:
node run.mjs original
As doze verificações passam. Cobrem entradas comuns, detalhes ausentes e vazios, campos posteriores, espaços internos, entradas em branco, rótulos alternativos fornecidos ou ausentes e a data UTC. São entradas string selecionadas. Capitalização Unicode, datas inválidas, argumentos de outros tipos, ordem de efeitos colaterais e chamadores reais não foram caracterizados aqui.
Refatore mantendo os resultados salvos
A versão que preserva o comportamento extrai análise e renderização para funções nomeadas. Seu parser mantém deliberadamente a escolha 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 essas funções preservando regras alternativas, conversão para maiúsculas e data UTC. Execute:
node run.mjs refactored
As doze observações salvas passam, e a saída completa de observação é idêntica à original, byte a byte. Isso sustenta a refatoração para as entradas registradas, sem ocultar os casos não testados.
Se pedir ajuda à IA nesta etapa, forneça o código e os casos de caracterização. Peça para preservar as strings existentes e identificar mudanças que alterariam comportamento. Pedir apenas “limpe este código” deixa espaço para decisões não anunciadas sobre espaços ou delimitadores.
Deixe uma limpeza atraente falhar de forma visível
A limpeza hipotética em accidental.mjs usa um parser que pode parecer mais conveniente:
function parseFields(text) {
const [label, ...rest] = text
.trim()
.split(':')
.map((field) => field.trim());
return { label, detail: rest.join(':') };
}
Ele remove espaços de cada campo e reúne os restantes. Execute as três versões separadamente:
node run.mjs original
node run.mjs refactored
node run.mjs accidental
| Versão | Passaram | Falharam | Código de saída |
|---|---|---|---|
original |
12 | 0 | 0 |
refactored |
12 | 0 | 0 |
accidental |
10 | 2 | 1, falha esperada |
Os casos que falham são exatamente as duas surpresas anteriores. build:cache:hit agora termina em cache:hit, e a entrada com espaços produz 2026-10-13 | BUILD | ready. Nenhuma mudança foi ocultada regenerando a fixture.
Use node observe.mjs accidental para examinar essas saídas. Execute comandos separadamente para continuar após a falha esperada; uni-los com && interrompe no código 1. O padrão node --test run.mjs verifica apenas original.
O resultado vermelho indica que a limpeza mudou o comportamento registrado. Decidir se alguma saída nova é preferível é o próximo passo.
Decida uma mudança de comportamento separadamente
DECISION.md propõe mudar o caso de dois-pontos extras: manter cache:hit em vez de cache. Registra as saídas antiga e proposta e deixa a proposta fora da refatoração que preserva comportamento.
Em uma aplicação real, examine entradas representativas e consumidores da string antes de aceitar a mudança. A observação salva não informa se o usuário espera a parte descartada ou depende da forma curta. Se a regra for aceita, implemente-a em um patch separado, atualize a expectativa com sua justificativa e adicione casos para o comportamento acordado. Preserve as regras de espaços e datas nesse patch.
Aqui também ajuda a análise de impacto de mudanças: a observação identifica uma diferença; chamadores e testes ajudam a avaliar suas consequências. Quando o patch já inclui testes, revise as asserções alteradas junto dele para saber se ainda conferem o comportamento pretendido.
Use as observações para orientar a próxima leitura
Se ainda não encontra a origem do valor formatado, comece seguindo um valor por uma base de código desconhecida. Ao localizar o formatador, mantenha juntos a implementação atual, os casos registrados e a refatoração proposta.
Uma pergunta opcional à IA:
Explique como este formatador produz cada saída registrada. Aponte o código dos campos descartados, espaços preservados, rótulos alternativos e data. Sugira uma refatoração que preserve as observações e liste separadamente mudanças de comportamento propostas. Não deduza que uma saída salva é um requisito.