Skip to main content

自己修復テストパイプライン

Synapse メモリを使用して失敗から学習し自動適応するテストパイプラインの構築。


自己修復テストパイプライン

従来のテストスイートは UI が変わると壊れます。自己修復テストは Synapse メモリを使用して過去の失敗から学習し適応します — フレイキーテストとメンテナンス負荷を削減します。

コンセプト

┌─────────┐  fails   ┌──────────┐  store   ┌──────────┐
│  Test   │ ───────▶ │  Synapse │ ───────▶ │ Memories │
│  Run    │          │  Memory  │          │ (failures)│
└─────────┘          └──────────┘          └──────────┘
                           ▲                     │
                           │   recall            │
                           │  before next run    │
                           └─────────────────────┘
  1. テストを実行
  2. 失敗したら、失敗内容(何が、なぜ、どう修正するか)を保存
  3. 次回実行:実行前に該当する失敗を再取得
  4. 既知の修正を自動的に適用

実装

ステップ 1:テストラッパー

各テストをメモリの再取得/保存でラップします。

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 past failures for this test
        past_failures = requests.get(
            f"{URL}/memory/search?q={test_name}+failure",
            headers={"Authorization": f"Bearer {MIND_KEY}"}
        ).json()
        
        # 2. Run test with failure context
        try:
            test_fn(known_failures=past_failures)
        except Exception as e:
            # 3. Store the failure
            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"
        })

ステップ 2:適応型テストロジック

テスト内で既知の失敗を確認し、修正を適用します。

@self_healing_test
def test_login_page(browser, known_failures=None):
    browser.goto("https://app.com/login")
    
    # Check if we've seen this page change before
    if known_failures and known_failures.get("results"):
        for failure in known_failures["results"]:
            if "button moved" in failure["content"].lower():
                # Use accessibility label instead of coordinates
                browser.click(by_label="Login button")
                return
    
    # Default: use coordinates
    browser.click(x=150, y=400)

ステップ 3:復旧戦略

復旧戦略をメモリとして保存します。

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"
        })

# Store recoveries for common failures
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.")

ステップ 4:CI 連携

# .gitlab-ci.yml
test:self-healing:
  script:
    - export SYNAPSE_MIND_KEY=$SYNAPSE_TEST_MIND_KEY
    - pytest tests/ --self-healing
  after_script:
    # Summarize new failures
    - python scripts/synapse_failure_summary.py

ステップ 5:失敗分析ダッシュボード

# Get all test failures from the last week
r = requests.get(
    f"{URL}/memory/search?q=test+failure",
    headers={"Authorization": f"Bearer {MIND_KEY}"}
)

# Group by test name
failures = {}
for mem in r.json().get("results", []):
    test_name = extract_test_name(mem["content"])
    failures.setdefault(test_name, []).append(mem)

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

ベストプラクティス

保存すべき一般的な失敗パターン

失敗種別 保存すべき内容
要素が見つからない 試したセレクタ、ページ状態、スクリーンショット
タイムアウト 待機時間、何を待っていたか
アサーション失敗 期待値と実際の値
ネットワークエラー URL、ステータスコード、レスポンス本文
権限拒否 必要な権限、現在のユーザーロール

次のステップ