This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/tests/test_github_adapter.py
Adam Moussa 15a416d31a Add Plane-2 leaf scaffold (pipeline graph, nodes, HITL, transports, CI)
Consolidates the 18 leaf modules from the r720-plane2-scaffold workflow onto
the foundation commit. Full suite: 535 passed, 1 skipped; ruff + format clean.

Built (pre-deployment scaffold only — nothing provisioned/enabled):
- LangGraph pipeline graph.py (INTAKE->CLARIFY->PLAN, interrupt()/resume, checkpointer-injectable)
- nodes: clarifier (98% gate), planner, review_loop (GPT-4.1), builders->candidate diff, verifier
- §3.3.1 HITL: ledger ops, resume_worker, deadline_timer, recovery sweep, responder
- transports: slack / github / claude_code adapters
- ci_gate (pure-code pass/fail), operator_cli, run-team.py entry, P1 sim harness
- ci/agent-team-apply-verify.yml (split untrusted/privileged jobs) — authored, disabled

KNOWN OPEN FINDINGS (verifier/cross-review, not yet fixed — see follow-up):
- builders denylist: 4 execution-proven bypasses (delete, mode-change, copy-to, out-of-scope delete)
- §3.3.1 CAS: BEGIN IMMEDIATE outside try/except; shared-connection txn nesting unsafe under concurrency
- operator_cli: missing re-deliver/force-resume; audit-after-mutate ordering gap
- ci yaml: GPT-4.1 cross-review PASS w/ 4 FIX items (symlink path escape, etc.)
- P1 sim harness models the ledger layer, not real LangGraph interrupt/resume; P1 exit criteria not yet truly proven

Deploy-gated (NOT done): IAM/step-ca/Roles Anywhere/confluence-bot provisioning,
/sh-security-review sign-off, live Slack/CI, rsync, live dry-runs, Adam approval.
2026-06-17 15:16:12 -04:00

396 lines
12 KiB
Python

"""Unit tests for agent_team.transport.github_adapter (§3.3.1, §7.1 P4).
Fully hermetic: the HTTP transport is dependency-injected with an in-memory
fake, so no network call, token, or live infrastructure is exercised.
"""
from __future__ import annotations
import json
from typing import Any
import pytest
from agent_team.transport.base import (
GITHUB_MARKER_TEMPLATE,
QuestionSet,
Transport,
)
from agent_team.transport.github_adapter import (
GITHUB_API_ROOT,
GitHubApiError,
GitHubTransport,
build_marker,
extract_question_id,
render_question_comment,
)
# --------------------------------------------------------------------------- #
# Fakes / fixtures
# --------------------------------------------------------------------------- #
class FakeHttpPost:
"""In-memory ``HttpPost`` double recording calls and returning a scripted
``(status, data)``."""
def __init__(self, status: int = 201, data: dict[str, Any] | None = None) -> None:
self.status = status
self.data = {"id": 987654321} if data is None else data
self.calls: list[dict[str, Any]] = []
def __call__(
self,
url: str,
*,
headers: dict[str, str],
json_body: dict[str, Any],
) -> tuple[int, dict[str, Any]]:
self.calls.append({"url": url, "headers": headers, "json_body": json_body})
return self.status, self.data
def make_transport(
http_post: FakeHttpPost | None = None,
*,
token: str | None = "ghp_fake",
) -> GitHubTransport:
return GitHubTransport(
owner="Sea-Haven-Industries",
repo="agent-team",
issue_number=42,
http_post=http_post or FakeHttpPost(),
token_provider=(lambda: token),
)
def make_question_set() -> QuestionSet:
return QuestionSet(
thread_id="thread-1",
question_id="qid-abc",
turn=3,
questions=["Proceed with the migration?", "Which region?"],
context={"repo": "Sea-Haven-Industries/agent-team", "summary": "DB cutover"},
)
# --------------------------------------------------------------------------- #
# Contract / typing
# --------------------------------------------------------------------------- #
def test_is_transport_subclass() -> None:
assert issubclass(GitHubTransport, Transport)
def test_instantiable_concrete_adapter() -> None:
# Transport is abstract; the leaf must implement both abstract methods.
transport = make_transport()
assert isinstance(transport, Transport)
# --------------------------------------------------------------------------- #
# Marker helpers
# --------------------------------------------------------------------------- #
def test_build_marker_uses_foundation_template() -> None:
assert build_marker("xyz") == GITHUB_MARKER_TEMPLATE.format(question_id="xyz")
assert build_marker("xyz") == "<!-- shq:xyz -->"
def test_extract_question_id_roundtrips_build_marker() -> None:
qid = "deadbeef1234"
assert extract_question_id(build_marker(qid)) == qid
def test_extract_question_id_found_in_quoted_reply() -> None:
quoted = "> <!-- shq:q-77 -->\n> ### Agent-team needs input\n\nYes, go ahead."
assert extract_question_id(quoted) == "q-77"
def test_extract_question_id_absent_returns_none() -> None:
assert extract_question_id("just a normal comment") is None
assert extract_question_id("") is None
# --------------------------------------------------------------------------- #
# Rendering
# --------------------------------------------------------------------------- #
def test_render_embeds_marker_and_questions() -> None:
qs = make_question_set()
body = render_question_comment(
qs, question_id="qid-abc", turn=3, deadline="2026-06-18T00:00:00Z"
)
assert "<!-- shq:qid-abc -->" in body
assert extract_question_id(body) == "qid-abc"
assert "1. Proceed with the migration?" in body
assert "2. Which region?" in body
assert "turn 3" in body
assert "2026-06-18T00:00:00Z" in body
# Context surfaced.
assert "Sea-Haven-Industries/agent-team" in body
assert "DB cutover" in body
def test_render_handles_empty_questions() -> None:
qs = QuestionSet(thread_id="t", question_id="q", turn=0, questions=[])
body = render_question_comment(
qs, question_id="q", turn=0, deadline="2026-06-18T00:00:00Z"
)
assert "(no questions)" in body
assert extract_question_id(body) == "q"
# --------------------------------------------------------------------------- #
# post_question
# --------------------------------------------------------------------------- #
def test_post_question_returns_comment_id_as_channel_ref() -> None:
http = FakeHttpPost(status=201, data={"id": 555})
transport = make_transport(http)
ref = transport.post_question(
thread_id="thread-1",
question_id="qid-abc",
turn=3,
question_set=make_question_set(),
deadline="2026-06-18T00:00:00Z",
)
assert ref == "555"
assert isinstance(ref, str)
def test_post_question_targets_correct_issue_endpoint() -> None:
http = FakeHttpPost()
transport = make_transport(http)
transport.post_question(
thread_id="thread-1",
question_id="qid-abc",
turn=1,
question_set=make_question_set(),
deadline="2026-06-18T00:00:00Z",
)
(call,) = http.calls
assert call["url"] == (
f"{GITHUB_API_ROOT}/repos/Sea-Haven-Industries/agent-team/issues/42/comments"
)
def test_post_question_body_embeds_question_id() -> None:
http = FakeHttpPost()
transport = make_transport(http)
transport.post_question(
thread_id="thread-1",
question_id="qid-abc",
turn=1,
question_set=make_question_set(),
deadline="2026-06-18T00:00:00Z",
)
body = http.calls[0]["json_body"]["body"]
assert extract_question_id(body) == "qid-abc"
def test_post_question_sends_bearer_auth_header() -> None:
http = FakeHttpPost()
transport = make_transport(http, token="ghp_secret_value")
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
headers = http.calls[0]["headers"]
assert headers["Authorization"] == "Bearer ghp_secret_value"
assert headers["Accept"] == "application/vnd.github+json"
assert headers["X-GitHub-Api-Version"] == "2022-11-28"
def test_post_question_raises_on_missing_token() -> None:
transport = make_transport(FakeHttpPost(), token=None)
with pytest.raises(GitHubApiError) as exc:
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
assert exc.value.status == 401
def test_post_question_reads_token_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
http = FakeHttpPost()
monkeypatch.setenv("GITHUB_TOKEN", "ghp_from_env")
transport = GitHubTransport(owner="o", repo="r", issue_number=1, http_post=http)
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
assert http.calls[0]["headers"]["Authorization"] == "Bearer ghp_from_env"
def test_post_question_raises_on_non_2xx() -> None:
http = FakeHttpPost(status=422, data={"message": "Validation Failed"})
transport = make_transport(http)
with pytest.raises(GitHubApiError) as exc:
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
assert exc.value.status == 422
assert "Validation Failed" in exc.value.body
def test_post_question_raises_when_response_missing_id() -> None:
http = FakeHttpPost(status=201, data={"no_id": True})
transport = make_transport(http)
with pytest.raises(GitHubApiError):
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
def test_comments_url_respects_custom_api_root() -> None:
transport = GitHubTransport(
owner="o",
repo="r",
issue_number=7,
http_post=FakeHttpPost(),
api_root="https://ghe.example.com/api/v3/",
token_provider=lambda: "t",
)
assert transport.comments_url == (
"https://ghe.example.com/api/v3/repos/o/r/issues/7/comments"
)
# --------------------------------------------------------------------------- #
# parse_answer
# --------------------------------------------------------------------------- #
def test_parse_answer_full_webhook_shape() -> None:
transport = make_transport()
raw = {
"comment": {
"body": "<!-- shq:qid-abc -->\nYes, proceed with us-east-1.",
"user": {"login": "amoussa1229"},
}
}
qid, answer, via = transport.parse_answer(raw)
assert qid == "qid-abc"
assert answer == "Yes, proceed with us-east-1."
assert via == "github:amoussa1229"
def test_parse_answer_flattened_shape() -> None:
transport = make_transport()
raw = {
"body": "<!-- shq:q9 --> looks good",
"user": {"login": "adam"},
}
qid, answer, via = transport.parse_answer(raw)
assert qid == "q9"
assert answer == "looks good"
assert via == "github:adam"
def test_parse_answer_strips_quoted_marker_lines() -> None:
transport = make_transport()
raw = {
"comment": {
"body": (
"> <!-- shq:q-77 -->\n"
"> ### Agent-team needs input (turn 1)\n"
"\n"
"Approved. Use the staging bucket."
),
"user": {"login": "adam"},
}
}
qid, answer, via = transport.parse_answer(raw)
assert qid == "q-77"
assert "<!-- shq:" not in answer
assert answer.endswith("Approved. Use the staging bucket.")
def test_parse_answer_without_marker_raises() -> None:
transport = make_transport()
with pytest.raises(ValueError):
transport.parse_answer(
{"comment": {"body": "no marker here", "user": {"login": "x"}}}
)
def test_parse_answer_without_login_falls_back_to_github() -> None:
transport = make_transport()
qid, answer, via = transport.parse_answer(
{"comment": {"body": "<!-- shq:q1 -->\nok"}}
)
assert qid == "q1"
assert via == "github"
# --------------------------------------------------------------------------- #
# Round-trip: post then parse the reply maps to the same question_id
# --------------------------------------------------------------------------- #
def test_post_then_answer_roundtrip_question_id() -> None:
http = FakeHttpPost()
transport = make_transport(http)
transport.post_question(
thread_id="thread-1",
question_id="round-trip-qid",
turn=2,
question_set=make_question_set(),
deadline="2026-06-18T00:00:00Z",
)
posted_body = http.calls[0]["json_body"]["body"]
# Simulate a human quoting the posted comment in their reply.
reply_body = "\n".join(f"> {line}" for line in posted_body.splitlines())
reply_body += "\n\nYes."
qid, answer, via = transport.parse_answer(
{"comment": {"body": reply_body, "user": {"login": "adam"}}}
)
assert qid == "round-trip-qid"
assert answer == "Yes."
def test_default_http_post_is_not_called_in_tests() -> None:
# Sanity: the adapter never falls back to the network when a poster is
# injected (guards against an accidental live call in CI).
http = FakeHttpPost()
transport = make_transport(http)
transport.post_question(
thread_id="t",
question_id="q",
turn=0,
question_set=make_question_set(),
deadline="d",
)
assert len(http.calls) == 1
def test_api_error_str_is_truncated_and_typed() -> None:
err = GitHubApiError(500, "x" * 1000)
assert err.status == 500
assert isinstance(err, RuntimeError)
# __str__ truncates the body to keep logs bounded.
assert len(str(err)) < 300
assert json.dumps({"ok": True}) # keep json import meaningful/no-op