mirror of
https://github.com/Sea-Haven-Industries/pr-reviewer.git
synced 2026-09-30 06:53:16 +00:00
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.
143 lines
4.6 KiB
Python
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.",
|
|
}
|