7 760 статей
arXiv:2611.03197 87 2 нояб. 2026 г. FREE

Триада контекста (Symptom + Cause + Support): автоматическая починка сломанных юнит-тестов после обновления кода

КЛЮЧЕВАЯ СУТЬ
Когда код проекта меняется, старые юнит-тесты падают с ложными тревогами. Метод Триады контекста позволяет автоматически обновлять устаревшие тесты через LLM и кодинг-агентов без потери смысла проверок. Фишка: подавай модели сразу три слоя — симптом, причину и связующий код. Так нейросеть видит всю цепочку при совместной эволюции кода и тестов — чинит логику теста и не удаляет проверки assert ради галочки.
Адаптировать под запрос
⚡

TL;DR

Триада контекста и самопроверка для починки тестов — это методика сборки промпта и конвейера для LLM и кодинг-агентов, которая автоматически обновляет упавшие юнит-тесты вслед за изменениями в основном коде проекта (source–test co-evolution, совместная эволюция кода и тестов). Вместо того чтобы скармливать модели только лог ошибки или только git diff, подход объединяет три слоя контекста — симптом падения, изменённый код на уровне целых методов или файлов и неизменённый связующий код — с коротким циклом самопроверки по результатам компиляции и запуска.

Когда разработчик меняет бизнес-логику, старые тесты начинают падать с ложными тревогами — не из-за бага в продукте, а потому что сам тест устарел. Если просто кинуть в чат с LLM стектрейс ошибки или кусок диффа, модель галлюцинирует сигнатуры, чинит не ту строчку или просто удаляет проверки (assert), чтобы тест формально стал «зелёным». Причина в том, что в реальных проектах тест редко ломается от правки одной функции: обычно цепочка вызовов проходит через несколько изменённых методов и вспомогательных классов, которые не видны напрямую из текста теста.

Метод решает эту проблему за счёт подачи в промпт полной причинно-следственной цепочки из трёх блоков: Симптом (лог выполнения и стектрейс упавшего теста), Причина (изменения в тестируемом коде на уровне методов или файлов, а не узких кусков диффа) и Опора (неизменённые методы-посредники, которые связывают тест с местом поломки). Дополнительно запуск одного раунда итеративной доработки с обратной связью от компилятора и тест-раннера и передача этой структуры кодинг-агенту радикально повышают долю успешно починенных сложных тестов при меньших затратах токенов.


🔬

Схема метода

ШАГ 1: Сборка контекста в один пакет → [Упавший тест] + [Symptom: лог падения] + [Cause: дифф и полные методы/файлы] + [Support: связующие неизменённые классы]
ШАГ 2: Первичная генерация патча с рассуждением по 4 этапам → [Диагностика симптома → Связка с диффом → Локализация правок в тесте → Патч]
ШАГ 3: Итеративная самопроверка (1 раунд с запуском теста) → [Лог новой ошибки компиляции/проверки → Исправленный тест]
ШАГ 4: Проверка сохранения смысла (LLM-судья / инструкция) → [Контроль, что assert не удалены и проверяют ту же логику]

Шаги 1 и 2 выполняются в одном запросе (или задаются как правило в инструкции кодинг-агенту вроде Codex / Claude Code). Шаг 3 требует запуска теста и второго запроса с логом результата, если первая версия не прошла.


🚀

Пример применения

Задача: В финтех-сервисе оплаты подписок (наподобие биллинга в Яндекс Плюсе или Т-Банке) бэкенд-разработчик переписал отправку метрик списания: теперь StubPaymentEmitter.emit() сохраняет ссылку на оригинальный объект события paymentEvent, а не его копию paymentEvent.copy(), чтобы не плодить объекты в памяти. Из-за этого старый юнит-тест testSubscriptionChargeMetrics(), который вызывал emit() до заполнения transactionId("tx-900"), начал падать с ComparisonFailure: expected:<[tx-900]> but was:<[]>. Нужно починить тест через LLM или агента, не сломав проверку.

Промпт:


Обнови устаревший юнит-тест, который начал падать после изменений в основном коде (Source-Test Co-evolution).
Цель — сделать так, чтобы тест снова компилировался и проходил на новой версии кода, полностью сохранив исходный смысл проверок (test intent). Запрещено удалять или ослаблять assert-проверки ради формального прохождения.



@Test
public void testSubscriptionChargeMetrics() {
    final StubPaymentEmitter emitter = new StubPaymentEmitter("billing", "ru-central");
    ChargeMetrics metrics = new ChargeMetrics();
    metrics.charge(order);
    metrics.reportChargeTime(0).emit(emitter);
    metrics.sqlOrderId("tx-900");
    metrics.transactionId("tx-900");
    Map actualEvent = emitter.getEvents().get(0).toMap();
    Assert.assertEquals("tx-900", actualEvent.get(BillingMetrics.TX_ID));
}



[ERROR] ru.fintech.billing.ChargeMetricsTest.testSubscriptionChargeMetrics -- Time elapsed: 0.003 s <<< FAILURE!
org.junit.ComparisonFailure: expected:<[tx-900]> but was:<[]>
    at org.junit.Assert.assertEquals(Assert.java:117)
    at ru.fintech.billing.ChargeMetricsTest.testSubscriptionChargeMetrics(ChargeMetricsTest.java:88)



Изменения в тестируемом коде (полные затронутые методы):
В классе StubPaymentEmitter extends PaymentEmitter:
public void emit(Event event) {
    if (event instanceof ServiceMetricEvent) {
        ServiceMetricEvent metricEvent = (ServiceMetricEvent) event;
        metricEvents.computeIfAbsent(metricEvent.getMetric(), name -> new ConcurrentLinkedDeque<>())
-           .add(metricEvent.copy());
+           .add(metricEvent);
    }
}
(Плюс в коммите изменены ещё 17 методов форматирования метрик в ChargeMetrics, не влияющие на ID транзакции).



Связующий неизменённый код (как вызов из теста доходит до изменённого класса):
В классе PaymentEmitter:
public void emit(ServiceMetricEvent.Builder builder) {
    emit(builder.build(service, host));
}
В классе ChargeMetrics:
public PaymentEmitter reportChargeTime(long time) {
    // собирает builder из текущих полей metrics на момент вызова
}



Перед написанием кода выполни 4 шага:
1. По  определи точную строку падения и какое значение оказалось пустым или неверным.
2. Сопоставь  и : отследи непрямой (транзитивный) вызов от теста через связующий метод к изменённому классу и отсей изменения в коде, которые не влияют на падение.
3. Определи, какие строки в  нужно переставить, обновить или дополнить, чтобы учесть новое поведение без удаления assert.
4. Выдай исправленный код теста.

Результат: Модель по шагам отследит, что вызов reportChargeTime(0).emit(emitter) на строке 6 теста через связующий метод PaymentEmitter.emit() триггерит изменённый StubPaymentEmitter, причём раньше событие формировалось иначе, а теперь вызывается до установки sqlOrderId("tx-900") и transactionId("tx-900"). В ответе модель перенесёт вызов metrics.reportChargeTime(0).emit(emitter) ниже строк с установкой "tx-900", оставив Assert.assertEquals("tx-900", ...) нетронутым.


🧠

Почему это работает

Главная слабость LLM при поддержке тестов — слепота к непрямым зависимостям. В живых проектах большинство сломанных тестов затрагивают сразу несколько изменённых методов и классов, а настоящая причина падения часто прячется в транзитивном вызове (метод А в тесте вызывает неизменённый метод Б, а тот передаёт управление в изменённый класс В). Когда модели дают только стектрейс (Symptom) или только узкие строки git diff (Hunk-level Cause), она не видит мостика между ними и начинает наугад менять аргументы вызова в самом тесте.

Сильная сторона современных моделей и кодинг-агентов — умение сопоставлять цепочки вызовов и отсеивать шум, когда все звенья цепи лежат перед глазами. Если дать модели изменения на уровне целых методов или файлов (а не обрезанных кусков диффа) и добавить связующий неизменённый код (Support), она восстанавливает логику диспетчеризации и точно отличает те части коммита, которые реально сломали тест, от десятка попутных правок.

Второй важный механизм — экономия на рассуждениях и итерациях. Режим глубокого размышления (reasoning mode) помогает модели связать симптом с причиной, но без структуры она тратит массу токенов на лишние ветки. Если явно задать четырёхэтапную схему действий и добавить ровно один раунд самопроверки с логом компиляции/выполнения после первой попытки, модель снимает основную долю ошибок без бесконечных циклов и без дорогого перебора.

Рычаги управления: - Масштаб контекста причины (Cause): для простых правок сигнатуры достаточно уровня метода (Method-level), но если в коммите менялись поля классов, конструкторы или наследование — подавай уровень файла (File-level). - Количество итераций самопроверки: максимальную отдачу даёт первый раунд обратной связи по логу запуска (r=1). Второй и третий раунды дают убывающую отдачу, поэтому при ограничении бюджета останавливайся после первой доработки. - Защита от «читерства» модели: всегда фиксируй запрет на удаление assert или проверяй патч отдельным запросом-судьёй, иначе модель может починить тест путём удаления проверки.


📋

Шаблон промпта


Ты — инженер по автоматизации тестирования. Твоя задача — починить упавший юнит-тест после обновления основного кода проекта (Source-Test Co-evolution).



{код_упавшего_теста}



  
  {лог_ошибки_и_стектрейс}
  

  
  {дифф_и_полный_код_изменённых_методов_или_файлов}
  

  
  {неизменённые_методы_посредники_и_классы_связывающие_тест_с_изменениями}
  



Выполни диагностику строго по 4 этапам перед генерацией патча:
1. Анализ симптома: зафиксируй строку падения в тесте и тип ошибки (ошибка компиляции или сбой assert/runtime).
2. Трассировка причины: свяжи вызовы внутри  через  с изменениями в . Отфильтруй изменения из коммита, которые не влияют на этот тест.
3. План адаптации теста: определи минимальные изменения в подготовке данных, порядке вызовов или ожидаемых значениях теста.
4. Проверка целостности (Oracle Check): убедись, что исходное намерение теста сохранено, а проверки (assertions) не удалены и не заменены на пустышки.



Сначала кратко выпиши выводы по 4 этапам , затем выдай готовый исправленный код метода теста.

Что подставлять в плейсхолдеры: - {код_упавшего_теста} — полный текст тестового метода (и setUp/@BeforeEach, если он есть в классе). - {лог_ошибки_и_стектрейс} — вывод тест-раннера (Maven/Gradle/pytest/vitest) с текстом ошибки и номерами строк. - {дифф_и_полный_код_изменённых_методов_или_файлов} — изменения в основном коде из коммита, развёрнутые до полных методов или файлов. - {неизменённые_методы_посредники_и_классы_связывающие_тест_с_изменениями} — определения промежуточных методов/интерфейсов, которые вызываются из теста и передают управление в изменённый код.

🚀 Быстрый старт — вставь в чат или в инструкцию кодинг-агенту (CLAUDE.md / AGENTS.md):

Вот шаблон починки упавших тестов после изменений в коде (Триада контекста Symptom + Cause + Support). Адаптируй его под мою задачу: [опиши свой стек и какой тест упал после рефакторинга].
Задавай вопросы или сам собери нужные файлы через поиск по репозиторию, чтобы заполнить поля.

[вставить шаблон выше]

LLM (или кодинг-агент) запросит лог падения теста, сам найдёт затронутые в коммите методы и промежуточные классы-посредники, после чего предложит патч без ослабления assert.


⚠️

Ограничения

⚠️ Деградация на гигантских коммитах: Когда один коммит затрагивает десятки и сотни файлов одновременно, а сам тест требует многострочной перестройки в нескольких местах, точность одиночных запросов к LLM заметно падает — для таких случаев нужен автономный кодинг-агент с доступом к репозиторию и запуску тестов.

⚠️ Риск ложного «зелёного» теста (ослабление оракула): Без явного контроля качества модель иногда добивается прохождения теста за счёт удаления падающего assert или подстановки тривиальной проверки. Авторы специально отсекали такие патчи через LLM-судью с высоким уровнем рассуждений.

⚠️ Слепота одного только лога ошибки: Подача только лога падения (Symptom) без кода изменений решает лишь малую часть задач — модель лечит симптом вслепую, не понимая новой логики приложения.


🔍

Как исследовали

Исследователи сначала изучили более 2 500 реальных случаев совместного обновления кода и тестов в популярных открытых Java-проектах за период с мая 2024 по апрель 2026 года (чтобы исключить утечку ответов в обучающие выборки современных моделей) и обнаружили, что старые бенчмарки оторваны от жизни: в реальности 70,9% сломанных тестов вызывают сразу несколько изменённых методов, 77,8% сопровождаются правками вызываемых классов, а 38,4% требуют правок теста сразу в нескольких местах. Из динамически проверенных коммитов (где старый тест проходил на старом коде, падал на новом, а патч разработчика снова проходил) команда отобрала эталонный бенчмарк на 500 задач из 15 крупных проектов (включая Apache Druid, ShardingSphere, IoTDB, Spring AI, Quarkus, Netty, Dubbo) и прогнала на нём три современные модели — GPT-5.4, DeepSeek-V3.2 и Qwen3-Coder-480B-Instruct, а также автономного агента Codex.

Эксперименты показали наглядную картину: если дать моделям только лог падения (Symptom), тесты проходят лишь в 17,6%–31,4% случаев (88 из 500 у DeepSeek-V3.2, 115 у Qwen3-Coder и 157 у GPT-5.4). Зато расширение контекста причины (Cause) с узких кусков диффа до полных методов и файлов резко поднимает успешность (например, у DeepSeek-V3.2 прохождение тестов вырастает со 162 (32,4%) на уровне кусков диффа до 262 (52,4%) на уровне файлов, а у GPT-5.4 — до 276 (55,2%) уже на уровне методов), а добавление связующего контекста (Support) и первого раунда самопроверки с обратной связью даёт дополнительный прирост. Разложив цепочки рассуждений DeepSeek-V3.2 на 14 типовых действий в 4 этапах, авторы смогли направить модели без дорогого режима размышлений по нужной траектории, а передача найденных принципов сборки контекста в кодинг-агент Codex повысила успешность починки самых сложных случаев (где обычные промпты пасовали) на 66,7%.


💡

Адаптации и экстраполяции

💡 Адаптация для инструкции кодинг-агенту (CLAUDE.md / AGENTS.md): Вместо ручного копирования файлов пропиши правило сборки «Триады контекста» прямо в системную инструкцию своего агента (Claude Code, Cursor, Codex).

## Правило починки упавших тестов (Source-Test Co-evolution)
Если после изменений в коде упал существующий тест:
1. Не правь тест сразу по тексту ошибки (Symptom).
2. Собери три слоя контекста:
   - Symptom: полный стектрейс и строка падения теста.
   - Cause: git diff И полный текст всех изменённых методов/классов, которые вызываются прямо или косвенно.
   - Support: неизменённые методы-диспетчеры и родительские классы, связывающие вызов в тесте с изменённым кодом.
3. Сделай максимум 1–2 итерации правки с прогоном тест-раннера.
4. Запрещено удалять, комментировать или ослаблять assert-проверки ради прохождения теста.

🔧 Техника: Проверка патча через промпт LLM-судьи → защита от удаления assert

Если автоматизируешь починку тестов в CI/CD конвейере, добавь после генерации патча короткий проверочный промпт (как сделали авторы исследования с помощью GPT-5.4):

Сравни исходный тест T и исправленный тест T' с учётом изменений в коде V -> V'.
Проверь: не прошёл ли тест T' только потому, что из него удалили проверки (assertions), загрубили ожидаемые значения или пропустили вызов проверяемого метода? Ответь PASS только если T' полностью сохраняет проверочный смысл исходного теста.

🔗

Ресурсы

  • Исследование: Finding the Pivots to Repair Broken Source–Test Evolution Using LLMs (arXiv:2611.03197v1, ноябрь 2026)
  • Авторы: Xiao Chen, Songqiang Chen, Jialun Cao, Shing-Chi Cheung (The Hong Kong University of Science and Technology), Yun Peng (Fudan University), Chun Yong Chong (Monash University Malaysia)

📋 Дайджест исследования

Ключевая суть

Когда код проекта меняется, старые юнит-тесты падают с ложными тревогами. Метод Триады контекста позволяет автоматически обновлять устаревшие тесты через LLM и кодинг-агентов без потери смысла проверок. Фишка: подавай модели сразу три слоя — симптом, причину и связующий код. Так нейросеть видит всю цепочку при совместной эволюции кода и тестов — чинит логику теста и не удаляет проверки assert ради галочки.

Принцип работы

Одного лога ошибки или куска git diff мало. Тест редко вызывает сломанную функцию напрямую — между ними стоят посредники. Собери три звена: лог падения (Symptom), полные изменённые методы (Cause) и неизменённый мостик (Support). Это как дать электрику не только сгоревшую лампочку, но и всю схему проводки в стене. Модель отслеживает транзитивный вызов через посредников и бьёт точно в цель.

Почему работает

При виде одного стектрейса модель слепнет к непрямым связям. Она начинает гадать или просто стирает падающий assert. Полный код затронутых методов и классов-посредников убирает слепые зоны. Модель отсеивает лишний шум коммита (например, 17 попутных правок в файле) и находит реальную поломку. Один раунд доработки по логу компиляции (r=1) снимает львиную долю оставшихся ошибок. Дальше гонять цикл почти нет смысла — отдача падает.

Когда применять

Разработка и поддержка кода → конкретно для починки упавших юнит-тестов после рефакторинга или изменения бизнес-логики, особенно когда вызов из теста идёт через цепочку промежуточных классов. НЕ подходит для гигантских коммитов на сотни файлов в один промпт — там нужен автономный кодинг-агент с доступом к репозиторию.

Мини-рецепт

1. Собери Symptom: возьми текст упавшего теста и полный стектрейс ошибки из тест-раннера.
2. Разверни Cause: вместо узких строчек git diff дай полный код изменённых методов (или целых файлов, если менялись поля и конструкторы).
3. Добавь Support: приложи неизменённые методы-посредники, связывающие тест с местом правки.
4. Задай 4 шага и защиту: потребуй пройти путь <этапы>симптом → трассировка связей → план правки → проверка сохранения assert и дай 1 попытку исправиться по логу компиляции.

Примеры

[ПЛОХО] : Вот стектрейс ComparisonFailure и кусок git diff. Почини тест testSubscriptionChargeMetrics, чтобы он проходил.
[ХОРОШО] : Обнови тест после правок кода. Запрещено удалять assert. Вот 3 блока: стектрейс падения на строке 88, полный код изменённого метода StubPaymentEmitter.emit(), неизменённые методы PaymentEmitter.emit и reportChargeTime. Пройди 4 шага: найди пустое значение, отследи вызов через посредника, переставь строки подготовки данных в тесте и выдай код.
Источник: Finding the Pivots to Repair Broken Source-Test Evolution Using LLMs
ArXiv ID: 2611.03197 | Сгенерировано: 2026-11-09 01:06

Проблемы LLM

ПроблемаСутьКак обойти
Модель ломает или удаляет проверки при починке упавшего тестаПосле обновления основного кода старый юнит-тест падает. Ты даёшь модели только лог ошибки или узкий кусок git diff. Модель не видит непрямой цепочки вызовов через промежуточные классы. В итоге она выдумывает несуществующие сигнатуры или просто удаляет проверку assert, чтобы тест формально стал зелёнымПодавай в промпт триаду контекста: лог падения (), изменённый код на уровне целых методов или файлов () и неизменённые связующие классы (). Явно запрети удалять или ослаблять assert

Методы

МетодСуть
Триада контекста (Симптом + Причина + Опора) для починки тестовСобери в промпт упавший тест () и три блока: (стектрейс и ошибка), (дифф, развёрнутый до полных методов или файлов) и (неизменённые методы-посредники между тестом и местом правки). Задай 4 шага рассуждений в : 1) найти строку и тип сбоя; 2) связать тест через посредников с диффом и отсеять лишние правки коммита; 3) спланировать минимальную правку данных или порядка вызовов; 4) проверить, что assert сохранены (Oracle Check). Если первый патч не прошёл — сделай 1 раунд доработки с новым логом компиляции. Почему работает: Связующий код восстанавливает скрытую цепочку вызовов, а полные методы позволяют отличить причину поломки от попутного шума в коммите. Когда применять: обновление упавших юнит-тестов после рефакторинга или смены бизнес-логики. Когда не работает: гигантские коммиты на десятки файлов (там нужен автономный агент с поиском по репозиторию)

Тезисы

ТезисКомментарий
Узкого куска диффа недостаточно для починки тестов с непрямыми вызовамиТесты редко ломаются от прямой правки одной функции. Чаще вызов из теста идёт через неизменённый метод-посредник в изменённый класс. Без кода посредников и полных тел затронутых методов модель лечит симптом вслепую и меняет аргументы наугад. Применяй: при починке тестов всегда разворачивай git diff до полных методов (или файлов, если менялись поля и наследование) и прикладывай промежуточные вызываемые методы
Один раунд обратной связи от компилятора снимает основную долю ошибокВ починке тестов повторные итерации с логом запуска дают быстро убывающую отдачу. Первый раунд самопроверки (r=1) исправляет большинство промахов с типами и импортами, а второй и третий раунды почти не добавляют точности, но жгут токены. Применяй: ограничивай автоматический цикл доработки теста ровно одной итерацией по логу тест-раннера
📖 Простыми словами

Finding the Pivots to Repair Broken Source-Test Evolution UsingLLMs

arXiv: 2611.03197

Когда разработчик меняет логику в основном коде, тесты начинают сыпаться как карточный домик, и это вечная головная боль. Проблема в том, что современные AI-кодеры часто тупят при починке таких поломок, потому что смотрят на мир через замочную скважину. Они видят либо только текст ошибки, либо крошечный кусок измененного кода, но не понимают, как эти части связаны между собой. Метод триады контекста решает это фундаментально: он дает модели не просто «что сломалось», а полную карту связей между симптомом, причиной и всей цепочкой вызовов, которая их соединяет.

Это как если бы ты пришел к врачу с болью в колене, а он вместо того, чтобы просто заклеить пластырем место, где болит, изучил бы всю твою походку и старую травму спины. Формально пластырь — это тоже лечение, но колено болит из-за кривого позвоночника. Обычные LLM пытаются «лечить пластырем» — они просто подгоняют значения в тесте под ответ, чтобы он прошел, вместо того чтобы понять, как изменение в глубине системы докатилось до поверхности. Триада контекста заставляет модель видеть всю биомеханику кода.

В основе лежат три конкретных слоя: Symptom (лог ошибки), Cause (измененные методы целиком, а не куцые строчки из git diff) и Pivot — те самые неизменные куски кода, которые связывают тест с местом поломки. Если в биллинге вы поменяли способ передачи объекта события, тест может упасть в самом неожиданном месте. Метод находит этот «мостик» и скармливает его модели вместе с циклом самопроверки. Если после правки код не компилируется или тест всё еще падает, агент не сдается, а делает работу над ошибками, пока пазл не сойдется.

Хотя метод обкатывали на юнит-тестах, принцип совместной эволюции универсален для любой сложной системы. Это работает для документации, которая устаревает после каждого коммита, для API-контрактов и даже для интеграционных сценариев. Главный инсайт здесь в том, что контекст решает всё: нельзя чинить деталь, не понимая, в какой механизм она вставлена. Мы переходим от тупого исправления строк к осознанному сопровождению архитектуры силами нейронок.

Короче: хватит кормить LLM обрывками логов и ждать магии — это путь к костылям и багам. Нужно давать модели связующий контекст и право на ошибку в закрытом цикле проверки. 10 из 10 тестов чинятся быстрее, если модель видит не только «где болит», но и «почему пошла цепная реакция». Кто внедрит такой конвейер в свой CI/CD первым, тот перестанет тратить половину спринта на рутинную починку тестов и наконец займется нормальными фичами.

Работа с исследованием

Адаптируйте исследование под ваши задачи или создайте готовый промпт на основе техник из исследования.

0 / 2000
~0.5-2 N-токенов ~10-30с
~0.3-1 N-токенов ~5-15с