tsma — линия обороны от регрессий в legacy-коде

Как рефакторить код без тестов?

Вам достался 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)24646.7%
DONE (best-effort)28153.3%
TODO (не обработано)00%

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
Gogo/astgo testgo test -coverprofile
TypeScriptregexnpx vitest / npx jestc8 / istanbul
Pythonregexpytestcoverage.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