Live github (issue-comment poster) and claude_code (file-drop) transports, plus GithubIntake (labeled issue -> coordinator.start_task, de-duped). run-team _build_transport now wires github/claude_code live (was SystemExit) + adds the intake-github subcommand. claude_code drop-path also neutralizes backslash (defense-in-depth).
233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
"""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
|