pr-reviewer/tests/conftest.py
Adam Moussa 46cd856fb3
Add pr-reviewer local PR review tool
A single-user local dashboard that pulls open PRs from the
Sea-Haven-Industries org, reviews each with a Fireworks model in the
BLOCK/FIX/NIT/QUESTION format, and posts the review to GitHub as the
token owner. Runs only on localhost; secrets stay in a gitignored .env
and never reach the browser.

Structured as an app/ package plus a static/ frontend so the module
imports and static mount resolve. HTTP uses httpx2 (the runtime lib
starlette's TestClient now prefers), pinned in requirements.txt.

Includes a stdlib-only pytest suite (network mocked, no extra test
deps so CI needs only pytest) with a skip-by-default live Fireworks
test, and CI wired to the org ci-python-app reusable workflow to lint
app and tests and run the mocked suite fully offline. Dependabot covers
pip and github-actions.
2026-07-01 13:48:10 -04:00

132 lines
4.1 KiB
Python

"""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,
}
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.",
}