"""Unit tests for agent_team.transport.github_live (§3.3.1, §7.1 P4). The live poster is the production ``requests`` backing for the §3.3.1 injected ``HttpPost`` seam. These tests prove the contract entirely with mocks (no network, and ``requests`` itself is never required): the poster performs the REST POST and returns ``(status, data)``; the posted comment carries the ```` marker; the new comment ``id`` round-trips as the ``channel_ref`` through a real ``GitHubTransport``; a missing package / token fails loudly; and a non-2xx response surfaces as ``GitHubApiError``. No CI, OIDC, git-apply, or GitHub Actions surface is touched — this is human-gate transport I/O only (production default stays P2: clarify->plan->review). """ from __future__ import annotations import importlib from typing import Any import pytest from agent_team.transport.base import GITHUB_MARKER_TEMPLATE, QuestionSet, Transport from agent_team.transport.github_adapter import GitHubApiError, GitHubTransport from agent_team.transport.github_live import ( build_github_poster, build_live_github_transport, ) # --------------------------------------------------------------------------- # # Test doubles # # --------------------------------------------------------------------------- # class _FakeResponse: """A ``requests.Response``-like object: ``status_code`` + ``json()``.""" def __init__(self, status_code: int, data: dict[str, Any]) -> None: self.status_code = status_code self._data = data def json(self) -> dict[str, Any]: return self._data class _FakeSession: """A fake ``requests.Session`` recording ``post`` kwargs and scripting a reply.""" def __init__( self, status_code: int = 201, data: dict[str, Any] | None = None ) -> None: self.response = _FakeResponse( status_code, {"id": 987654321} if data is None else data ) self.calls: list[dict[str, Any]] = [] def post( self, url: str, *, headers: dict[str, str], json: dict[str, Any] ) -> _FakeResponse: self.calls.append({"url": url, "headers": headers, "json": json}) return self.response def _question_set() -> QuestionSet: return QuestionSet( thread_id="task-7", question_id="q-42", turn=1, questions=["Ship it?"], context={"repo": "agent-team"}, ) # --------------------------------------------------------------------------- # # Clean import without requests # # --------------------------------------------------------------------------- # def test_module_imports_without_requests(monkeypatch: pytest.MonkeyPatch) -> None: """The module imports cleanly even when ``requests`` cannot be imported.""" import builtins real_import = builtins.__import__ def _blocked_import(name: str, *args: Any, **kwargs: Any) -> Any: if name == "requests" or name.startswith("requests."): raise ImportError("requests is blocked for this test") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", _blocked_import) module = importlib.reload( importlib.import_module("agent_team.transport.github_live") ) assert hasattr(module, "build_github_poster") assert hasattr(module, "build_live_github_transport") # --------------------------------------------------------------------------- # # Happy path: injected fake client # # --------------------------------------------------------------------------- # def test_poster_returns_status_and_data() -> None: """The poster forwards to the client and returns ``(status, data)``.""" session = _FakeSession() poster = build_github_poster(client=session) status, data = poster( "https://api.github.com/repos/o/r/issues/1/comments", headers={"Authorization": "Bearer x"}, json_body={"body": "hi"}, ) assert status == 201 assert data["id"] == 987654321 assert len(session.calls) == 1 assert session.calls[0]["json"] == {"body": "hi"} def test_post_question_round_trips_comment_id_as_channel_ref() -> None: """Wired through a real ``GitHubTransport``, the comment id is the channel_ref.""" session = _FakeSession(data={"id": 555}) transport = GitHubTransport( owner="Sea-Haven-Industries", repo="agent-team", issue_number=1, http_post=build_github_poster(client=session), token_provider=lambda: "ghp_fake", ) channel_ref = transport.post_question( thread_id="task-7", question_id="q-42", turn=1, question_set=_question_set(), deadline="2026-06-18T00:00:00Z", ) assert channel_ref == "555" def test_posted_comment_carries_question_id_marker() -> None: """The posted comment body embeds ````.""" session = _FakeSession() transport = GitHubTransport( owner="Sea-Haven-Industries", repo="agent-team", issue_number=1, http_post=build_github_poster(client=session), token_provider=lambda: "ghp_fake", ) transport.post_question( thread_id="task-7", question_id="q-42", turn=1, question_set=_question_set(), deadline="2026-06-18T00:00:00Z", ) assert len(session.calls) == 1 body = session.calls[0]["json"]["body"] assert GITHUB_MARKER_TEMPLATE.format(question_id="q-42") in body def test_convenience_transport_factory_round_trips_and_parses() -> None: """``build_live_github_transport`` wires the poster and the marker round-trips.""" session = _FakeSession(data={"id": 777}) transport = build_live_github_transport( owner="Sea-Haven-Industries", repo="agent-team", issue_number=1, token="ghp_fake", client=session, ) assert isinstance(transport, Transport) channel_ref = transport.post_question( thread_id="task-7", question_id="q-42", turn=1, question_set=_question_set(), deadline="2026-06-18T00:00:00Z", ) assert channel_ref == "777" # The factory threads one token into the transport's auth header. assert session.calls[0]["headers"]["Authorization"] == "Bearer ghp_fake" # The marker the poster shipped round-trips back through parse_answer: a # human reply quoting the question comment recovers the same question_id. posted_body = session.calls[0]["json"]["body"] reply = "> " + posted_body.replace("\n", "\n> ") + "\nLooks good, ship it." question_id, answer, via = transport.parse_answer( {"comment": {"body": reply, "user": {"login": "adam"}}} ) assert question_id == "q-42" assert answer == "Looks good, ship it." assert via == "github:adam" def test_response_with_status_attr_accepted() -> None: """A response exposing ``status`` (not ``status_code``) is also accepted.""" class _StatusOnly: status = 200 def json(self) -> dict[str, Any]: return {"id": 1} class _Session: def post(self, url: str, **kwargs: Any) -> _StatusOnly: return _StatusOnly() poster = build_github_poster(client=_Session()) status, data = poster("u", headers={}, json_body={}) assert status == 200 assert data["id"] == 1 def test_empty_body_coerces_to_dict() -> None: """A non-dict JSON body coerces to ``{}`` (adapter's missing-id guard fires).""" class _NullJson: status_code = 201 def json(self) -> Any: return None class _Session: def post(self, url: str, **kwargs: Any) -> _NullJson: return _NullJson() poster = build_github_poster(client=_Session()) status, data = poster("u", headers={}, json_body={}) assert status == 201 assert data == {} # --------------------------------------------------------------------------- # # Failure modes # # --------------------------------------------------------------------------- # def test_non_2xx_response_raises_github_api_error() -> None: """A non-2xx response surfaces as ``GitHubApiError`` carrying the status.""" session = _FakeSession(status_code=403, data={"message": "Forbidden"}) poster = build_github_poster(client=session) with pytest.raises(GitHubApiError) as excinfo: poster("u", headers={}, json_body={"body": "x"}) assert excinfo.value.status == 403 def test_unsupported_response_raises_type_error() -> None: """A response with neither status nor json() is fatal (not a silent success).""" class _Session: def post(self, url: str, **kwargs: Any) -> object: return object() poster = build_github_poster(client=_Session()) with pytest.raises(TypeError, match="status_code"): poster("u", headers={}, json_body={}) def test_missing_token_raises_runtime_error(monkeypatch: pytest.MonkeyPatch) -> None: """No token and no GITHUB_TOKEN raises a clear RuntimeError. Stub ``requests`` into ``sys.modules`` so the deferred import SUCCEEDS and the no-token branch is what's under test (avoids local-vs-CI drift where a missing package would otherwise mask the token check). """ import sys from types import ModuleType fake = ModuleType("requests") fake.Session = lambda: type( "S", (), {"headers": {}, "post": lambda self, *a, **k: None} )() # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "requests", fake) monkeypatch.delenv("GITHUB_TOKEN", raising=False) with pytest.raises(RuntimeError, match="GitHub token"): build_github_poster() def test_missing_package_raises_runtime_error(monkeypatch: pytest.MonkeyPatch) -> None: """A missing ``requests`` package raises a clear RuntimeError.""" import builtins real_import = builtins.__import__ def _blocked_import(name: str, *args: Any, **kwargs: Any) -> Any: if name == "requests" or name.startswith("requests."): raise ImportError("requests is blocked for this test") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", _blocked_import) monkeypatch.setenv("GITHUB_TOKEN", "ghp_present") with pytest.raises(RuntimeError, match="requests is unavailable"): build_github_poster() def test_token_falls_back_to_env(monkeypatch: pytest.MonkeyPatch) -> None: """With ``requests`` stubbed, a GITHUB_TOKEN env var builds a session cleanly.""" import sys from types import ModuleType captured: dict[str, Any] = {} class _Session: def __init__(self) -> None: self.headers: dict[str, str] = {} def post(self, *a: Any, **k: Any) -> None: # pragma: no cover - unused return None def _make_session() -> _Session: session = _Session() captured["session"] = session return session fake = ModuleType("requests") fake.Session = _make_session # type: ignore[attr-defined] monkeypatch.setitem(sys.modules, "requests", fake) monkeypatch.setenv("GITHUB_TOKEN", "ghp_from_env") poster = build_github_poster() assert callable(poster) assert captured["session"].headers["Authorization"] == "Bearer ghp_from_env"