Ein Formatter verwandelt build:cache:hit in eine Notiz, die auf cache endet. Eine andere Eingabe lässt doppelte Leerzeichen in der Ausgabe. Es gibt keine Tests, die sagen, ob eines dieser Verhalten beabsichtigt ist. Bevor Sie KI um Vereinfachung bitten, brauchen Sie eine Möglichkeit, Änderungen dieser Ergebnisse zu erkennen.

Angenommen, dieser Formatter schreibt datierte Notizen aus label:detail-Zeichenfolgen. Laden Sie das vollständige Beispiel herunter und führen Sie es mit Node.js 22 oder neuer aus. Der dokumentierte Lauf nutzte 22.22.1 ohne Drittanbieter-Abhängigkeiten.

Einige Eingaben beobachten, bevor Sie eine bessere Implementierung vorschlagen

Entpacken Sie den Download, wechseln Sie nach characterization-tests-before-ai-refactoring und führen Sie aus:

node observe.mjs original

Der Befehl gibt zwölf Beobachtungen als JSON aus, jeweils mit Eingabe, vorgegebenem Zeitpunkt, vorgegebener Umgebung und Ausgabe. Beginnen Sie mit einer normalen Zeichenfolge und ändern Sie jeweils ein Detail. Hier sind zwei Einträge dieses Laufs, nur mit Eingabe und Ausgabe:

[
  {
    "text": "build:cache:hit",
    "output": "2026-10-13 | BUILD | cache"
  },
  {
    "text": " build : ready ",
    "output": "2026-10-13 | BUILD  |  ready"
  }
]

Die zweite Ausgabe enthält vor und nach dem letzten | je zwei Leerzeichen. Halten Sie diese bei der Untersuchung sichtbar. Weitere Beobachtungen zeigen, dass build und build: beide auf - enden. Ein leeres Label nutzt das vorgegebene NOTE_LABEL oder note, wenn der Wert fehlt oder leer ist.

Öffnen Sie nun 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] erklärt das verlorene hit: Spätere Felder werden nie verwendet. Das erste trim() wirkt auf die gesamte Eingabe und lässt daher das Leerzeichen nach build sowie vor ready stehen. Beim Rendern kommen eigene Trennzeichen-Leerzeichen daneben.

Eine Beobachtung gibt Ihnen etwas zu untersuchen, bevor Sie eine Korrektur wählen. Michael Feathers beschreibt in Unit Conversations einen ähnlichen Weg: bestehenden Code aufrufen, seine Antwort untersuchen und nützliche Interaktionen als Tests speichern.

Datum und Umgebung explizit machen

Eine gespeicherte Ausgabe mit heutigem Datum schlägt morgen fehl. Auch das NOTE_LABEL eines Entwicklers kann die Ausgabe eines leeren Labels verändern. Diese Werte gehören zu den Testeingaben.

Das Beispiel bietet bereits einen kleinen Austauschpunkt: das optionale Argument runtime. Standardmäßig liest es Uhrzeit und die benannte Prozessumgebungsvariable. Alle dokumentierten Beobachtungs- und Testbefehle verwenden stattdessen diese Implementierung aus runtime.mjs:

export function fixedRuntime({ at, env }) {
  return {
    now: () => new Date(at),
    readEnv: (name) => env[name],
  };
}

Die meisten Fälle nutzen 2026-10-13T12:00:00.000Z und { NOTE_LABEL: 'daily' }. Einer nutzt 2026-10-13T23:30:00-07:00; seine Ausgabe beginnt mit 2026-10-14, weil toISOString() UTC formatiert. Fehlende und leere Umgebungswerte haben eigene Fälle.

Ein solcher Austauschpunkt wird häufig Seam genannt. Fowlers Legacy Seam beschreibt das Abtrennen einer Abhängigkeit, damit Tests ihr Verhalten wählen können. In diesem Download ist der Parameter bereits vorhanden. Liegt die Abhängigkeit im realen Code tief im Inneren, führen Sie vor dem Umbau des Parsers den kleinsten passenden Seam als separate, geprüfte Änderung ein. Bewahren Sie dabei den bisherigen Wertebezug des Standardpfads.

Hier verändert kein Test die globale Uhr oder die Prozessumgebung. Die Prüfungen bestanden auch mit TZ=Pacific/Kiritimati und einem anderen NOTE_LABEL im Kindprozess. Das bestätigt, dass die übergebenen Eingaben diese Beobachtungen bestimmen. Das Zusammenspiel des Standardadapters mit der echten Anwendung benötigt weiterhin eine eigene Integrationsprüfung.

Beobachtetes festhalten, auch die fragwürdigen Teile

cases.mjs enthält die untersuchten Eingaben. Nach Ausführung und Prüfung der Originalergebnisse wurden sie in characterized.json gespeichert. Die Datei hält exakte Zeichenfolgen fest, einschließlich unerwarteter Leerzeichen und verworfener Felder.

Ein Charakterisierungstest prüft, ob sich das ausgewählte Verhalten geändert hat. Er beweist nicht, dass das aufgezeichnete Verhalten richtig ist. Das Beibehalten von cache in dieser Fixture lässt etwa offen, ob die Anwendung eigentlich cache:hit erhalten sollte.

run.mjs liest das gespeicherte JSON und vergleicht damit das Ergebnis jeder Implementierung:

for (const item of fixtures) {
  test(item.name, () => {
    const actual = versions[mode](item.text, fixedRuntime(item));
    assert.equal(actual, item.output);
  });
}

Der Runner ruft während der Assertion nie den ursprünglichen Formatter auf, um einen Erwartungswert zu erzeugen. Eine Änderung der Originalimplementierung kann die gespeicherte Antwort daher nicht unbemerkt neu definieren. Auch observe.mjs gibt nur aus und überschreibt die Fixture nicht.

Führen Sie das Original gegen diese Aufzeichnung aus:

node run.mjs original

Alle zwölf Prüfungen bestehen. Sie decken normale Eingaben, fehlende und leere Details, spätere Felder, innere Leerzeichen, leere Eingaben, vorhandene und fehlende Ersatzlabels sowie den UTC-Datumsfall ab. Dies sind ausgewählte Zeichenfolgeneingaben. Unicode-Großschreibung, ungültige Datumswerte, Nicht-String-Argumente, Reihenfolge von Seiteneffekten und reale Aufrufer wurden hier nicht charakterisiert.

Refactoring mit erhaltenen Ergebnissen

Die verhaltenstreue Version trennt Parsing und Rendering in benannte Funktionen. Ihr Parser behält die ursprüngliche Feldauswahl bewusst bei:

function parseFields(text) {
  const [label, detail] = text.trim().split(':');
  return { label, detail };
}

function render(day, label, detail) {
  return `${day} | ${label} | ${detail}`;
}

refactored.mjs nutzt diese Funktionen und behält Ersatzregeln, Großschreibung und UTC-Datumskonvertierung bei. Führen Sie aus:

node run.mjs refactored

Alle zwölf gespeicherten Beobachtungen bestehen, und die vollständige Beobachtungsausgabe stimmt Byte für Byte mit dem Original überein. Das stützt dieses Refactoring für die aufgezeichneten Eingaben, während ungetestete Fälle sichtbar offenbleiben.

Wenn Sie hier KI um Hilfe bitten, geben Sie ihr Quellcode und Charakterisierungsfälle. Bitten Sie um Erhalt der bestehenden Zeichenfolgen und Kennzeichnung jeder verhaltensändernden Anpassung. „Räume das auf“ lässt deutlich mehr Spielraum für eine unangekündigte Entscheidung über Leerzeichen oder Trennzeichen.

Eine verlockende Bereinigung sichtbar scheitern lassen

Die hypothetische Bereinigung in accidental.mjs nutzt einen Parser, der bequemer wirken kann:

function parseFields(text) {
  const [label, ...rest] = text
    .trim()
    .split(':')
    .map((field) => field.trim());
  return { label, detail: rest.join(':') };
}

Er kürzt jedes Feld und verbindet den Rest erneut. Führen Sie die drei Versionen getrennt aus:

node run.mjs original
node run.mjs refactored
node run.mjs accidental
Version Bestanden Fehlgeschlagen Exit-Code
original 12 0 0
refactored 12 0 0
accidental 10 2 1, erwarteter Fehler

Genau die beiden zuvor beobachteten Überraschungen schlagen fehl. build:cache:hit endet nun auf cache:hit, und die Eingabe mit Leerzeichen liefert jetzt 2026-10-13 | BUILD | ready. Keine der Änderungen wurde durch Neuerzeugen der Fixture versteckt.

Mit node observe.mjs accidental können Sie die neuen Ausgaben prüfen. Führen Sie Befehle einzeln aus, wenn Sie nach dem erwarteten Fehler fortfahren möchten. Eine Verknüpfung mit && stoppt bei Exit-Code 1. Der Standardbefehl node --test run.mjs prüft nur original.

Das rote Ergebnis bedeutet, dass die Bereinigung aufgezeichnetes Verhalten verändert hat. Ob eine neue Ausgabe vorzuziehen ist, ist die nächste Entscheidung.

Eine Verhaltensänderung getrennt entscheiden

DECISION.md enthält einen Vorschlag für den Fall zusätzlicher Doppelpunkte: cache:hit statt cache erhalten. Es dokumentiert alte und vorgeschlagene Ausgaben und hält den Vorschlag aus dem verhaltenstreuen Refactoring heraus.

Prüfen Sie in einer echten Anwendung repräsentative Eingaben und Nutzer dieser Zeichenfolge, bevor Sie die Änderung akzeptieren. Die gespeicherte Beobachtung verrät nicht, ob Benutzer den verworfenen Teil erwarten oder sich auf die Kurzform verlassen. Wird die neue Regel akzeptiert, setzen Sie sie in einem separaten Patch um, aktualisieren Sie die Erwartung mit Begründung und ergänzen Sie Fälle für das vereinbarte Verhalten. Lassen Sie Leerzeichen- und Datumsregeln in diesem Patch unverändert.

Hier hilft auch die Auswirkungsanalyse von Codeänderungen: Die Beobachtung zeigt einen Unterschied, Aufrufer und Tests helfen, seine Folgen zu bewerten. Enthält ein Patch bereits Tests, prüfen Sie die gleichzeitig geänderten Assertions, um zu sehen, ob sie das beabsichtigte Verhalten noch absichern.

Beobachtungen für die nächste Lektüre nutzen

Wenn Sie die Quelle des formatierten Werts noch nicht finden, beginnen Sie damit, einen Wert durch den unbekannten Code zu verfolgen. Sobald der Formatter gefunden ist, halten Sie aktuelle Implementierung, aufgezeichnete Fälle und vorgeschlagenes Refactoring zusammen.

Ein optionaler KI-Prompt:

Erkläre, wie dieser Formatter jede aufgezeichnete Ausgabe erzeugt. Zeige den Code für verworfene Felder, erhaltene Leerzeichen, Ersatzlabels und Datum. Schlage ein Refactoring vor, das diese Beobachtungen bewahrt, und liste Verhaltensänderungen getrennt auf. Leite aus einer gespeicherten Ausgabe keine Anforderung ab.