mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
Plan step C4 (docs/upstream-sync/domain-reorg/reorg-build-plan.md, approved
decisions 1-2): split the 2,590-line agent/webapp.py monolith into
agent/webhooks/common.py (shared verify/dispatch helpers), agent/api/app.py
(composition), agent/api/health.py (/health + /webhooks/run-complete), and
per-source {github,linear,slack,jira,confluence}_routes.py. Atlassian
Connect lifecycle + descriptor routes (/connect/*) fold into
confluence_routes.py; webapp.py becomes the upstream-shaped compatibility
shim (from .api.app import app). langgraph.json http.app stays
agent.webapp:app via the shim.
Fork content, upstream layout: linear/slack route files verified
content-identical to upstream 8356eb34 and taken verbatim; github_routes is
upstream + the fork's CI auto-fix trigger wiring; jira/confluence routes are
fork-only, transformed to the same common.X / service.X module-attribute
style. All signature verification (GitHub HMAC, Slack, Linear
timestamp-freshness, verify_jira_secret + opt-in HMAC/timestamp/IP
allowlist, Connect JWT/qsh), token-attribution gating, TID-COLLIDE-01 repo
binding, _is_repo_auto_review_enabled gates, and public-repo org gate move
unchanged.
Handlers rewired from webapp.X to common.X; test monkeypatch sites across
26 files + conftest.py + e2e/harness.py retargeted to
webhook_common/handler/route modules per upstream's pattern. Residual
agent.webapp importers: only the shim, langgraph.json http.app, Makefile
uvicorn target, and docs (doc-path updates land in C7).
Gates: ruff check + format, pytest --co, full unit (1637 passed), full
Playwright E2E vs real langgraph dev (9/9), residual-importer sweep.
196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
"""Tests for Jira webhook PR author linking and repo-mapping cascade."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from agent.webhooks import common as webhook_common
|
|
from agent.webhooks import jira as jira_webhook
|
|
|
|
|
|
def _full_issue(*, title: str = "Fix the flaky test") -> dict:
|
|
return {
|
|
"key": "PROJ-42",
|
|
"id": "10001",
|
|
"title": title,
|
|
"description": "Do the thing",
|
|
"url": "https://seahaven.atlassian.net/browse/PROJ-42",
|
|
"project_key": "PROJ",
|
|
}
|
|
|
|
|
|
def _issue_data(*, account_id: str | None, email: str | None, name: str = "Jane") -> dict:
|
|
# jira_webhook resolves account_id -> email once and attaches it to
|
|
# comment_author before dispatch (see jira_routes.jira_webhook).
|
|
return {
|
|
"key": "PROJ-42",
|
|
"project_key": "PROJ",
|
|
"triggering_comment": "@openswe fix this",
|
|
"triggering_comment_id": "10050",
|
|
"comment_author": {"account_id": account_id, "email": email, "name": name},
|
|
}
|
|
|
|
|
|
def _run_process(
|
|
issue_data: dict,
|
|
repo_config: dict[str, str],
|
|
*,
|
|
active_logins: set[str] | None = None,
|
|
) -> tuple[dict, dict, str | None]:
|
|
captured: dict[str, Any] = {}
|
|
active = {"jane"} if active_logins is None else active_logins
|
|
|
|
async def fake_dispatch(
|
|
thread_id, content, configurable, *, source, metadata=None, client=None
|
|
):
|
|
captured["configurable"] = configurable
|
|
return {"run_id": "run-1"}
|
|
|
|
async def fake_upsert(
|
|
thread_id,
|
|
*,
|
|
source,
|
|
repo_config=None,
|
|
github_login="",
|
|
user_email="",
|
|
title="",
|
|
source_context=None,
|
|
):
|
|
captured["upsert"] = {"github_login": github_login, "user_email": user_email}
|
|
return None
|
|
|
|
async def fake_resolve_login(email):
|
|
captured["resolved_email"] = email
|
|
return "jane" if email == "jane@example.com" else None
|
|
|
|
with (
|
|
patch.object(
|
|
webhook_common,
|
|
"generate_thread_id_from_jira_issue",
|
|
return_value="thread-1",
|
|
),
|
|
patch.object(
|
|
webhook_common,
|
|
"fetch_jira_issue_details",
|
|
new_callable=AsyncMock,
|
|
return_value=_full_issue(),
|
|
),
|
|
patch.object(
|
|
webhook_common,
|
|
"fetch_jira_issue_comments",
|
|
new_callable=AsyncMock,
|
|
return_value=[],
|
|
),
|
|
patch.object(
|
|
webhook_common,
|
|
"resolve_login_from_email_async",
|
|
side_effect=fake_resolve_login,
|
|
),
|
|
patch.object(
|
|
webhook_common,
|
|
"is_login_mapped",
|
|
side_effect=lambda login: login in active,
|
|
),
|
|
patch.object(webhook_common, "dispatch_agent_run", side_effect=fake_dispatch),
|
|
patch.object(
|
|
webhook_common,
|
|
"upsert_agent_thread_owner_metadata",
|
|
side_effect=fake_upsert,
|
|
),
|
|
patch.object(webhook_common, "post_jira_trace_comment", new_callable=AsyncMock),
|
|
):
|
|
asyncio.run(jira_webhook.process_jira_issue(issue_data, repo_config))
|
|
|
|
return (
|
|
captured.get("configurable", {}),
|
|
captured.get("upsert", {}),
|
|
captured.get("resolved_email"),
|
|
)
|
|
|
|
|
|
def test_jira_configurable_carries_github_login() -> None:
|
|
configurable, _upsert, resolved_email = _run_process(
|
|
_issue_data(account_id="acc-1", email="jane@example.com"),
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
|
|
assert resolved_email == "jane@example.com"
|
|
assert configurable["source"] == "jira"
|
|
assert configurable["github_login"] == "jane"
|
|
assert configurable["user_email"] == "jane@example.com"
|
|
assert configurable["jira_issue"]["key"] == "PROJ-42"
|
|
assert configurable["jira_issue"]["project_key"] == "PROJ"
|
|
assert configurable["jira_issue"]["issue_number"] == "42"
|
|
|
|
|
|
def test_jira_upsert_tags_thread_with_login() -> None:
|
|
_configurable, upsert, _email = _run_process(
|
|
_issue_data(account_id="acc-1", email="jane@example.com"),
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
|
|
assert upsert["github_login"] == "jane"
|
|
assert upsert["user_email"] == "jane@example.com"
|
|
|
|
|
|
def test_jira_omits_login_when_unmapped() -> None:
|
|
configurable, upsert, resolved_email = _run_process(
|
|
_issue_data(account_id="acc-2", email="nobody@example.com"),
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
|
|
assert resolved_email == "nobody@example.com"
|
|
assert "github_login" not in configurable
|
|
assert upsert["github_login"] == ""
|
|
|
|
|
|
def test_jira_omits_login_when_mapping_not_active() -> None:
|
|
# A resolvable email whose mapping is pending/inactive must not be attributed.
|
|
configurable, upsert, resolved_email = _run_process(
|
|
_issue_data(account_id="acc-1", email="jane@example.com"),
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
active_logins=set(),
|
|
)
|
|
|
|
assert resolved_email == "jane@example.com"
|
|
assert "github_login" not in configurable
|
|
assert upsert["github_login"] == ""
|
|
|
|
|
|
def test_jira_omits_login_when_no_email_resolved() -> None:
|
|
configurable, upsert, resolved_email = _run_process(
|
|
_issue_data(account_id=None, email=None),
|
|
{"owner": "langchain-ai", "name": "open-swe"},
|
|
)
|
|
|
|
assert resolved_email is None
|
|
assert "github_login" not in configurable
|
|
assert upsert["github_login"] == ""
|
|
|
|
|
|
def test_repo_cascade_uses_project_mapping(monkeypatch) -> None:
|
|
monkeypatch.setattr(
|
|
webhook_common, "JIRA_PROJECT_TO_REPO", {"PROJ": {"owner": "acme", "name": "widgets"}}
|
|
)
|
|
assert webhook_common.get_repo_config_from_jira_mapping("PROJ") == {
|
|
"owner": "acme",
|
|
"name": "widgets",
|
|
}
|
|
|
|
|
|
def test_repo_cascade_falls_back_to_default_repo(monkeypatch) -> None:
|
|
monkeypatch.setattr(webhook_common, "JIRA_PROJECT_TO_REPO", {})
|
|
monkeypatch.setattr(webhook_common, "DEFAULT_REPO_OWNER", "langchain-ai")
|
|
monkeypatch.setattr(webhook_common, "DEFAULT_REPO_NAME", "open-swe")
|
|
assert webhook_common.get_repo_config_from_jira_mapping("UNKNOWN") == {
|
|
"owner": "langchain-ai",
|
|
"name": "open-swe",
|
|
}
|
|
|
|
|
|
def test_repo_cascade_empty_without_default(monkeypatch) -> None:
|
|
monkeypatch.setattr(webhook_common, "JIRA_PROJECT_TO_REPO", {})
|
|
monkeypatch.setattr(webhook_common, "DEFAULT_REPO_NAME", "")
|
|
assert webhook_common.get_repo_config_from_jira_mapping("UNKNOWN") == {}
|