Документация Тест-планы: расширенные возможности

Тест-планы: расширенные возможности

Дополнение к Тест-планам. Здесь описаны функции
возможности уровня исполнения:

  • типизированные условия зависимостей (on_success / on_fail /
    always / on_skipped);
  • пер-айтем ретраи, таймауты, флаги critical и continueOnError;
  • стабильные ключи зависимостей itemUid;
  • живой вид запуска (Graph / Timeline / Items / Logs / Artifacts /
    Report);
  • шесть форматов отчётов и сравнение запусков;
  • опциональный путь распределённого раннера для айтемов плана;
  • бэкенд поиска сущностей, на котором работают все пикеры в UI.

О URL в примерах: используется http://localhost:5770 как адрес
Mockarty по умолчанию. Замените на адрес вашей установки. См.
Полезные функции и советы.

Связанные страницы: Тест-планы ·
Рецепты API тест-планов ·
Тест-планы в CI/CD ·
Поиск сущностей ·
Руководство по SDK

Режимы исполнения

У каждого плана есть поле executionMode — одно из трёх значений.

Режим Когда использовать Как работает
fifo Легаси-последовательные планы; по одному шагу за раз. Айтемы идут в порядке возрастания order; падение обрывает остальные (если не переопределено).
parallel Независимые айтемы, которые можно запускать одновременно. Все айтемы диспатчатся параллельно; рёбра зависимостей не учитываются.
dag Типизированные зависимости с условиями (режим по умолчанию для новых). Айтемы идут в порядке зависимостей; условие решает, когда айтем становится готов к запуску.

Если executionMode не задан, сервер автоматически выбирает dag при
наличии хотя бы одного айтема с типизированными gates, иначе fifo.
Ради обратной совместимости по-прежнему принимается прежнее значение
schedule (parallel / dag).

Айтемы и зависимости

ItemUID — стабильный ключ зависимости

Каждый айтем плана несёт itemUid (UUID). Он генерируется на сервере
при первом сохранении и остаётся стабильным при правках. Рёбра
зависимостей указывают на itemUid, а не на UUID внешней сущности
(refId), поэтому:

  • одну и ту же коллекцию / конфиг можно добавить дважды (например,
    smoke + regression + cleanup) без неоднозначности;
  • подмена подлежащей сущности (правка refId) не ломает граф плана;
  • переименование или удаление сущности влияет только на этот айтем, не
    на соседей.

Легаси-планы, созданные до появления itemUid, продолжают работать:
слой исполнения на этапе построения DAG откатывается на refId, а UI
дозаполняет UID при следующем сохранении.

Условия зависимостей (gates)

Каждая зависимость — это пара { itemUid, condition }. Условие
определяет, когда зависимый айтем становится готовым к запуску.

Gate Зависимый запускается, если предок … Типичное применение
on_success завершился со статусом passed. Основной happy-path. По умолчанию.
on_fail завершился со статусом failed. Ветки cleanup / rollback.
always достиг любого терминального статуса (passed/failed/skipped/cancelled). Teardown / очистка ресурсов.
on_skipped был пропущен (например, его собственный gate не сработал). Ветки восстановления.

Пустое / отсутствующее condition трактуется как on_success —
безопасный исторический дефолт. Когда gate не сработал, зависимый
айтем помечается как skipped с skipReason: gate_not_met.

Пример — три айтема, два из которых делят шаг очистки:

{
  "name": "API regression",
  "executionMode": "dag",
  "items": [
    {
      "itemUid": "11111111-1111-1111-1111-111111111111",
      "type": "functional",
      "refId": "aaaa...",
      "order": 1,
      "name": "Auth smoke"
    },
    {
      "itemUid": "22222222-2222-2222-2222-222222222222",
      "type": "fuzz",
      "refId": "bbbb...",
      "order": 2,
      "name": "Auth fuzz",
      "gates": [
        { "itemUid": "11111111-1111-1111-1111-111111111111", "condition": "on_success" }
      ]
    },
    {
      "itemUid": "33333333-3333-3333-3333-333333333333",
      "type": "functional",
      "refId": "cccc...",
      "order": 3,
      "name": "Teardown",
      "gates": [
        { "itemUid": "11111111-1111-1111-1111-111111111111", "condition": "always" },
        { "itemUid": "22222222-2222-2222-2222-222222222222", "condition": "always" }
      ]
    }
  ]
}

Шаг teardown выполнится независимо от того, прошёл smoke или fuzz,
упал или был пропущен.

Легаси dependsOn

Планы, сохранённые до редизайна, использовали dependsOn: [UUID, ...]
в каждом айтеме. Сервер по-прежнему читает это поле: каждый элемент
раскрывается в типизированное ребро { itemUid, condition: on_success }
на рантайме. Новому коду стоит писать типизированный gates[] — он
строго более выразителен.

Политика ретраев

У любого айтема может быть объект retry, управляющий поведением при
падении.

"retry": {
  "maxAttempts": 3,
  "backoffMs": 5000,
  "onlyOn": ["timeout", "connection reset"]
}
Поле Диапазон Значение
maxAttempts 1–10 Всего попыток, включая первую (т.е. 3 = исходная + до 2 ретраев). 1 отключает ретраи.
backoffMs 0–300000 мс Базовая линейная задержка. Реальная пауза = backoffMs × номерПопытки. 0 — мгновенный повтор.
onlyOn массив строк Совпадение подстроки без учёта регистра в тексте ошибки. Пусто — ретраим на любую ошибку.

Каждая повторная попытка получает свежий per-attempt контекст: timeoutMs
действует на попытку, а не на весь айтем. На успехе оставшиеся
попытки пропускаются. При отмене контекста (run cancel или
остановка шедулера) текущая попытка прерывается, а новые не
стартуют.

Таймаут на айтем

timeoutMs ограничивает каждую попытку через context.WithTimeout.
Когда deadline наступает:

  • попытка помечается как failed с error = "timeout";
  • в состоянии айтема выставляется timeoutHit: true;
  • если остались ретраи и onlyOn разрешает — следующая попытка
    стартует после заданной паузы.

Допустимый диапазон: 0–14 400 000 мс (4 часа). 0 означает «нет
переопределения per-item дедлайна» (применится общий дефолт
оркестратора). Более длительные задачи стоит разбивать на отдельные
запланированные прогоны.

Критические айтемы

Пометьте айтем critical: true, когда его падение должно останавливать
весь прогон. Семантика:

  • Если айтем завершился со статусом failed, оркестратор отменяет
    все ещё не начатые и работающие айтемы.
  • Отменённые соседи получают status: cancelled,
    cancelCause: dependency_critical_failure.
  • Сам прогон завершается со статусом failed.

Это не то же самое, что просто зависеть от айтема: соседи, которые уже
успели завершиться, остаются нетронутыми — каскадится только ещё не
сделанная работа.

Используйте critical для предусловий (setup, auth, миграция), когда
запускать остальную часть плана после их падения бессмысленно или
небезопасно.

ContinueOnError

continueOnError: true работает наоборот: «даже если этот айтем
упал — downstream gate on_success должны вести себя так, будто он
прошёл». Статус прогона всё равно учитывает падение в счётчике
failedItems, но последующие работы не пропускаются, а выполняются.

Полезно для нестабильных айтемов, чьё падение вы хотите зафиксировать,
но не принудительно блокировать.

Условия on_fail и on_skipped не затрагиваются — они по-прежнему
читают реальный статус.

Живой вид запуска

Страница Run Detail (/ui/test-plans/<id>/runs/<runId>) открывает
шесть вкладок:

Вкладка Что видно
Overview Конверт запуска (статус, старт/финиш, длительность, инициатор), итоги по айтемам, чип статуса.
Graph DAG на Cytoscape с живой покраской статусов. Клик по ноде открывает боковую панель.
Timeline Gantt: строки — айтемы, ось X — стеночное время. Зависимости — пунктирные линии. Лучший вид для параллельных прогонов.
Items Плоская таблица: порядок, имя, тип, статус, попытки, длительность.
Logs Живой tail per item. Фильтр по айтему + уровню + regex. Автопрокрутка с паузой.
Artifacts Список вложений (allure.zip, har.json, скриншоты, JUnit). Inline-превью для мелких text / JSON.
Report Allure-рендер дерева шагов + кнопки выгрузки для всех форматов.
Compare Выбор второго прогона и per-item diff (доступно на любом завершённом прогоне).

Обновления приходят по SSE. Если браузер разрывает соединение,
переподключение прозрачное — поток возобновляется с последнего
полученного event ID через заголовок Last-Event-ID, так что ни одно
событие не теряется.

Форматы экспорта

Каждый прогон может быть выгружен в шести форматах. Выберите тот,
который понимает ваша downstream-обвязка — сервер генерирует по
запросу.

Эндпоинт MIME Когда нужен
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report application/json Allure JSON summary.
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.zip application/zip Allure-архив (result-*.json + вложения).
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.junit.xml application/xml JUnit XML для Jenkins / GitLab / GitHub Actions.
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.md text/markdown Summary для Slack / email / wiki.
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.html text/html Автономный HTML, открывается в браузере, Save-as-PDF.
GET /api/v1/namespaces/:ns/test-plans/:idOrNumericID/runs/:runID/report.unified.json application/json Родной конверт Mockarty (план + счётчики + per-item state).

Все форматы побайтово детерминированы для одних и тех же входных
данных — удобно для snapshot-тестов. Эндпоинты Allure уважают
If-None-Match со строгим ETag; остальные регенерируются на каждый
запрос (это дёшево).

CLI

mockarty-cli testplan report <runID> --plan plan-abc --format allure   -o report.json
mockarty-cli testplan report <runID> --plan plan-abc --format zip      -o report.zip
mockarty-cli testplan report <runID> --plan plan-abc --format junit    -o report.junit.xml
mockarty-cli testplan report <runID> --plan plan-abc --format markdown -o report.md
mockarty-cli testplan report <runID> --plan plan-abc --format html     -o report.html
mockarty-cli testplan report <runID> --plan plan-abc --format unified  -o report.unified.json

SDK

Go Python Java
GetRunReport get_run_report getRunReport
GetRunReportZIP get_run_report_zip getRunReportZip
GetRunReportJUnit get_run_report_junit getRunReportJUnit
GetRunReportMarkdown get_run_report_markdown getRunReportMarkdown
GetRunReportHTML get_run_report_html getRunReportHTML
GetRunReportUnified get_run_report_unified getRunReportUnified

Сравнение прогонов

Сравните любые два прогона. Оба должны жить в пространстве имён вызывающего.
Сравнение прогонов разных планов разрешено — в ответе ставится
summary.differentPlans: true, чтобы UI мог показать баннер.

GET /api/v1/test-plans/runs/compare?run_a=<runID>&run_b=<runID>

Передавайте более старый прогон как run_a, а более новый как
run_b — тогда знаки регрессий и улучшений читаются интуитивно.

Классификатор ставит каждому айтему одну из меток: pass_to_fail,
fail_to_pass, skipped_to_ran, ran_to_skipped, fail_to_fail,
pass_to_pass, added, removed, unchanged. Айтем pass_to_pass
дополнительно помечается durationWorsened, если новый прогон занял
минимум на 20 % дольше базового и абсолютная дельта превысила
250 мс. Мелкий шум на быстрых айтемах таким образом игнорируется.

CLI

mockarty-cli testplan compare-runs <runA> <runB>                     # таблица, скрывает unchanged
mockarty-cli testplan compare-runs <runA> <runB> --diff-only=false   # показывает и unchanged
mockarty-cli testplan compare-runs <runA> <runB> -o json | jq .summary

Exit-код равен 1 при наличии хотя бы одной регрессии — удобно для
CI-гейта.

Поиск сущностей

Все пикеры в UI (Add Item, выбор зависимости, выбор запусков для
агрегированного отчёта, цель расписания) дергают единый search-эндпоинт:

GET /api/v1/entity-search?type=<kind>&q=<имя>&namespace=<ns>&limit=20&offset=0

Поддерживаемые значения type: mock, test_plan, perf_config,
fuzz_config, chaos_experiment, contract_pact. q — поиск
подстроки без учёта регистра по имени; ответ содержит
{ items: [{id, type, name, namespace, createdAt, numericId}], total }.

Полная форма ответа, примеры SDK + CLI и описание MCP-инструмента —
на отдельной странице Поиск сущностей.

Путь распределённого раннера (опция)

По умолчанию admin-нода выполняет каждый айтем тест-плана в своём
процессе. Когда нужно вынести исполнение айтемов на отдельный воркер
mockarty-runner — ради изоляции, сетевой топологии или просто
масштаба — включите claim-очередь:

  1. Admin-нода: установите MOCKARTY_RUNNER_TESTPLAN_ENABLED=true.
    Оркестратор теперь публикует айтемы во внутреннюю claim-очередь
    вместо локального выполнения.
  2. Нода-раннер: установите RUNNER_TESTPLAN_ENABLED=true и
    COORDINATOR_URL=https://<admin>. Опционально:
    RUNNER_TESTPLAN_CONCURRENCY (по умолчанию 4) — сколько айтемов
    раннер может брать параллельно.
  3. Таргетирование per-item: выставьте
    runnerLabels: ["gpu", "staging"] на айтемах, которые должен
    подхватывать конкретный пул раннеров. Пустой список — «любой
    раннер, чьё пространство имён совпадает». Для более выразительного
    таргетинга (OR, NOT, regex) используйте runnerLabelExpr — см.
    Метки раннеров и таргетинг. Поля
    AND-комбинируются, если заданы оба; айтем с выражением минует
    in-memory claim queue и идёт через координатор, чтобы DSL-evaluator
    на стороне раннера мог корректно отработать AST.

Раннер long-poll-ит три эндпоинта:

Метод Путь Назначение
POST /api/v1/runner/testplan/claim Забрать следующий подходящий айтем (до 30 с).
POST /api/v1/runner/testplan/report Отрапортовать терминальный статус + summary + артефакты.
POST /api/v1/runner/testplan/heartbeat Удерживать клейм живым, пока айтем выполняется.

При рестарте admin-ноды или истечении heartbeat-дедлайна не
подтверждённая работа автоматически переочередуется — путь через
раннер имеет at-least-once семантику, и сами раннеры должны делать
попытки идемпотентными.

Если на admin-ноде MOCKARTY_RUNNER_TESTPLAN_ENABLED не выставлен,
все три эндпоинта отвечают 503 Service Unavailable, и раннер
корректно откатывается на классический task-протокол — старые раннеры
не ломаются.

SDK / CLI / MCP

Каждый эндпоинт с этой страницы доступен через:

  • SDK Go / Python / Java — типизированные методы для create, run,
    report, compare, schedule и CRUD вебхуков. См.
    Руководство по SDK.
  • CLI — mockarty-cli testplan <create|run|report|compare-runs| schedule|webhook|stream>. См.
    Руководство по CLI.
  • MCP-инструменты — агенты вызывают list_test_plans,
    run_test_plan, get_test_plan_run, compare_test_plan_runs,
    search_entities, а также экспорты (get_test_plan_run_report,
    …_junit, …_markdown, …_html, …_unified). См.
    ИИ-функции.

Обратная совместимость

  • В новых интеграциях предпочтительнее executionMode
    (fifo / parallel / dag). Прежнее значение schedule
    по-прежнему принимается и держится консистентным, поэтому существующие
    интеграции продолжают работать без изменений.
  • Планы до редизайна использовали dependsOn: [UUID, ...].
    Оркестратор по-прежнему читает этот список и конвертит каждый
    элемент в типизированный gate on_success на рантайме. Повторное
    сохранение плана в новом UI материализует типизированную форму
    gates[].
  • items[].itemUid генерируется при первом сохранении. Легаси-планы,
    не получившие UID, продолжают работать — оркестратор использует
    refId для разрешения зависимостей до следующего сохранения.

Траблшутинг

  • /api/v1/runner/testplan/* возвращает 503 — claim-очередь
    отключена. Выставьте MOCKARTY_RUNNER_TESTPLAN_ENABLED=true на
    admin-ноде и перезапустите.
  • Айтем остаётся в pending бесконечно — при заданных
    runnerLabels проверьте, что хотя бы один mockarty-runner несёт
    подмножество лейблов. На admin /metrics виден
    testplan_claim_queue_depth, на раннере — testplan_runner_claims_total.
  • Ретраи не срабатывают — убедитесь, что retry.maxAttempts > 1.
    Если задан onlyOn, текст ошибки от вашего исполнителя должен
    содержать одну из подстрок (сравнение без учёта регистра).
  • Критический каскад не сработал — каскадятся только айтемы,
    которые на момент падения критического были ещё в pending или
    running. Уже завершённые остаются в своём финальном состоянии.
  • Живой стрим рвётся и теряет события — браузер отправляет
    Last-Event-ID при переподключении, и сервер возобновляет поток.
    Если вы за корпоративным прокси, который режет SSE-заголовки,
    откатитесь на опрос GET /runs/:id с коротким интервалом.

Таймлайн по попыткам (Gantt-стек)

Если у айтема настроена retry-политика, вкладка Timeline рисует одну
под-полосу на каждую попытку, выстраивая их слева направо вместо
единой агрегированной полосы. Цвет под-полосы соответствует статусу
именно этой попытки (passed / failed / timeout), так что транзитные
падения с последующим успешным ретраем видны сразу.

Что показывает UI:

  • Ярлык под-полосы — номер попытки (1, 2, 3, …). Ширина
    пропорциональна окну выполнения этой попытки.
  • Тултип — статус, номер попытки / общее число, длительность,
    отметка таймаута, идентификатор раннера, текст ошибки этой
    попытки (усечённый до 200 символов).
  • Индикатор таймаута — под-полосы попыток, прерванных
    per-item TimeoutMs, рисуются пунктирной рамкой danger-цвета.
  • Активные попытки — «пульсируют», пока не прилетит finish-событие
    по SSE. Ручное обновление страницы не требуется.

Формат payload: каждая строка ItemState в GET /test-plan-runs/:id
теперь содержит массив attemptLog:

{
  "itemUid": "…",
  "status": "passed",
  "attempts": 3,
  "attemptLog": [
    {"startedAt": "2026-04-20T10:00:00Z", "completedAt": "2026-04-20T10:00:02Z",
     "durationMs": 2000, "status": "failed", "error": "transient"},
    {"startedAt": "2026-04-20T10:00:03Z", "completedAt": "2026-04-20T10:00:04Z",
     "durationMs": 1000, "status": "failed", "error": "transient"},
    {"startedAt": "2026-04-20T10:00:05Z", "completedAt": "2026-04-20T10:00:06Z",
     "durationMs": 1000, "status": "passed"}
  ]
}

Для прогонов, созданных до этого релиза, attemptLog пустой — UI в
таком случае показывает единую полосу с аннотацией счётчика попыток
(×3).

Сравнение таймлайнов (наложение прогонов)

Под основным таймлайном можно наложить один или несколько предыдущих
прогонов того же плана на общую ось времени. Кнопка Добавить прогон
тянет itemsState выбранного прогона и отрисовывает его отдельной
дорожкой под основным. Каждая дорожка использует ту же логику
под-полос (стек попыток, индикатор таймаута, цветовая кодировка), так
что расхождения между прогонами — шаг, который раньше выполнялся 2 с,
а сейчас 8 с, или ретрай, который в этом прогоне не сработал — видны
без переключения вкладок.

Сравнение работает клиентски: выбор прогона — это один
GET /api/v1/test-plans/runs/:id и локальный рендер. Никаких
серверных объединений и нагрузки на оркестратор.

Агрегированные отчёты по запускам (Aggregate reports)

Агрегированный отчёт сворачивает два и более существующих test run в
один релизный отчёт. Пригодится, когда одна релизная проверка состоит из
нескольких независимых запусков (fuzz-кампания + chaos-эксперимент +
функциональная регрессия), а вам нужно одно число pass/fail и один отчёт
для stakeholder’ов.

Отчёт без состояния: ничего нового не сохраняется. Вы передаёте список
run ID на каждый вызов, и сервер пересчитывает отчёт на лету. Исходные
запуски сохраняют свою историю, отчёты и retention — нетронутыми.

Построение агрегированного отчёта

# CLI — HTML самодостаточен и дружелюбен к print-to-PDF
mockarty-cli testplan aggregate-report <run-id-1> <run-id-2> [<run-id-3> ...] \
    --name "Release 1.4 gate" --format html -o release.html
// Go SDK
data, err := client.TestRuns().AggregateRunsReport(ctx,
    mockarty.AggregateRunsReportRequest{
        Name:   "Release 1.4 gate",
        RunIDs: []string{runAID, runBID, runCID},
    },
    mockarty.AggregateReportFormatHTML)

Все run ID должны жить в namespace вызывающего (admin и support могут
кросс-namespace). Чтобы получить отчёт по другому набору запусков — просто
вызовите снова с новыми ID: создавать, обновлять или удалять нечего.

Форматы

Формат MIME Назначение
unified application/json Программные потребители (SDK / CLI / CI)
markdown text/markdown Slack / wiki, ручной просмотр
html text/html Релизный отчёт — самодостаточный, print-to-PDF
junit application/xml Приём в CI (Jenkins / GitLab / и т. д.)
mockarty-cli testplan aggregate-report r1 r2 --format junit -o ci.xml
mockarty-cli testplan aggregate-report r1 r2 --format markdown -o out.md

HTML-вывод инлайнит CSS и графики, поэтому открывается в air-gapped
браузере и чисто сохраняется в PDF через диалог печати браузера.

Когда использовать агрегированный отчёт, а когда Test Plan

  • Test Plan — вы заранее решаете, что группа айтемов выполняется
    вместе с явными зависимостями, retry, gate’ами и общим отчётом.
    Подходит для релизных пайплайнов и nightly.
  • Агрегированный отчёт — вы хотите подвести итог постфактум. Запуски
    уже есть, возможно из разных CI-job’ов или ad-hoc, и вам нужен один отчёт.
    Без оркестрации, без сохранённой сущности — только пересчитываемое
    представление.