pr-reviewer/tests/conftest.py
Adam Moussa 89494c1710
Ground reviews in the engineering-handbook
Feed the reviewer a distilled digest of the Sea Haven engineering-handbook
so findings reflect our naming, commit, PR, secrets, and IaC conventions
instead of generic code-review judgment.

A new handbook module keeps an app-managed shallow clone of the (private)
handbook, distills the review-relevant pages into a compact conventions
checklist via the Fireworks model once a day, caches it under ~/.cache,
and hands it to the reviewer to inject into every review's system prompt.
The refresh runs in-process at the start of each worker cycle; failures
keep the last good digest and back off, so a handbook outage never blocks
reviews. Set HANDBOOK_ENABLED=false to disable.

Extract a shared fireworks_complete helper used by both the reviewer and
the distiller, so the handbook provider needs no reviewer reference and
the guidance callable is set once at construction. Clone auth uses a
Basic http.extraHeader (GitHub git-over-HTTPS rejects Bearer), and the
distiller wraps its answer in delimiters to strip a reasoning model's
chain-of-thought preamble. Adds GET /api/handbook and a header status
line. Stdlib-only, no new dependencies.
2026-07-01 17:08:21 -04:00

143 lines
4.6 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,
# 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.",
}