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) | 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. Это не ограничение инструмента, а отражение тестируемости кода. Дилемма 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 |
|---|---|---|---|
| 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
Источники
- 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: Первая версия