
Как рефакторить код без тестов?
Вам достался legacy-проект на 100 тысяч строк. Тестов нет. Хочется рефакторить, но тронешь — и непонятно, что сломается. Чтобы написать тесты, нужно понять код, а чтобы понять код, нужна документация — которой тоже нет.
Никто не трогает. Код гниёт дальше.
Все legacy-системы мира находятся в этом тупике. Компании из Fortune 500 тратят 60–80% IT-бюджета на поддержку legacy. 42% времени разработчиков уходит на обслуживание технического долга.
А что если LLM может писать тесты за вас?
Проблемы, когда тесты пишет LLM
Если попросить LLM «напиши тест для этой функции», что-то появится. Но есть три проблемы.
Первая: непонятно, с чего начинать. Когда функций 527, идти по порядку с первой? Начать с самой важной? Критерия нет.
Вторая: качество тестов невозможно проверить. LLM написал тест, тест прошёл. Но действительно ли он проверяет поведение функции — или это пустышка без единого assert, которая просто вызывает функцию? Чтобы понять, нужно читать каждый тест глазами.
Третья: без обратной связи LLM останавливается на 60–70%. Одной фразы «напиши тест для этой функции» недостаточно для 100% branch coverage. Нужно указать, какие ветви пропущены, — тогда LLM дополнит остальное.
Дело не в том, что LLM не умеет писать тесты. Проблема в отсутствии структуры, которая говорит LLM, что именно тестировать и насколько хорошо он это сделал.
tsma: тестовый рельс, запускаемый одной командой
tsma — это CLI-инструмент, который индексирует все функции проекта, определяет наличие тестов, измеряет coverage и даёт LLM-агенту точную обратную связь.
Агенту нужно знать одну команду:
$ tsma next
Эта единственная команда управляет всем циклом:
$ tsma next # показывает следующую функцию без теста
→ пишем тест
$ tsma next # обнаруживает новый тест, запускает, измеряет coverage
→ 100%? PASS, переходим к следующей
→ <100%? показывает непокрытые ветви с номерами строк
$ tsma next # перемеряет исправленный тест
→ улучшился или нет — помечает DONE и переходит дальше
Повторять, пока не появится «All functions complete!».
Проверено на 527 функциях
tsma был применён к реальному Go-проекту (527 функций).
| Результат | Кол-во | Доля |
|---|---|---|
| PASS (100% branch coverage) | 246 | 46.7% |
| DONE (best-effort) | 281 | 53.3% |
| TODO (не обработано) | 0 | 0% |
246 функций достигли 100% branch coverage. Остальные 281 — не достигли, но были протестированы настолько, насколько это возможно.
Почему некоторые функции не достигают 100%?
Функции, которые достигают 100%, и те, которые нет
Достижимость 100% branch coverage зависит от того, как функция получает зависимости.
Интерфейс (mockable) — 100% достижимо:
type Handler struct {
svc AuthSvc // interface — можно подменить mock'ом
}
В тесте mock позволяет контролировать все пути:
svc := mocks.NewMockAuthSvc(ctrl)
svc.EXPECT().Login(...).Return(result, nil) // успешный путь
svc.EXPECT().Login(...).Return(nil, err) // путь ошибки
Конкретный тип (not mockable) — 100% невозможно:
type Handler struct {
svc *service.SMSImportService // указатель на struct — не подменить
}
Реальная реализация тянет за собой БД, внешние API и другие зависимости. Нельзя вызвать конкретную ошибку или вернуть конкретный результат. Ветви, зависящие от таких результатов, недостижимы в unit-тестах.
Подход tsma: после фидбэка о непокрытых ветвях даётся ещё одна попытка. Если ветвь по-прежнему недостижима — функция помечается DONE. Это не ограничение инструмента, а отражение тестируемости кода. Введение интерфейсов (DI) сделает 100% возможным, но это уже изменение исходного кода.
Обратная связь кардинально меняет тесты LLM
Главная ценность tsma — не индексирование и не измерение coverage. Это точное указание непокрытых ветвей с номерами строк.
Без обратной связи:
"Напиши тест для функции ListContracts"
→ LLM тестирует только happy path
→ coverage 60–70%
С обратной связью:
"Напиши тест для функции ListContracts"
→ coverage 65% (11/17)
→ UNCOVERED:
line 41 — if params.Status != nil
line 44 — if params.BuildingId != nil
line 70 — if err != nil (CountSummary)
→ LLM добавляет тесты точно для этих ветвей
→ coverage 100%
Тот же самый LLM. Разница — только в наличии обратной связи. Три строки с номерами строк разделяют 60% и 100%.
Агент упал — прогресс сохранён
LLM-агенты падают. Лимит токенов, сетевая ошибка, обрыв сессии. 527 функций за одну сессию не обработать.
tsma сохраняет прогресс в .tsma/session.json.
$ tsma status
527 functions
PASS: 246 (46.7%)
DONE: 281 (53.3%)
TODO: 0 (0.0%)
Агент упал на 200-й функции? Новый агент запускает tsma next и продолжает с 201-й. session.json — это чекпоинт.
Несколько агентов могут работать по очереди без конфликтов. Атомарность — на уровне функции.
Сессия — это кэш, а исходный код — истина
Один из принципов tsma: session — это кэш, source of truth — исходный код.
Если удалить тестовый файл, функция вернётся в TODO, даже если в session.json записан PASS. Сессия не расходится с реальностью.
Принцип:
session.json говорит "PASS"
но тестового файла нет → TODO
исходный файл изменился → нужно перемерить
Инструкция для LLM-агента
Агенту нужна инструкция из 6 строк:
1. Выполни tsma next
2. TODO — прочитай функцию и напиши тест
3. Тест упал — прочитай ошибку и исправь тест
4. Показаны непокрытые ветви — добавь тесты для них
5. PASS/DONE — следующая функция появится автоматически
6. Повторяй, пока не появится "All functions complete!"
Агенту нужна одна команда — tsma next. Остальное задаёт CLI.
Поезд и рельсы
Vibe coding — это поезд. Быстрый. Но без рельсов сходит с пути.
Все AI-инструменты для разработки сосредоточены на том, чтобы сделать поезд быстрее. Более мощная модель, более умный агент, лучший промпт. Но чем быстрее поезд, тем страшнее крушение.
tsma — это рельсы. LLM генерирует тесты (Neural), CLI определяет границы (Symbolic Constraint). Креативность LLM остаётся нетронутой, но качество результата обеспечивается машинно.
| Раньше | tsma | |
|---|---|---|
| Написание тестов | Человек (медленно) или LLM (хаотично) | LLM пишет, CLI проверяет |
| С чего начать? | Решает человек | CLI определяет порядок |
| Проверка качества | Человек ревьюит | CLI измеряет coverage |
| Обратная связь | Нет | Номера строк непокрытых ветвей |
| Отслеживание прогресса | Нет | session.json автоматически |
LLM генерирует свободно. Но только по рельсам tsma next.
Поддержка языков
| Язык | Индексер | Тест-раннер | Coverage |
|---|---|---|---|
| Go | go/ast | go test | go test -coverprofile |
| TypeScript | regex | npx vitest / npx jest | c8 / istanbul |
| Python | regex | pytest | coverage.py |
Go использует AST-парсер для точного извлечения функций. TypeScript и Python — на основе регулярных выражений.
Сгенерированные файлы (*_gen.go, *.pb.go), тестовые файлы, vendor/node_modules автоматически исключаются из индексирования.
Установка и запуск
make install
cd your-legacy-project
tsma next
Это всё.
MIT License. github.com/park-jun-woo/tsma