Modern QA2026Паттерн аварийного выключателя
Join

Course03 Agentic Testing Architectures

Cutting-edge · Chapter 03

Паттерн аварийного выключателя

Updated Jul 2026

Последний рубеж обороны

Даже при наличии всех пяти ограждений существуют сценарии, когда сама обвязка выходит из строя. Агент может зависнуть на системном вызове. Процесс обвязки может получить segfault. Сетевой разрыв может заморозить вызов LLM навсегда. Аварийный выключатель -- это внешняя страховочная сеть, которая убивает тестовый прогон, когда сама обвязка перестаёт отвечать.

Реализация в CI/CD

Аварийный выключатель находится вне агента и обвязки. Он настраивается на уровне CI-раннера:

GitHub Actions

- name: Run agentic tests
  timeout-minutes: 15           # Hard CI-level timeout
  env:
    AGENT_MAX_STEPS: 30
    AGENT_MAX_TOKENS: 50000
    AGENT_ALLOWED_DOMAINS: "staging.myapp.com,api.staging.myapp.com"
  run: |
    python -m pytest tests/agentic/ \
      --timeout=300 \            # Per-test timeout (pytest-timeout)
      --agent-budget=$AGENT_MAX_TOKENS

Многоуровневая структура таймаутов:

Layer 1: Per-action timeout (30s)
  → Catches: hung browser, slow element lookup
  → Set by: the harness (action-level)

Layer 2: Per-test timeout (300s / 5 min)
  → Catches: stuck agent loop, slow LLM response chain
  → Set by: pytest-timeout plugin

Layer 3: CI job timeout (15 min)
  → Catches: harness hang, process deadlock, zombie processes
  → Set by: GitHub Actions timeout-minutes

Layer 4: CI workflow timeout (60 min)
  → Catches: entire workflow stuck (multiple jobs)
  → Set by: GitHub Actions workflow-level timeout

GitLab CI

agentic_tests:
  timeout: 15 minutes
  variables:
    AGENT_MAX_STEPS: "30"
    AGENT_MAX_TOKENS: "50000"
  script:
    - python -m pytest tests/agentic/ --timeout=300
  after_script:
    - python scripts/cleanup_agent_processes.py

Jenkins

pipeline {
    options {
        timeout(time: 15, unit: 'MINUTES')
    }
    stages {
        stage('Agentic Tests') {
            steps {
                sh '''
                    timeout 300 python -m pytest tests/agentic/ \
                        --timeout=120
                '''
            }
            post {
                always {
                    sh 'pkill -f "vibe-check daemon" || true'
                    sh 'pkill -f "chromedriver" || true'
                }
            }
        }
    }
}

Очистка процессов

Когда аварийный выключатель срабатывает, он убивает процесс резко. Это может оставить зомби-процессы (экземпляры Chrome, демон-процессы, временные файлы). Всегда реализуйте очистку:

#!/usr/bin/env python3
"""cleanup_agent_processes.py — Run after agentic tests to clean up."""

import subprocess
import os
import signal
import glob

def cleanup():
    # Kill any remaining Chrome processes started by tests
    subprocess.run(["pkill", "-f", "chrome.*--test-type"], capture_output=True)

    # Kill any vibe-check daemon processes
    subprocess.run(["pkill", "-f", "vibe-check daemon"], capture_output=True)

    # Kill any chromedriver processes
    subprocess.run(["pkill", "-f", "chromedriver"], capture_output=True)

    # Remove temporary screenshot files
    for f in glob.glob("/tmp/agent_screenshot_*.png"):
        os.remove(f)

    # Remove stale lock files
    for f in glob.glob("/tmp/agent_lock_*"):
        os.remove(f)

    # Remove shared state files from agent runs
    for f in glob.glob("/tmp/agent_state_*.json"):
        os.remove(f)

    print("Agent cleanup complete")

if __name__ == "__main__":
    cleanup()

Мониторинг heartbeat

Для длительных агентных сессий (ночное исследовательское тестирование) реализуйте heartbeat:

import threading
import time
import sys

class HeartbeatMonitor:
    """Kills the process if the agent stops making progress."""

    def __init__(self, max_idle_seconds: int = 120):
        self.max_idle = max_idle_seconds
        self.last_heartbeat = time.time()
        self._running = True
        self._thread = threading.Thread(target=self._monitor, daemon=True)
        self._thread.start()

    def beat(self):
        """Call this every time the agent takes an action."""
        self.last_heartbeat = time.time()

    def _monitor(self):
        while self._running:
            idle = time.time() - self.last_heartbeat
            if idle > self.max_idle:
                print(f"DEAD MAN'S SWITCH: No heartbeat for {idle:.0f}s. "
                      f"Killing process.", file=sys.stderr)
                os._exit(1)  # Hard exit — bypass cleanup
            time.sleep(10)  # Check every 10 seconds

    def stop(self):
        self._running = False

# Usage in the harness
monitor = HeartbeatMonitor(max_idle_seconds=120)

class HarnessWithHeartbeat(ConstrainedTestHarness):
    def execute(self, objective):
        for step in range(self.config.max_steps):
            monitor.beat()  # Signal that we are still making progress
            action = self.agent.next_action()
            result = self.agent.execute_action(action)
            if result.is_terminal:
                monitor.stop()
                return result

Полный стек безопасности

+----------------------------------------------------------+
|  Layer 4: CI Workflow Timeout (60 min)                    |
|  +------------------------------------------------------+|
|  |  Layer 3: CI Job Timeout (15 min)                    ||
|  |  +--------------------------------------------------+||
|  |  |  Layer 2: Per-Test Timeout (300s)                |||
|  |  |  +----------------------------------------------+|||
|  |  |  |  Layer 1: Per-Action Timeout (30s)           ||||
|  |  |  |  +------------------------------------------+||||
|  |  |  |  |  Guardrails: steps, tokens, domains,     |||||
|  |  |  |  |  actions — enforced by the harness       |||||
|  |  |  |  +------------------------------------------+||||
|  |  |  +----------------------------------------------+|||
|  |  +--------------------------------------------------+||
|  +------------------------------------------------------+|
+----------------------------------------------------------+

Каждый уровень ловит сбои, которые внутренний уровень пропускает:

  • Ограждения ловят нарушения агентом (слишком много шагов, неправильный домен)
  • Таймаут на действие ловит зависшие браузерные операции
  • Таймаут на тест ловит застрявшие циклы агента
  • Таймаут задачи CI ловит зависание обвязки/процесса
  • Таймаут рабочего процесса CI ловит сбои на уровне инфраструктуры

Антипаттерн: отсутствие выключателя

# DANGEROUS: no timeouts at any level
- name: Run agentic tests
  run: python -m pytest tests/agentic/

Эта задача может работать бесконечно. Если агент входит в бесконечный цикл и обвязка не обнаруживает это, CI-раннер заблокирован до ручного завершения. В общем CI-окружении это блокирует другие команды.

Всегда устанавливайте как минимум два уровня таймаута: один в тестовом фреймворке и один в CI-раннере.

Ключевой вывод

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