Серия: агент против легаси. Рефакторинг под защитой тестов.

Пятый пост серии. После тестов и болезненной миграции — рефакторинг: 130 тестов как страховка, правило простое — исправление красит ровно один известный баг из красного в зелёный, не больше.

История первая: грепни, прежде чем бояться. Три репозитория с самого начала объявлены с неверным generic-типом поля ID для сущностей — исправление звучало рискованно: а вдруг где-то в проекте полагаются на старый тип?

Один grep по всему src/main показал: унаследованные CRUD-методы этих репозиториев вызываются ровно один раз во всём проекте, и не у тех трёх. Правка — три файла, три строки, риск для прода — нулевой, доказанный, а не предположенный.

Побочный эффект интереснее самого фикса: тест на этот баг пришлось не переписать, а удалить — вызов с неверным типом после исправления просто перестаёт компилироваться. Формально покрытие уменьшилось. Фактически безопасность выросла: ошибка переехала из рантайма в компилятор. Метрика coverage этой разницы не видит вообще.

История вторая: тест, который сопротивлялся собственному исправлению. Баг с осиротевшей строкой в корзине (см. прошлый пост) чинился двумя строками кода — маппинг уже семь лет как объявлял orphanRemoval = true, просто сервис в обход этого дёргал не ту сторону связи.

Но оригинальный тест 2019 года на удаление проверял не результат, а факт вызова метода — Mockito.verify().deleteById(). Он был зелёным все годы, пока жил баг, и покраснел именно в момент, когда баг исправили. На моках он и не мог поймать проблему: отмена удаления происходила на flush, а persistence context в юнит-тесте попросту отсутствует. Такой тест вреден вдвойне: молчит, пока баг живёт, и кричит, когда его чинят — то есть активно сопротивляется улучшению, которое должен был бы приветствовать.

История третья: порядок исправления — тоже решение. Утечка BCrypt-хэша через GET /profile и безусловный перехэш в update() вместе давали потерю доступа к аккаунту при обычном round-trip. Напрашивающийся порядок — сначала закрыть утечку, она ведь «security».

Но именно этот порядок уничтожил бы ответ на главный вопрос: какой из двух багов на самом деле нёс риск. Починили сначала перехэш — и потеря доступа исчезла, а утечка осталась и ещё какое-то время документировалась отдельным зелёным тестом на известную проблему. Только так стало видно: несущий дефект — перехэш, утечка лишь делала ловушку удобной для попадания. Если бы оба фикса выкатили одним релизом, при разборе будущего инцидента команда так и не узнала бы, какая причина реально работала.

Общий вывод трёх историй. Рефакторинг легаси — не про смелость исправлять, а про то, чтобы заранее знать цену вопроса: grep вместо страха, наблюдаемый результат вместо факта вызова в тестах, разделение сцепленных фиксов вместо одновременного релиза. Ни один из трёх приёмов не специфичен для AI-агента — но именно агент, работающий по чек-листу и без спешки, применяет их последовательно там, где человек под дедлайном обычно срезает угол.

Сейчас в фоне гоняется архитектурный аудит текущего состояния кода — про него в следующем посте.