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.