"""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 pathlib import Path from typing import Any import pytest from agent_team.transport.github_intake import ( GITHUB_TRANSPORT_NAME, GithubIntake, build_ledger_ingest_store, 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 # --------------------------------------------------------------------------- # # Durable de-dup (the ledger-backed IngestStore) # --------------------------------------------------------------------------- # def test_durable_store_dedup_survives_a_fresh_intake_process(tmp_path: Path) -> None: """A new GithubIntake (= a new cron process) over the same ledger DB does not re-ingest a still-open labeled issue — the bug a scheduled timer would hit with the in-memory default.""" db = tmp_path / "ledger.sqlite" source = "github:o/r" # Process 1: ingests issue 1. store1 = build_ledger_ingest_store(db_path=db, source=source) coord1 = FakeCoordinator() intake1 = GithubIntake( client=FakeIssueClient([_issue(1)]), coordinator=coord1, label=INTAKE_LABEL, store=store1, ) assert intake1.poll_once() == ["1"] assert len(coord1.calls) == 1 # Process 2 (restart): brand-new intake + store over the SAME DB; the issue # is still open, but durable de-dup means it is NOT ingested again. store2 = build_ledger_ingest_store(db_path=db, source=source) coord2 = FakeCoordinator() intake2 = GithubIntake( client=FakeIssueClient([_issue(1)]), coordinator=coord2, label=INTAKE_LABEL, store=store2, ) assert intake2.poll_once() == [] assert coord2.calls == [] assert intake2.ingested_ids == frozenset({"1"}) def test_durable_store_namespaces_by_source(tmp_path: Path) -> None: """The same issue id under a different repo source is ingested independently.""" db = tmp_path / "ledger.sqlite" store_a = build_ledger_ingest_store(db_path=db, source="github:o/repo-a") coord_a = FakeCoordinator() GithubIntake( client=FakeIssueClient([_issue(1)]), coordinator=coord_a, label=INTAKE_LABEL, store=store_a, ).poll_once() assert len(coord_a.calls) == 1 # Different repo, same issue number → not de-duped against repo-a. store_b = build_ledger_ingest_store(db_path=db, source="github:o/repo-b") coord_b = FakeCoordinator() ingested = GithubIntake( client=FakeIssueClient([_issue(1)]), coordinator=coord_b, label=INTAKE_LABEL, store=store_b, ).poll_once() assert ingested == ["1"] assert len(coord_b.calls) == 1 def test_build_ledger_ingest_store_rejects_empty_source(tmp_path: Path) -> None: with pytest.raises(ValueError): build_ledger_ingest_store(db_path=tmp_path / "x.sqlite", source="")