"""Shared test fixtures and stdlib-only httpx2 stubs. The app creates ``httpx2.Client(...)`` inline inside each method, so we mock the network by monkeypatching ``httpx2.Client`` in the target module namespace with a ``FakeClient`` that records requests and returns canned ``FakeResponse`` objects. No real network, no secrets, fully offline. """ from __future__ import annotations import json from types import SimpleNamespace from typing import Any import pytest class FakeResponse: """Minimal stand-in for ``httpx2.Response`` covering what the app touches: ``status_code``, ``.json()``, ``.text``, and ``.raise_for_status()``.""" def __init__( self, status_code: int = 200, *, json_data: Any = None, text: str | None = None, ) -> None: self.status_code = status_code self._json = json_data self._json_is_set = json_data is not None if text is not None: self.text = text elif json_data is not None: self.text = json.dumps(json_data) else: self.text = "" def json(self) -> Any: if not self._json_is_set: # Mimic httpx/requests raising when the body is not valid JSON. raise json.JSONDecodeError("no json", self.text or "", 0) return self._json def raise_for_status(self) -> None: if self.status_code >= 400: raise RuntimeError(f"HTTP {self.status_code}") class FakeClient: """Context-manager stub for ``httpx2.Client``. Returns a single canned response for every ``get``/``post`` and appends each call (method, url, kwargs) to ``calls`` so tests can assert on the payload. """ def __init__(self, response: FakeResponse, calls: list[dict[str, Any]]) -> None: self._response = response self._calls = calls def __enter__(self) -> FakeClient: return self def __exit__(self, *exc: object) -> bool: return False def get(self, url: str, **kwargs: Any) -> FakeResponse: self._calls.append({"method": "GET", "url": url, **kwargs}) return self._response def post(self, url: str, **kwargs: Any) -> FakeResponse: self._calls.append({"method": "POST", "url": url, **kwargs}) return self._response def patch_httpx(monkeypatch: pytest.MonkeyPatch, module: Any, response: FakeResponse): """Patch ``module.httpx2.Client`` to hand back a ``FakeClient``. Returns the ``calls`` list that records every request made through it. """ calls: list[dict[str, Any]] = [] def _factory(*_args: Any, **_kwargs: Any) -> FakeClient: return FakeClient(response, calls) monkeypatch.setattr(module.httpx2, "Client", _factory) return calls @pytest.fixture def make_cfg(): """Factory for a lightweight config object with sane test defaults. Uses ``SimpleNamespace`` so tests never depend on env/.env and never need a real ``Config``. Override any field via keyword args. """ def _make(**overrides: Any) -> SimpleNamespace: defaults: dict[str, Any] = { # GitHub "GITHUB_TOKEN": "test-token", "GITHUB_ORG": "Sea-Haven-Industries", "PR_SEARCH_FILTER": "is:pr state:open org:Sea-Haven-Industries", "MAX_PRS": 30, "MAX_DIFF_BYTES": 120000, # Fireworks "FIREWORKS_API_KEY": "fw_test_key", "FIREWORKS_BASE_URL": "https://api.fireworks.ai/inference/v1", "FIREWORKS_MODEL": "accounts/fireworks/models/deepseek-v4-pro", "FIREWORKS_TEMPERATURE": 0.2, "FIREWORKS_MAX_TOKENS": 4000, # Review behavior "MENTION_AUTHORS": ["openswe"], "HOST": "127.0.0.1", "PORT": 8765, # Worker "POLL_INTERVAL": 300, "WORKER_CONCURRENCY": 2, "MAX_REVIEW_ATTEMPTS": 3, "CACHE_DB": ":memory:", # Handbook "HANDBOOK_ENABLED": True, "HANDBOOK_REPO_URL": "https://example.test/engineering-handbook.git", "HANDBOOK_CACHE_DIR": "/tmp/pr-reviewer-test/handbook", "HANDBOOK_DIGEST_PATH": "/tmp/pr-reviewer-test/handbook_digest.json", "HANDBOOK_REFRESH_HOURS": 24, } defaults.update(overrides) return SimpleNamespace(**defaults) return _make @pytest.fixture def sample_pr() -> dict[str, Any]: return { "owner": "Sea-Haven-Industries", "repo": "pr-reviewer", "number": 7, "title": "Add caching layer", "author": "octocat", "body": "This PR adds a cache.", }