125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
"""Tests for the GitHub dispatcher's identity helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from app.gateway.github.identity import (
|
|
GITHUB_THREAD_NAMESPACE,
|
|
extract_target,
|
|
resolve_thread_id,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_thread_id
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_thread_id_is_deterministic() -> None:
|
|
a = resolve_thread_id("zhfeng/llm-gateway", 11, "coder")
|
|
b = resolve_thread_id("zhfeng/llm-gateway", 11, "coder")
|
|
assert a == b
|
|
|
|
|
|
def test_thread_id_differs_per_pr() -> None:
|
|
assert resolve_thread_id("zhfeng/llm-gateway", 11, "coder") != resolve_thread_id("zhfeng/llm-gateway", 12, "coder")
|
|
|
|
|
|
def test_thread_id_differs_per_repo() -> None:
|
|
assert resolve_thread_id("a/b", 1, "coder") != resolve_thread_id("c/d", 1, "coder")
|
|
|
|
|
|
def test_thread_id_differs_per_agent() -> None:
|
|
"""Different agents on the same PR must land on different threads.
|
|
|
|
This is the headline guarantee of the per-agent thread design: coder and
|
|
reviewer bound to ``owner/repo#7`` never share a LangGraph thread, so
|
|
dual-mentions can never silently drop one run via
|
|
``multitask_strategy="reject"`` and the agents' message histories stay
|
|
independent.
|
|
"""
|
|
assert resolve_thread_id("zhfeng/llm-gateway", 7, "coder") != resolve_thread_id("zhfeng/llm-gateway", 7, "reviewer")
|
|
|
|
|
|
def test_thread_id_is_uuid5_under_namespace() -> None:
|
|
repo, num, agent = "zhfeng/llm-gateway", 11, "coder"
|
|
expected = str(uuid.uuid5(GITHUB_THREAD_NAMESPACE, f"{repo}#{num}:{agent}"))
|
|
assert resolve_thread_id(repo, num, agent) == expected
|
|
# And valid UUID.
|
|
uuid.UUID(resolve_thread_id(repo, num, agent))
|
|
|
|
|
|
def test_thread_id_rejects_bad_repo() -> None:
|
|
with pytest.raises(ValueError):
|
|
resolve_thread_id("no-slash", 1, "coder")
|
|
|
|
|
|
def test_thread_id_rejects_non_int_number() -> None:
|
|
with pytest.raises(ValueError):
|
|
resolve_thread_id("a/b", "11", "coder") # type: ignore[arg-type]
|
|
|
|
|
|
def test_thread_id_rejects_empty_agent_name() -> None:
|
|
with pytest.raises(ValueError):
|
|
resolve_thread_id("a/b", 1, "")
|
|
with pytest.raises(ValueError):
|
|
resolve_thread_id("a/b", 1, " ")
|
|
with pytest.raises(ValueError):
|
|
resolve_thread_id("a/b", 1, None) # type: ignore[arg-type]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_target
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_extract_target_pull_request() -> None:
|
|
payload = {
|
|
"repository": {"full_name": "a/b"},
|
|
"pull_request": {"number": 7},
|
|
}
|
|
assert extract_target("pull_request", payload) == ("a/b", 7)
|
|
|
|
|
|
def test_extract_target_pull_request_falls_back_to_top_level_number() -> None:
|
|
# Some PR payloads put the number at the top level, not on pull_request.
|
|
payload = {"repository": {"full_name": "a/b"}, "pull_request": {}, "number": 4}
|
|
assert extract_target("pull_request", payload) == ("a/b", 4)
|
|
|
|
|
|
def test_extract_target_issue_comment() -> None:
|
|
payload = {
|
|
"repository": {"full_name": "a/b"},
|
|
"issue": {"number": 12},
|
|
"comment": {"body": "hi"},
|
|
}
|
|
assert extract_target("issue_comment", payload) == ("a/b", 12)
|
|
|
|
|
|
def test_extract_target_pull_request_review() -> None:
|
|
payload = {
|
|
"repository": {"full_name": "a/b"},
|
|
"pull_request": {"number": 5},
|
|
"review": {},
|
|
}
|
|
assert extract_target("pull_request_review", payload) == ("a/b", 5)
|
|
|
|
|
|
def test_extract_target_issues() -> None:
|
|
payload = {"repository": {"full_name": "a/b"}, "issue": {"number": 99}}
|
|
assert extract_target("issues", payload) == ("a/b", 99)
|
|
|
|
|
|
def test_extract_target_ping_returns_none() -> None:
|
|
assert extract_target("ping", {"repository": {"full_name": "a/b"}}) is None
|
|
|
|
|
|
def test_extract_target_missing_repo_returns_none() -> None:
|
|
assert extract_target("pull_request", {"pull_request": {"number": 1}}) is None
|
|
|
|
|
|
def test_extract_target_missing_number_returns_none() -> None:
|
|
assert extract_target("pull_request", {"repository": {"full_name": "a/b"}, "pull_request": {}}) is None
|