Форматировщик превращает build:cache:hit в заметку, заканчивающуюся на cache. Другой ввод оставляет двойные пробелы в результате. В коде нет тестов, объясняющих, задумано ли такое поведение. Прежде чем просить ИИ упростить его, нужен способ заметить изменение результатов.
Допустим, форматировщик создаёт датированные заметки из строк label:detail. Скачайте полный пример и запустите с Node.js 22 или новее. Зафиксированный запуск использовал 22.22.1 без сторонних зависимостей.
Изучите несколько входов до предложения лучшей реализации
Распакуйте архив, перейдите в characterization-tests-before-ai-refactoring и выполните:
node observe.mjs original
Команда выводит двенадцать наблюдений в JSON: ввод, заданное время, заданное окружение и результат. Начните с обычной строки, затем меняйте по одной детали. Вот две записи запуска, только с вводом и выводом:
[
{
"text": "build:cache:hit",
"output": "2026-10-13 | BUILD | cache"
},
{
"text": " build : ready ",
"output": "2026-10-13 | BUILD | ready"
}
]
Во втором результате по два пробела до и после последнего |. Не упускайте их при исследовании. Другие наблюдения показывают, что build и build: заканчиваются на -; пустая метка использует заданный NOTE_LABEL или note, если значение отсутствует либо пусто.
Теперь откройте 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] объясняет потерю hit: последующие поля не используются. Первый trim() применяется ко всей строке и оставляет пробел после build и перед ready. Формирование результата добавляет рядом свои пробелы разделителя.
Наблюдение даёт предмет исследования до выбора исправления. В Unit Conversations Майкл Физерс описывает похожую последовательность: вызвать существующий код, изучить ответ и сохранить полезные взаимодействия в тестах.
Сделайте дату и окружение явными
Сохранённый результат с сегодняшней датой завтра не совпадёт. NOTE_LABEL разработчика тоже может менять результат для пустой метки. Эти значения должны быть входами теста.
В примере уже есть небольшая точка подмены — необязательный аргумент runtime. По умолчанию он читает часы и указанную переменную окружения процесса. Все описанные команды наблюдения и тестов вместо этого передают реализацию из runtime.mjs:
export function fixedRuntime({ at, env }) {
return {
now: () => new Date(at),
readEnv: (name) => env[name],
};
}
Большинство случаев используют 2026-10-13T12:00:00.000Z и { NOTE_LABEL: 'daily' }. Один использует 2026-10-13T23:30:00-07:00; результат начинается с 2026-10-14, потому что toISOString() форматирует в UTC. Для отсутствующих и пустых значений окружения есть отдельные случаи.
Такую точку подмены часто называют seam. В Legacy Seam Фаулер описывает отделение зависимости, чтобы тесты могли выбирать её поведение. В архиве параметр уже существует. Если зависимость спрятана внутри реального кода, до перестройки парсера добавьте минимальную точку подмены отдельной проверенной правкой, сохранив способ получения значений по умолчанию.
Здесь ни один тест не меняет глобальные часы или окружение процесса. Проверки также прошли с TZ=Pacific/Kiritimati и другим NOTE_LABEL в дочернем процессе, подтверждая, что наблюдения определяются переданными входами. Взаимодействие адаптера по умолчанию с реальным приложением всё ещё требует отдельной интеграционной проверки.
Сохраните наблюдения, включая сомнительные части
cases.mjs содержит исследуемые входы. После запуска и изучения исходных результатов мы сохранили их в characterized.json. Там записаны точные строки, включая неожиданные пробелы и отброшенные поля.
Характеризационный тест проверяет, изменилось ли выбранное поведение. Он не доказывает правильность записанного поведения. Например, сохранение cache в фикстуре оставляет открытым вопрос, должно ли приложение на самом деле сохранять cache:hit.
run.mjs читает сохранённый JSON и сравнивает с ним результат каждой реализации:
for (const item of fixtures) {
test(item.name, () => {
const actual = versions[mode](item.text, fixedRuntime(item));
assert.equal(actual, item.output);
});
}
Во время проверки runner никогда не вызывает исходный форматировщик для создания ожидаемого результата. Поэтому изменение исходной реализации не может незаметно переопределить сохранённый ответ. observe.mjs тоже лишь печатает и не перезаписывает фикстуру.
Проверьте исходную версию по этой записи:
node run.mjs original
Все двенадцать проверок проходят. Они охватывают обычный ввод, отсутствующие и пустые детали, последующие поля, внутренние пробелы, пустой ввод, заданные и отсутствующие резервные метки и дату UTC. Это выбранные строковые входы. Регистр Unicode, неверные даты, нестроковые аргументы, порядок побочных эффектов и реальные вызовы приложения здесь не исследованы.
Рефакторинг с сохранением записанных результатов
Версия с сохранением поведения выделяет разбор и формирование результата в именованные функции. Парсер намеренно сохраняет исходный выбор полей:
function parseFields(text) {
const [label, detail] = text.trim().split(':');
return { label, detail };
}
function render(day, label, detail) {
return `${day} | ${label} | ${detail}`;
}
refactored.mjs использует эти функции, сохраняя резервные правила, перевод в верхний регистр и преобразование даты в UTC. Выполните:
node run.mjs refactored
Все двенадцать сохранённых наблюдений проходят, а полный вывод побайтно совпадает с исходным. Это подтверждает рефакторинг для записанных входов, оставляя непроверенные случаи явно открытыми.
Если просите ИИ помочь на этом этапе, передайте исходники и характеризационные случаи. Попросите сохранить строки и отметить любые изменения поведения. Запрос «почисти код» оставляет намного больше места для необъявленных решений о пробелах и разделителях.
Сделайте сбой привлекательной очистки заметным
Гипотетическая очистка в accidental.mjs использует парсер, который может показаться удобнее:
function parseFields(text) {
const [label, ...rest] = text
.trim()
.split(':')
.map((field) => field.trim());
return { label, detail: rest.join(':') };
}
Он обрезает пробелы каждого поля и объединяет оставшиеся. Запустите три версии отдельно:
node run.mjs original
node run.mjs refactored
node run.mjs accidental
| Версия | Успешно | Сбой | Код выхода |
|---|---|---|---|
original |
12 | 0 | 0 |
refactored |
12 | 0 | 0 |
accidental |
10 | 2 | 1, ожидаемый сбой |
Падают именно два ранее замеченных неожиданных случая. build:cache:hit теперь заканчивается на cache:hit, а ввод с пробелами даёт 2026-10-13 | BUILD | ready. Ни одно изменение не скрыто перегенерацией фикстуры.
Используйте node observe.mjs accidental для просмотра новых результатов. Запускайте команды отдельно, если хотите продолжить после ожидаемого сбоя; объединение через && остановится на коде 1. По умолчанию node --test run.mjs проверяет только original.
Красный результат означает изменение записанного поведения. Предпочтительнее ли новый результат — следующий отдельный вопрос.
Отдельно решите вопрос изменения поведения
DECISION.md предлагает для дополнительного двоеточия сохранять cache:hit вместо cache. Он фиксирует старый и предлагаемый вывод и отделяет предложение от рефакторинга с сохранением поведения.
В реальном приложении перед принятием изменения изучите типичные входы и потребителей строки. Сохранённое наблюдение не говорит, ожидает ли пользователь отброшенную часть или полагается на короткую форму. Если правило принято, реализуйте его отдельным патчем, обновите ожидание с обоснованием и добавьте случаи согласованного поведения. Правила пробелов и даты в этом патче не меняйте.
Здесь полезен анализ влияния изменений кода: наблюдение выявляет различие, а вызывающий код и тесты помогают оценить последствия. Если патч уже содержит тесты, проверьте изменённые вместе с ним утверждения, чтобы понять, проверяют ли они нужное поведение.
Используйте наблюдения для следующего чтения
Если источник форматированного значения ещё не найден, начните с отслеживания одного значения в незнакомом коде. Найдя форматировщик, держите рядом текущую реализацию, записанные случаи и предлагаемый рефакторинг.
Необязательный запрос ИИ:
Объясни, как форматировщик получает каждый записанный результат. Укажи код отброшенных полей, сохранённых пробелов, резервных меток и даты. Предложи рефакторинг с сохранением наблюдений, а изменения поведения перечисли отдельно. Не считай сохранённый результат требованием.