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% branch coverage не означает 100% корректности. tsma проверяет, что все ветви выполняются, а не что все утверждения осмысленны – покрытие это нижняя планка качества, а не потолок.

Почему некоторые функции не достигают 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
  Исходный файл изменился → нужно перемерить

tsma nexttsma status) при каждом запуске заново сканирует исходники и пересогласовывает множество функций – вновь добавленные и извлечённые функции всплывают как TODO, удалённые выпадают, а уже достигнутый прогресс сохраняется. Поэтому рефакторинг, добавляющий функции, не может оставить “All functions complete!” ложным. Чтобы навязать только синхронизацию, не трогая прогресс, используйте tsma rescan (tsma reset --all стирает сессию целиком). Чтобы увидеть все функции вместе с их статусами, используйте tsma list.


Инструкция для 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.modgo/astgo testgo test -coverprofile
TypeScriptpackage.jsonregexnpx vitest / npx jestc8 / istanbul
Pythonpyproject.toml и др.regexpytestcoverage.py
RustCargo.tomlregexcargo testcargo llvm-cov
Javapom.xml / build.gradleregexmvn / gradle test (учёт модулей)JaCoCo
C#*.csproj / *.slnregexdotnet test --filtercoverlet (Cobertura)

Go использует AST-парсер для точного извлечения функций, остальные – на основе регулярных выражений. Сопоставление тестов следует конвенциям – Go handler.gohandler_test.go, TS .test.ts/.spec.ts, Python – префикс test_, Rust #[cfg(test)] mod tests или tests/*.rs, Java FooTest.java, C# FooTests.cs.

Сгенерированные файлы (*_gen.go, *.pb.go) и пути, исключаемые по умолчанию (vendor/, .git/, .tsma/, node_modules/), автоматически выпадают из индексирования. Если нужны собственные исключения, поместите в корне проекта файл .tsmignore (синтаксис как у .gitignore).


Установка и запуск

go install github.com/park-jun-woo/tsma/cmd/tsma@latest
# или как скилл Claude Code:
npx skills add park-jun-woo/tsma
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-06-18: учтена версия v0.5.0 – поддержка шести языков (добавлены Rust, Java, C#), установка через go install/npx skills add, команды tsma rescan/list/reset, собственные исключения через .tsmignore, добавлено предупреждение «покрытие ≠ корректность»
  • 2026-05-14: Первая версия