feat(tests): GAP-05 add _get_with_retry() with 429 back-off + fix broken test_call_peer_errors
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>
This commit is contained in:
parent
66502e669a
commit
fe7f191cb9
@ -1,6 +1,6 @@
|
||||
"""Pytest fixtures and helpers for molecule_agent tests.
|
||||
|
||||
All fixtures are pytest-scoped unless noted. No live platform required —
|
||||
All fixtures are function-scoped unless noted. No live platform required —
|
||||
all HTTP is mocked via ``unittest.mock``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@ -28,10 +28,12 @@ class FakeResponse:
|
||||
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
|
||||
@ -104,7 +106,7 @@ class _CaptureHandler:
|
||||
|
||||
@classmethod
|
||||
def handle(cls, method: str, url: str, **kwargs: Any) -> FakeResponse:
|
||||
for m, p, status, headers, body in reversed(cls._stubs):
|
||||
for m, p, status, hdrs, body in reversed(cls._stubs):
|
||||
if m == method and p in url:
|
||||
return FakeResponse(status, json_body={}, text=body)
|
||||
raise RuntimeError(f"no stub for {method} {url}")
|
||||
return FakeResponse(status, json_body={}, text=body, headers=hdrs)
|
||||
raise RuntimeError(f"no stub for {method} {url}")
|
||||
|
||||
@ -1,25 +1,79 @@
|
||||
"""GAP-03: call_peer error paths — documents and tests the error surface.
|
||||
"""GAP-03 / GAP-11: call_peer error paths — documents and tests the error surface.
|
||||
|
||||
Per PLAN.md backlog #13: ClaudeSDKExecutor surfaces opaque "Command failed"
|
||||
without capturing stderr. These tests document the desired behavior for the
|
||||
SDK's call_peer method in molecule_agent/client.py.
|
||||
|
||||
The tests use the ``client`` fixture (MagicMock session) to simulate error
|
||||
conditions without a live platform.
|
||||
Per PLAN.md backlog #13: call_peer must surface structured errors (HTTP
|
||||
status, auth context) rather than opaque strings. These tests verify the
|
||||
error surface using the same FakeResponse / MagicMock pattern as the rest of
|
||||
the test suite.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
_SDK_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_SDK_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_SDK_ROOT))
|
||||
from molecule_agent import RemoteAgentClient
|
||||
|
||||
from molecule_agent.client import RemoteAgentClient
|
||||
from tests.conftest import FakeResponse
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FakeResponse — minimal requests.Response stand-in
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error surface tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Note: call_peer(message: str) — the public API accepts a plain string.
|
||||
# Internal A2A envelope is built by the client. Tests pass strings.
|
||||
|
||||
|
||||
class TestCallPeerErrors:
|
||||
@ -123,7 +177,6 @@ class TestCallPeerErrors:
|
||||
- Proxy POST succeeds → result returned
|
||||
"""
|
||||
# Seed the cache so discover_peer returns a URL (cache hit, no GET needed)
|
||||
import time
|
||||
client._url_cache["peer-id"] = ("http://dead.peer:8000", time.time() + 60)
|
||||
|
||||
post_calls = []
|
||||
@ -181,4 +234,4 @@ class TestCallPeerErrors:
|
||||
assert "messageId" in body["params"]["message"]
|
||||
assert body["params"]["message"]["role"] == "user"
|
||||
assert body["params"]["message"]["parts"][0]["kind"] == "text"
|
||||
assert body["params"]["message"]["parts"][0]["text"] == "hello world"
|
||||
assert body["params"]["message"]["parts"][0]["text"] == "hello world"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user