tsma – линия обороны от регрессий в legacy-коде Image: AI generated

Если вы хотите рефакторить legacy-код с помощью ИИ, но тестов нет, если ваш LLM пишет тесты, но останавливается на полпути, если вы хотите механически отслеживать покрытие, контролируя агента — tsma строит эту линию обороны.

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

Вам достался legacy-проект на 100 тысяч строк. Тестов нет. Хочется рефакторить, но тронешь – и непонятно, что сломается. Чтобы написать тесты, нужно понять код, а чтобы понять код, нужна документация – которой тоже нет.

Никто не трогает. Код гниёт дальше.

Все legacy-системы мира находятся в этом тупике. Компании из Fortune 500 тратят 60-80% IT-бюджета на поддержку legacy. 42% времени разработчиков уходит на обслуживание технического долга.

А что если LLM может писать тесты за вас?


Проблемы, когда тесты пишет LLM

Если попросить LLM “напиши тест для этой функции”, что-то появится. Но есть три проблемы.

Первая: непонятно, с чего начинать. Когда функций 527, идти по порядку с первой? Начать с самой важной? Критерия нет.

Вторая: качество тестов невозможно проверить. LLM написал тест, тест прошёл. Но действительно ли он проверяет поведение функции – или это пустышка без единого assert, которая просто вызывает функцию? Чтобы понять, нужно читать каждый тест глазами.

Третья: без обратной связи LLM останавливается на 60-70%. Согласно эмпирическому исследованию Schafer et al. (2023), медиана тестов, сгенерированных LLM, составляет 70.2% statement coverage и 52.8% branch coverage. Одной фразы “напиши тест для этой функции” недостаточно для 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. Это не ограничение инструмента, а отражение тестируемости кода. Дилемма legacy-кода, систематизированная Feathers (2004), – “чтобы изменить код, нужны тесты; чтобы добавить тесты, нужно изменить код” – решается разрывом зависимостей и введением интерфейсов (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%. CoverUp (Pizzorno & Berger, 2024) эмпирически подтвердил тот же принцип. Многократно вставляя результаты анализа покрытия в промпты и фокусируя внимание LLM на непокрытых строках, они достигли медианного line coverage на уровне модуля 81% – на 19pp выше, чем baseline без обратной связи.


Агент упал – прогресс сохранён

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


Источники

  • Schafer, M., Nadi, S., Eghbali, A., & Tip, F. (2023). An Empirical Evaluation of Using Large Language Models for Automated Unit Test Generation. IEEE Transactions on Software Engineering, 50(1), 85–105. arXiv:2302.06527
  • Pizzorno, J. A., & Berger, E. D. (2024). CoverUp: Coverage-Guided LLM-Based Test Generation. arXiv preprint arXiv:2403.16218. arXiv:2403.16218
  • Ryan, G., Jain, S., Shang, M., Wang, S., Ma, X., Ramanathan, M. K., & Ray, B. (2024). Code-Aware Prompting: A Study of Coverage-Guided Test Generation in Regression Setting using LLM. Proceedings of the ACM on Software Engineering (FSE 2024), 1(FSE), 951–971. ACM DL
  • Lemieux, C., Inala, J. P., Lahiri, S. K., & Sen, S. (2023). CodaMOSA: Escaping Coverage Plateaus in Test Generation with Pre-trained Large Language Models. ICSE 2023, 951–963. ACM DL
  • Feathers, M. C. (2004). Working Effectively with Legacy Code. Prentice Hall. ACM DL
  • Besker, T., Martini, A., & Bosch, J. (2018). Technical Debt Cripples Software Developer Productivity. TechDebt 2018, 105–114. ACM DL
  • Stripe. (2018). The Developer Coefficient. PDF
  • U.S. Government Accountability Office. (2019). Information Technology: Agencies Need to Develop Modernization Plans for Critical Legacy Systems. GAO-19-471. GAO
  • Tornhill, A., & Borg, M. (2022). Code Red: The Business Impact of Code Quality. TechDebt 2022, 11–20. arXiv:2203.04374
  • Peng, S., Kalliamvakou, E., Cihon, P., & Demirer, M. (2023). The Impact of AI on Developer Productivity: Evidence from GitHub Copilot. arXiv:2302.06590

Связанное: Ratchet Pattern – Как заставить агента довести дело до конца – Паттерн, стоящий за tsma. Почему механическая верификация лучше суждений LLM.

Связанное: IQ модели менее важен, чем топология обратной связи – Почему структура обратной связи определяет результат больше, чем производительность модели.

История изменений

  • 2026-05-14: Первая версия