Как проверить, что ИИ-агент в IDE работает, если на одинаковые запросы LLM отвечает по-разному? Ответы модели недетерминированы, а интерфейс и бизнес-логика вполне детерминированы, и их нужно тестировать отдельно.

Мы делаем ИИ-агента, встраиваемого в JetBrains IDE. В статье расскажу, как мы выстроили UI-автоматизацию плагина так, чтобы тесты ловили регрессии в интерфейсе, бизнес-логике и при этом не «моргали» из-за нестабильности LLM.

Статья пригодится, если вы QA-инженер или разработчик и вам интересны:

  1. Выстраивание UI-автоматизации на примере IDE-плагина

  2. Тестирование приложений с ИИ-функциями

  3. Разделение ответственности между детерминированной и недетерминированной частями системы

Подход к автоматизации

Veai — это встраиваемый в JetBrains IDE ИИ-агент для написания кода, отладки и тестирования.

В условиях ограниченных QA-ресурсов мы в первую очередь фокусируемся на высокоприоритетной функциональности. В то же время не забываем инвестировать и в автоматизацию тестирования. Как и большинство команд, мы сталкиваемся с проблемой фич, добавленных в последний момент. Поэтому поставили перед собой задачу улучшить Quality Gates.

В проекте уже были автотесты на разных уровнях: тесты на основе модели IDE, тесты для разных LLM-провайдеров, агентский бенчмарк. Наш продукт, в отличие от консольных ИИ-агентов, имеет пользовательский интерфейс, но классических UI-тестов в проекте не было. То есть пирамида тестирования у нас в целом присутствует, и UI-тесты были бы ее верхним уровнем, а не заменой остальным. Вывод: нужны UI-тесты.

Тестирование приложений с LLM-функциями условно делим на два направления:
тестирование классических функций, таких как хранение настроек пользователя,
и ИИ-функций, таких как получение ответа LLM-провайдера.

Отправляя агенту вопрос, можно получить как ответ в чате, так и вывод в терминале IDE. С точки зрения автоматизации тестирования это может привести к нестабильности тестов, но мы нашли решение — разделили тесты на несколько уровней:

Запуск

Когда

Что проверяет

LLM

Длительность

Smoke

Каждый PR

Базовая работоспособность с акцентом на UI

Без LLM

4-5 минут

Full

Ночной запуск

Все тесты (включая ожидание ответов LLM)

Реальный сервер

10-12 минут (параллельно для каждой IDE)

Benchmark (не UI-тесты)

Ночной запуск

Пользовательские сценарии и оценка результатов агента

Реальный сервер

около 5-6 часов

В Full-прогоне мы не мокируем ответы LLM, а работаем с реальным сервером и проверяем, что ответы приходят. Так мы сохраняем более полную цепочку подсистем плагина. Полная цепочка — это IDE-плагин, сервер лицензий, LLM-сервер, вспомогательные внутренние библиотеки и другие компоненты. Если один из серверных компонентов недоступен или не работает одна из вариаций сценария «предусловие — запрос пользователя — ответ агента», то мы узнаем об этом быстрее.

При запуске тестов на реальном LLM-сервере мы отправляем один и тот же короткий запрос: «2+2=? Ответь покороче». Это позволяет агенту быстрее перейти в состояние Ready. Мы не стремимся получить ответ «4», а проверяем, что ответ приходит после предусловий в тесте и интерфейс отображает этот ответ. Соответственно, нужно дожидаться завершения стриминга токенов LLM.

Качество агентских сценариев проверяем бенчмарком с использованием подхода LLM-as-a-Judge. Бенчмарк оставим за пределами этой статьи, так как коллеги планируют выпустить материал и по этой теме.

Техническое решение

Starter и Driver

Для автоматизации тестирования плагинов JetBrains IDE актуальное решение — это библиотеки Starter и Driver.

  1. Starter позволяет подготавливать IDE, настраивать тестовый проект и собирать итоговый вывод

  2. Driver позволяет взаимодействовать с интерфейсом и обращаться к внутреннему состоянию IDE через прокси (об этом ниже)

Решение позволяет описывать части интерфейса с помощью паттерна Page Object. Несмотря на то что мы работаем с десктопом, в решении можно описывать иерархию элементов по аналогии с Web и искать элементы с помощью XPath.

Информация о том, как искать элементы, представлена в документации.
Привожу пример Page Object:

fun Finder.chatPromptEditor(action: ChatPromptEditor.() -> Unit = {}): ChatPromptEditor {
    return x(xQuery { byClass("PromptEditorLocator") }, ChatPromptEditor::class.java).apply(action)
}

class ChatPromptEditor(data: ComponentData) : UiComponent(data) {
    val inputField = x { byAccessibleName("Editor") }
    val sendButton = x { byAccessibleName("Send") }

    fun sendRequest(text: String) {
        inputField.click()
        driver.ui.pasteText(text)
        sendButton.click()
    }
}

К нему можно обращаться в DSL-стиле за счет Type-safe builders.

chatPromptEditor {
    sendRequest("2+2=?")
}

Иногда в иерархии элементов нет подходящих локаторов для использования в Driver, поэтому в продуктовом коде приходится добавлять accessibleContext.accessibleName для поиска элементов по byAccessibleName. Где-то приходится менять правила обфускации для UI-элементов, чтобы можно было продолжать обращаться к ним по byClass.

Автотестам не требуется каждый раз начинать с Welcome-экрана, так как мы можем подготавливать состояние в виде XML-настроек плагина. Это дает возможность писать более быстрые тесты. Также Driver позволяет с помощью JMX (Java Management Extensions) писать прокси для внутренних компонентов запущенной IDE и получать информацию о ее состоянии. Это дает возможность делать более полные проверки.

В Driver встроена поддержка Allure, поэтому после небольшой настройки и завершения тестов появляется папка allure-results, из которой можно сгенерировать Allure Report.

Теперь рассмотрим пример Smoke-теста с использованием приведенного выше Page Object:

@Test
fun `WHEN user uses slash skill THEN skill badge appears in chat`() {
    Starter.newContext(
        CurrentTestMethod.qualifiedName(),
        Projects.simpleProject
    ).apply {
        configureEnvironment() // путь до собранного плагина, ускорение VM опциями
        configureEnterpriseKey(EnterpriseKey.DUMMY) // заглушка лицензии, чтобы не нагружать LLM
    }.runIdeWithDriver().useDriverAndCloseIde {
        setDefaultBehavior() // выключение онбординга
        ideFrame {
            step("Open Agent") {
                ideRightToolbar { openAgent() } // индексация IDE, открытие Агента
            }

            step("Use slash skill") {
                chatPromptEditor { // Page Object в DSL-стиле
                    sendRequest(SKILL_PROMPT) // отправка сообщения с командой SKILL
                }
            }

            step("Await used skill in chat messages") {
                chatMessages().awaitUsedSkillBadge() // ожидание UI-элемента, соответствующего SKILL

                assertInChatDump(driver, project) { // использование JMX для получения внутреннего состояния IDE (чата)
                    expect(USER_PROMPT_JSONPATH, SKILL_PROMPT) // поиск конкретного вызова SKILL
                }
            }
        }
    }
}

private companion object {
    const val USER_PROMPT_JSONPATH = "$.messages[0].prompt"
    const val SKILL_PROMPT = "/init " // на примере SKILL для создания AGENTS.md
}

Заметим, что в процессе выполнения этого теста с использованием EnterpriseKey.DUMMY на UI отобразится сетевая ошибка, но она не повлияет на стабильность теста. Важнее то, что запрос в этом сценарии не уйдет в LLM без необходимости.

Также заметим, что assertInChatDump — это наша утилита, использующая JMX для получения внутреннего состояния IDE в виде чата ИИ-агента и позволяющая делать проверки через JSONPath.

Continuous Integration

В нашем CI мы поддерживаем несколько комбинаций: версии IntelliJ, разные JetBrains IDE, feature flags и необходимость обфускации.

Обфускация интересна тем, что бывают ситуации, когда новая функция проверена разработчиком, но в итоговом артефакте после обфускации обнаруживается регрессия. Зачастую это связано с потоками данных в плагине и не касается UI. Решение: запускаем тесты на обфусцированном артефакте в ночной сборке. Только на ночной, потому что обфускация увеличивает время сборки.

В IntelliJ Platform регулярно выходят новые версии (2025.1/2025.2/2025.3/2026.1). Также требуется поддерживать IDE для разных языков (IDEA/PyCharm/Rider/WebStorm и других).

Отметим, что возможность запуска на разных вариантах IDE может принести пользу долгосрочно, потому что покрывать все варианты ручным тестированием крайне ресурсозатратно.

Ниже пример конфигурации CI для ручных запусков:

Отдельно стоит сказать про режим запуска (headless vs non-headless). Driver работает с реальным графическим интерфейсом вплоть до движения курсора. При локальном запуске тест может красть фокус окна, что мешает параллельной работе. В CI на Linux эта проблема решается с помощью виртуального дисплея — пакета Xvfb (X Virtual Framebuffer), который эмулирует графическую среду без монитора.

Если говорить о более частых для QA проблемах, то мы используем «ретраи» тестов с помощью Test Retry Gradle plugin. Сейчас каждый тест перезапускается не более одного раза, и на весь запуск может быть не больше трех падений. Это компромиссные значения, которые пересматривать не приходилось.

Результаты

Покрытие

Примеры функций плагина, на которые мы уже написали UI-тесты (несколько десятков тестов):

  • настройки плагина

  • переходы между состояниями чата

  • внутреннее состояние JSON чата (выбор агента и LLM)

  • вызов SKILL

  • навигация по истории чатов

  • вход нового пользователя

Багрепорты

Примеры найденных тестами проблем (за первые месяцы):

  • при добавлении в чате drag & drop файлов copy & paste ломались 🙂

  • при доработке прогресс-бара контекста учитывались не все LLM-провайдеры, что могло привести к некорректной отрисовке прогресс-бара и несвоевременному сжатию контекста чата

  • при доработке выбора агента из чата на UI учитывались не все LLM-провайдеры

  • при сценарии восстановления агента после ошибки начали «моргать» тесты, потому что и сам доработанный UI начал «моргать»

Была ситуация, когда для целей тестирования деплоилась новая конфигурация LLM-сервера, и при ночном запуске падали тесты. Это дало больше информации о нестабильности конкретной конфигурации.

Итоги

В этой статье мы рассмотрели один из способов обеспечения качества IDE ИИ-агента — UI-автоматизацию. Показали, как с помощью автотестов мы отвечаем на вопрос о работоспособности плагина после его доработок.

Мы видим, что даже небольшое число UI-автотестов находит ошибки. Если какие-то критичные баги пока еще не покрыты, то мы дописываем тесты и в следующий раз узнаем о проблемах быстрее.


Приглашаем попробовать Veai плагин. Обратную связь можно оставлять в Telegram-чате с командой разработки.

Материалы по автоматизации для плагинов