"""Unit tests for agent_team.transport.github_intake (ยง3.3.1, INTAKE poller). Fully hermetic: both the GitHub issue client and the coordinator are injected in-memory fakes, so no network call, token, GitHub SDK, or model is exercised. The tests pin the poller's contract: a labeled issue creates exactly one task, a re-poll does not double-ingest, unlabeled issues are never seen (the client filters by label), and the intake text is the issue title + body. """ from __future__ import annotations from typing import Any import pytest from agent_team.transport.github_intake import ( GITHUB_TRANSPORT_NAME, GithubIntake, issue_task_text, ) INTAKE_LABEL = "agent-team" # --------------------------------------------------------------------------- # # Fakes # --------------------------------------------------------------------------- # class FakeIssueClient: """In-memory ``GithubIssueClient`` returning only issues carrying ``label``. Mirrors the production client's contract: ``list_open_issues(label=...)`` returns the subset of the configured issues whose ``labels`` include the requested label. Records each requested label so a test can assert the poller queries with the configured label. """ def __init__(self, issues: list[dict[str, Any]]) -> None: self.issues = issues self.requested_labels: list[str] = [] def list_open_issues(self, *, label: str) -> list[dict[str, Any]]: self.requested_labels.append(label) return [issue for issue in self.issues if label in (issue.get("labels") or [])] class FakeCoordinator: """In-memory coordinator double recording every ``start_task`` call. Captures the keyword arguments of each call so a test can assert exactly one task was started, with the expected ``task_text`` / ``transport_name``. Returns a synthetic ``thread_id`` like the real coordinator. """ def __init__(self) -> None: self.calls: list[dict[str, Any]] = [] def start_task(self, *, task_text: str, transport_name: str) -> str: self.calls.append({"task_text": task_text, "transport_name": transport_name}) return f"thread-{len(self.calls)}" def _issue( issue_id: int, *, title: str = "Do the thing", body: str = "with details", labels: list[str] | None = None, ) -> dict[str, Any]: """Build a minimal GitHub-issue-shaped mapping for the fakes.""" return { "id": issue_id, "number": issue_id, "title": title, "body": body, "labels": [INTAKE_LABEL] if labels is None else labels, } # --------------------------------------------------------------------------- # # issue_task_text # --------------------------------------------------------------------------- # def test_issue_task_text_joins_title_and_body() -> None: text = issue_task_text(_issue(1, title="Add poller", body="for GitHub intake")) assert text == "Add poller\n\nfor GitHub intake" def test_issue_task_text_title_only_when_body_empty() -> None: assert issue_task_text(_issue(1, title="Title only", body="")) == "Title only" assert issue_task_text(_issue(1, title="Title only", body=" ")) == "Title only" def test_issue_task_text_falls_back_to_id_when_title_empty() -> None: text = issue_task_text(_issue(42, title="", body="")) assert text == "issue #42" def test_issue_task_text_strips_surrounding_whitespace() -> None: text = issue_task_text(_issue(1, title=" Trim me ", body="\n body \n")) assert text == "Trim me\n\nbody" # --------------------------------------------------------------------------- # # GithubIntake construction # --------------------------------------------------------------------------- # def test_empty_label_is_rejected() -> None: with pytest.raises(ValueError): GithubIntake( client=FakeIssueClient([]), coordinator=FakeCoordinator(), label="" ) # --------------------------------------------------------------------------- # # poll_once: the core contract # --------------------------------------------------------------------------- # def test_labeled_issue_creates_exactly_one_task() -> None: client = FakeIssueClient([_issue(1, title="Build it", body="now")]) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) ingested = intake.poll_once() assert ingested == ["1"] assert len(coordinator.calls) == 1 call = coordinator.calls[0] assert call["task_text"] == "Build it\n\nnow" assert call["transport_name"] == GITHUB_TRANSPORT_NAME assert client.requested_labels == [INTAKE_LABEL] def test_repoll_does_not_double_ingest() -> None: client = FakeIssueClient([_issue(1)]) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) first = intake.poll_once() second = intake.poll_once() assert first == ["1"] assert second == [] # already ingested -> no new task assert len(coordinator.calls) == 1 assert intake.ingested_ids == frozenset({"1"}) def test_unlabeled_issues_are_ignored() -> None: client = FakeIssueClient( [ _issue(1, labels=[INTAKE_LABEL]), _issue(2, labels=["bug"]), _issue(3, labels=[]), ] ) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) ingested = intake.poll_once() assert ingested == ["1"] assert len(coordinator.calls) == 1 assert coordinator.calls[0]["task_text"].startswith("Do the thing") def test_new_issue_on_second_poll_is_ingested() -> None: issues = [_issue(1)] client = FakeIssueClient(issues) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) first = intake.poll_once() issues.append(_issue(2, title="Second", body="task")) second = intake.poll_once() assert first == ["1"] assert second == ["2"] assert len(coordinator.calls) == 2 assert coordinator.calls[1]["task_text"] == "Second\n\ntask" def test_multiple_labeled_issues_each_create_one_task() -> None: client = FakeIssueClient([_issue(1), _issue(2), _issue(3)]) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) ingested = intake.poll_once() assert ingested == ["1", "2", "3"] assert len(coordinator.calls) == 3 def test_id_falls_back_to_number_when_id_absent() -> None: issue = {"number": 7, "title": "No id", "body": "", "labels": [INTAKE_LABEL]} client = FakeIssueClient([issue]) coordinator = FakeCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) ingested = intake.poll_once() assert ingested == ["7"] assert intake.poll_once() == [] # de-dup on number-derived id def test_failed_start_task_leaves_issue_eligible_for_retry() -> None: """A raising start_task must NOT mark the issue ingested (no silent drop).""" class FlakyCoordinator: def __init__(self) -> None: self.attempts = 0 def start_task(self, *, task_text: str, transport_name: str) -> str: self.attempts += 1 if self.attempts == 1: raise RuntimeError("transient intake failure") return "thread-ok" client = FakeIssueClient([_issue(1)]) coordinator = FlakyCoordinator() intake = GithubIntake(client=client, coordinator=coordinator, label=INTAKE_LABEL) with pytest.raises(RuntimeError): intake.poll_once() assert intake.ingested_ids == frozenset() # not recorded -> retryable # The retry succeeds and ingests the issue exactly once. ingested = intake.poll_once() assert ingested == ["1"] assert coordinator.attempts == 2