Adds retry-on-429 with exponential back-off (1 s → 2 s → 4 s, ±25% jitter, 30 s cap, Retry-After header honoured) to all idempotent RemoteAgentClient GET calls: poll_state, pull_secrets, get_peers, discover_peer. Also fixes the merged test_call_peer_errors.py (PR #7) which was broken: - Removed pytest-mock dependency (mocker not installed) - Fixed call_peer(message: str) vs dict - Fixed non-existent _call_direct/_call_proxy method patches - Uses FakeResponse + _session.post.side_effect pattern consistently Adds tests/conftest.py (FakeResponse + client fixture + _CaptureHandler) and tests/test_retry_backoff.py (18 new tests). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.2 KiB
Python
113 lines
3.2 KiB
Python
"""Pytest fixtures and helpers for molecule_agent tests.
|
|
|
|
All fixtures are function-scoped unless noted. No live platform required —
|
|
all HTTP is mocked via ``unittest.mock``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import stat
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from molecule_agent import RemoteAgentClient
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FakeResponse — minimal requests-shaped response
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class FakeResponse:
|
|
"""Minimal stand-in for ``requests.Response``."""
|
|
|
|
def __init__(
|
|
self,
|
|
status_code: int = 200,
|
|
json_body: Any = None,
|
|
text: str = "",
|
|
headers: dict[str, str] | None = None,
|
|
) -> None:
|
|
self.status_code = status_code
|
|
self._json = json_body
|
|
self.text = text
|
|
self.headers = headers or {}
|
|
|
|
def json(self) -> Any:
|
|
return self._json
|
|
|
|
def raise_for_status(self) -> None:
|
|
if self.status_code >= 400:
|
|
import requests
|
|
raise requests.HTTPError(f"HTTP {self.status_code}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_token_dir(tmp_path: Path) -> Path:
|
|
return tmp_path / "molecule-token-cache"
|
|
|
|
|
|
@pytest.fixture
|
|
def client(tmp_token_dir: Path) -> RemoteAgentClient:
|
|
"""RemoteAgentClient with a MagicMock session for unit tests."""
|
|
session = MagicMock()
|
|
return RemoteAgentClient(
|
|
workspace_id="ws-test-123",
|
|
platform_url="http://platform.test",
|
|
agent_card={"name": "test-agent"},
|
|
token_dir=tmp_token_dir,
|
|
session=session,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _CaptureHandler — minimal HTTP mock for integration tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _CaptureHandler:
|
|
"""Thread-local registry of HTTP stubs for use in integration tests.
|
|
|
|
Registered stubs are checked in order (last-registered first); the first
|
|
matching (method, path) pair wins. Unmatched requests raise
|
|
``RuntimeError("no stub for {method} {path}")``.
|
|
|
|
Usage::
|
|
|
|
_CaptureHandler.clear()
|
|
_CaptureHandler.stub("GET", "/foo", 200, {}, "body")
|
|
with some_client:
|
|
result = await some_client.get("/foo")
|
|
"""
|
|
|
|
_stubs: list[tuple[str, str, int, dict[str, str], str]] = []
|
|
|
|
@classmethod
|
|
def clear(cls) -> None:
|
|
cls._stubs.clear()
|
|
|
|
@classmethod
|
|
def stub(
|
|
cls,
|
|
method: str,
|
|
path: str,
|
|
status: int,
|
|
headers: dict[str, str],
|
|
body: str,
|
|
) -> None:
|
|
cls._stubs.append((method, path, status, headers, body))
|
|
|
|
@classmethod
|
|
def handle(cls, method: str, url: str, **kwargs: Any) -> FakeResponse:
|
|
for m, p, status, hdrs, body in reversed(cls._stubs):
|
|
if m == method and p in url:
|
|
return FakeResponse(status, json_body={}, text=body, headers=hdrs)
|
|
raise RuntimeError(f"no stub for {method} {url}")
|