Рефакторинг звучит как безопасная работа: меняем форму, поведение оставляем. Из этого определения вырастает опасное следствие - раз поведение не меняется, проверять вроде бы нечего. Именно здесь и прячется главная ловушка: "поведение сохранено" - это утверждение, а всякое утверждение либо измерено, либо остаётся гипотезой в удобной формулировке.
Наивный ход - довериться намерению. Мы собирались только переставить и переименовать, значит снаружи всё как было; агент аккуратен, diff выглядит механическим. Эта уверенность растёт из того, что рефакторинг ощущается как перекладывание, а не как изменение. Но модель не знает, что вы "собирались только переставить" - она видит код и вольна тронуть больше, чем вы держали в голове.
Ломается это на границе между внутренней структурой и публичным контрактом. Пока не выписано, что именно считается наблюдаемым поведением, разделить "поменяли только форму" и "заодно поменяли смысл" нечем. Поэтому первый шаг - перечислить публичные контракты: сигнатуры, сериализуемые формы данных, ошибки и исключения, побочные эффекты и чувствительные к производительности пути. Это и есть тот периметр, который рефакторинг обязан оставить неподвижным.
Второй шаг - сделать сохранность измеримой до того, как начнётся перекладывание. Инструмент для этого - characterization tests: тесты, фиксирующие текущее поведение как есть, включая его странности, ещё до изменения структуры. Они отвечают не на вопрос "как должно быть", а на вопрос "как есть сейчас", и именно поэтому служат эталоном: если после рефакторинга они всё ещё зелёные, наблюдаемое поведение на покрытом периметре не сдвинулось.
Само изменение ведут малыми компилируемыми шагами, а не одним большим прыжком. Каждый шаг оставляет код собранным и тесты зелёными, так что регрессия локализуется тем шагом, на котором появилась, а не ищется во всём diff разом. Крупный монолитный рефактор лишает вас этой локализации: когда сломалось всё сразу, непонятно, какое из десяти изменений виновато, и цена разбора превышает цену самой перестройки.
Отдельно стоит жёстко запретить работу "заодно". Обновить версии зависимостей, переформатировать весь репозиторий, подчистить соседние файлы - каждое из этих действий по отдельности безобидно, но подмешанное в рефакторинг, оно уничтожает главное его свойство: возможность утверждать, что поведение не менялось. Полезный приём - потребовать отдельный отчёт о намеренных изменениях поведения; при честном рефакторинге он пуст, а пустой отчёт легко сверить с diff и тестами. Такой отчёт полезен и как дисциплина мышления: чтобы честно написать "намеренных изменений поведения нет", агент вынужден сам просмотреть свой diff на предмет незамеченного - и часто именно тут всплывает то самое "заодно".
Цена пропущенного измерения проявляется не сразу и потому обманчива. Рефакторинг без характеризации часто "проходит": код компилируется, беглый просмотр не находит проблем, задачу закрывают. Сдвиг в поведении всплывает позже - на редком пути, при определённых данных, у конкретного потребителя API - и к тому моменту он уже не связан в голове ни с кем с той перестройкой. Отложенный дефект дороже немедленного ровно на стоимость его позднего обнаружения.
Это не значит, что любое переименование требует полного протокола. Локальная правка внутри одной функции, не пересекающая публичного контракта, проверяется существующими тестами и чтением diff. Протокол с характеризацией окупается там, где рефакторинг трогает контракт, охватывает несколько модулей или готовит почву под миграцию: чем шире периметр и чем больше у кода внешних потребителей, тем дороже обойтись без измеримого эталона.
Проверять сохранность поведения нужно тем же периметром, который выписали в начале. Зелёный suite сам по себе слаб: если тесты не покрывают перечисленные контракты, формулировка "поведение сохранено" остаётся гипотезой, как бы уверенно ни горел зелёный. Честная проверка звучит так: для каждого пункта из списка контрактов есть тест, который его стережёт, и до рефакторинга этот тест уже проходил на старом коде. Тогда зелёный после - действительно доказательство, а не совпадение. Иначе suite лишь подтверждает, что код по-прежнему делает то, что тесты и так никогда не проверяли.
Типичные провалы группируются вокруг пропущенного измерения. Нет списка контрактов - непонятно, что вообще охранялось. Нет характеризации - "сохранено" держится на слове. Разрешённое "заодно" - и уже не отличить перестройку от тихого изменения смысла. Слабый suite - зелёный горит, а контракт не покрыт. Признак у всех один: сохранность поведения утверждают, но не могут показать тест, который её измеряет. Сделайте её измеримой заранее - и рефакторинг перестанет быть ставкой.