Skip to main content

Samonaprawiające się potoki testowe

Budowa potoków testowych, które uczą się na błędach i adaptują automatycznie z użyciem pamięci Synapse.


Samonaprawiające się potoki testowe

Tradycyjne zestawy testów psują się, gdy zmienia się UI. Testy samonaprawiające używają pamięci Synapse, aby uczyć się na przeszłych błędach i adaptować — co zmniejsza flaky tests i obciążenie utrzymaniem.

Koncepcja

┌─────────┐  fails   ┌──────────┐  store   ┌──────────┐
│  Test   │ ───────▶ │  Synapse │ ───────▶ │ Memories │
│  Run    │          │  Memory  │          │ (failures)│
└─────────┘          └──────────┘          └──────────┘
                           ▲                     │
                           │   recall            │
                           │  before next run    │
                           └─────────────────────┘
  1. Uruchomienie testu
  2. W razie niepowodzenia zapisanie błędu (co poszło nie tak, dlaczego, jak naprawić)
  3. Następne uruchomienie: recall istotnych błędów przed wykonaniem
  4. Automatyczne zastosowanie znanych poprawek

Implementacja

Krok 1: opakowanie testu

Opakowanie każdego testu recall/zapisem pamięci:

import requests
from datetime import datetime

URL = "https://synapse.schaefer.zone"
MIND_KEY = "mk_..."

def self_healing_test(test_name, test_fn):
    """Decorator: wrap a test with self-healing memory."""
    def wrapper():
        # 1. Recall przeszłych błędów tego testu
        past_failures = requests.get(
            f"{URL}/memory/search?q={test_name}+failure",
            headers={"Authorization": f"Bearer {MIND_KEY}"}
        ).json()
        
        # 2. Uruchomienie testu z kontekstem błędów
        try:
            test_fn(known_failures=past_failures)
        except Exception as e:
            # 3. Zapisanie błędu
            store_failure(test_name, e, traceback.format_exc())
            raise
    
    return wrapper

def store_failure(test_name, error, traceback_str):
    requests.post(f"{URL}/memory",
        headers={"Authorization": f"Bearer {MIND_KEY}",
                 "Content-Type": "application/json"},
        json={
            "category": "mistake",
            "key": f"test_failure_{test_name}_{datetime.now().isoformat()}",
            "content": f"Test: {test_name}\nError: {error}\nTrace:\n{traceback_str}",
            "tags": ["test", "failure", test_name],
            "priority": "high"
        })

Krok 2: adaptacyjna logika testu

Wewnątrz testu sprawdzać znane błędy i stosować poprawki:

@self_healing_test
def test_login_page(browser, known_failures=None):
    browser.goto("https://app.com/login")
    
    # Sprawdzenie, czy ta zmiana strony była już wcześniej widziana
    if known_failures and known_failures.get("results"):
        for failure in known_failures["results"]:
            if "button moved" in failure["content"].lower():
                # Użycie etykiety dostępności zamiast współrzędnych
                browser.click(by_label="Login button")
                return
    
    # Domyślnie: użycie współrzędnych
    browser.click(x=150, y=400)

Krok 3: strategie odzyskiwania

Zapis strategii odzyskiwania jako wspomnień:

def store_recovery(failure_type, strategy):
    requests.post(f"{URL}/memory",
        headers={"Authorization": f"Bearer {MIND_KEY}",
                 "Content-Type": "application/json"},
        json={
            "category": "skill",
            "key": f"recovery_{failure_type}",
            "content": strategy,
            "tags": ["test", "recovery", failure_type],
            "priority": "high"
        })

# Zapis odzyskiwań dla częstych błędów
store_recovery("element_not_found",
    "When element not found by ID, try by CSS class, then by XPath, "
    "then by accessibility label. Take screenshot for debugging.")

store_recovery("timeout",
    "Increase timeout to 30s. If still fails, check if page is loading "
    "dynamically — wait for specific element instead of fixed time.")

store_recovery("stale_element",
    "Re-find element before each interaction. Don't cache element references "
    "across page transitions.")

Krok 4: integracja CI

# .gitlab-ci.yml
test:self-healing:
  script:
    - export SYNAPSE_MIND_KEY=$SYNAPSE_TEST_MIND_KEY
    - pytest tests/ --self-healing
  after_script:
    # Podsumowanie nowych błędów
    - python scripts/synapse_failure_summary.py

Krok 5: panel analizy błędów

# Pobranie wszystkich błędów testów z ostatniego tygodnia
r = requests.get(
    f"{URL}/memory/search?q=test+failure",
    headers={"Authorization": f"Bearer {MIND_KEY}"}
)

# Grupowanie po nazwie testu
failures = {}
for mem in r.json().get("results", []):
    test_name = extract_test_name(mem["content"])
    failures.setdefault(test_name, []).append(mem)

# Raport
for test, fails in sorted(failures.items(), key=lambda x: -len(x[1])):
    print(f"{test}: {len(fails)} failures")

Najlepsze praktyki

Częste wzorce błędów do zapisu

Typ błędu Co zapisać
Element nie znaleziony Próbowany selektor, stan strony, zrzut ekranu
Timeout Czas oczekiwania, na co czekano
Asercja nie powiodła się Oczekiwana vs rzeczywista wartość
Błąd sieci URL, kod statusu, treść odpowiedzi
Odmowa uprawnień Wymagane uprawnienie, bieżąca rola użytkownika

Następne kroki