mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 12:03:15 +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.
187 lines
7.4 KiB
Python
187 lines
7.4 KiB
Python
"""Tests for agent.utils.repo and Linear webhook repo override behavior."""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from agent.utils.repo import extract_repo_from_text
|
|
from agent.utils.slack import extract_channel_description_text
|
|
|
|
|
|
class TestExtractRepoFromText:
|
|
def test_repo_colon_with_org(self) -> None:
|
|
result = extract_repo_from_text("please use repo:my-org/my-repo")
|
|
assert result == {"owner": "my-org", "name": "my-repo"}
|
|
|
|
def test_repo_space_with_org(self) -> None:
|
|
result = extract_repo_from_text("please use repo langchain-ai/langchainjs")
|
|
assert result == {"owner": "langchain-ai", "name": "langchainjs"}
|
|
|
|
def test_repo_colon_name_only_uses_default_owner(self) -> None:
|
|
result = extract_repo_from_text("fix bug in repo:langchainplus")
|
|
assert result == {"owner": "langchain-ai", "name": "langchainplus"}
|
|
|
|
def test_repo_space_name_only_uses_default_owner(self) -> None:
|
|
result = extract_repo_from_text("fix bug in repo open-swe")
|
|
assert result == {"owner": "langchain-ai", "name": "open-swe"}
|
|
|
|
def test_repo_name_only_custom_default_owner(self) -> None:
|
|
result = extract_repo_from_text("repo:my-repo", default_owner="custom-org")
|
|
assert result == {"owner": "custom-org", "name": "my-repo"}
|
|
|
|
def test_github_url(self) -> None:
|
|
result = extract_repo_from_text(
|
|
"check https://github.com/langchain-ai/langgraph-api please"
|
|
)
|
|
assert result == {"owner": "langchain-ai", "name": "langgraph-api"}
|
|
|
|
def test_explicit_repo_beats_github_url(self) -> None:
|
|
result = extract_repo_from_text(
|
|
"see https://github.com/langchain-ai/langgraph-api but use repo:my-org/my-repo"
|
|
)
|
|
assert result == {"owner": "my-org", "name": "my-repo"}
|
|
|
|
def test_no_repo_returns_none(self) -> None:
|
|
result = extract_repo_from_text("please fix the bug")
|
|
assert result is None
|
|
|
|
def test_empty_string_returns_none(self) -> None:
|
|
result = extract_repo_from_text("")
|
|
assert result is None
|
|
|
|
def test_trailing_slash_stripped(self) -> None:
|
|
result = extract_repo_from_text("repo:my-org/my-repo/")
|
|
assert result == {"owner": "my-org", "name": "my-repo"}
|
|
|
|
|
|
class TestExtractChannelDescriptionText:
|
|
def test_combines_topic_and_purpose(self) -> None:
|
|
channel = {
|
|
"topic": {"value": "repo:my-org/my-repo"},
|
|
"purpose": {"value": "Team channel"},
|
|
}
|
|
assert extract_channel_description_text(channel) == "repo:my-org/my-repo\nTeam channel"
|
|
|
|
def test_handles_missing_sections(self) -> None:
|
|
assert extract_channel_description_text({"topic": {"value": "hi"}}) == "hi"
|
|
|
|
def test_empty_for_none(self) -> None:
|
|
assert extract_channel_description_text(None) == ""
|
|
|
|
def test_empty_for_blank_values(self) -> None:
|
|
channel = {"topic": {"value": " "}, "purpose": {"value": ""}}
|
|
assert extract_channel_description_text(channel) == ""
|
|
|
|
def test_repo_token_extractable_from_description(self) -> None:
|
|
channel = {"topic": {"value": "Use repo:langchain-ai/open-swe here"}, "purpose": {}}
|
|
description = extract_channel_description_text(channel)
|
|
assert extract_repo_from_text(description) == {
|
|
"owner": "langchain-ai",
|
|
"name": "open-swe",
|
|
}
|
|
|
|
|
|
class TestLinearWebhookRepoOverride:
|
|
"""Test that the Linear webhook handler checks comment body for repo config first."""
|
|
|
|
@pytest.fixture()
|
|
def _base_payload(self) -> dict:
|
|
return {
|
|
"type": "Comment",
|
|
"action": "create",
|
|
"data": {
|
|
"id": "comment-123",
|
|
"body": "@openswe please fix this repo:custom-org/custom-repo",
|
|
"issue": {
|
|
"id": "issue-456",
|
|
"title": "Test issue",
|
|
},
|
|
"user": {"id": "user-1", "name": "Test User", "email": "test@test.com"},
|
|
},
|
|
}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_comment_repo_overrides_team_mapping(self, _base_payload: dict) -> None:
|
|
from agent.webhooks.linear_routes import linear_webhook
|
|
|
|
with (
|
|
patch("agent.webhooks.common.verify_linear_signature", return_value=True),
|
|
patch(
|
|
"agent.webhooks.common.fetch_linear_issue_details",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"id": "issue-456",
|
|
"title": "Test issue",
|
|
"identifier": "TEST-1",
|
|
"url": "https://linear.app/test/issue/TEST-1",
|
|
"team": {"id": "t1", "name": "Some Team", "key": "ST"},
|
|
"project": {"id": "p1", "name": "Some Project"},
|
|
"comments": {"nodes": []},
|
|
},
|
|
),
|
|
patch("agent.webhooks.common._is_repo_allowed", return_value=True),
|
|
patch("agent.webhooks.common.BackgroundTasks"),
|
|
):
|
|
mock_request = AsyncMock()
|
|
mock_request.body.return_value = json.dumps(_base_payload).encode()
|
|
mock_request.headers = {"Linear-Signature": "valid"}
|
|
|
|
bg_tasks = AsyncMock()
|
|
result = await linear_webhook(mock_request, bg_tasks)
|
|
|
|
assert result["status"] == "accepted"
|
|
assert "custom-org/custom-repo" in result["message"]
|
|
|
|
call_args = bg_tasks.add_task.call_args
|
|
repo_config = call_args[0][2]
|
|
assert repo_config == {"owner": "custom-org", "name": "custom-repo"}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_falls_back_to_team_mapping_when_no_repo_in_comment(self) -> None:
|
|
from agent.webhooks.linear_routes import linear_webhook
|
|
|
|
payload = {
|
|
"type": "Comment",
|
|
"action": "create",
|
|
"data": {
|
|
"id": "comment-123",
|
|
"body": "@openswe please fix this bug",
|
|
"issue": {
|
|
"id": "issue-456",
|
|
"title": "Test issue",
|
|
},
|
|
"user": {"id": "user-1", "name": "Test User", "email": "test@test.com"},
|
|
},
|
|
}
|
|
|
|
with (
|
|
patch("agent.webhooks.common.verify_linear_signature", return_value=True),
|
|
patch(
|
|
"agent.webhooks.common.fetch_linear_issue_details",
|
|
new_callable=AsyncMock,
|
|
return_value={
|
|
"id": "issue-456",
|
|
"title": "Test issue",
|
|
"identifier": "TEST-1",
|
|
"url": "https://linear.app/test/issue/TEST-1",
|
|
"team": {"id": "t1", "name": "Open SWE", "key": "OS"},
|
|
"project": None,
|
|
"comments": {"nodes": []},
|
|
},
|
|
),
|
|
patch("agent.webhooks.common._is_repo_allowed", return_value=True),
|
|
):
|
|
mock_request = AsyncMock()
|
|
mock_request.body.return_value = json.dumps(payload).encode()
|
|
mock_request.headers = {"Linear-Signature": "valid"}
|
|
|
|
bg_tasks = AsyncMock()
|
|
result = await linear_webhook(mock_request, bg_tasks)
|
|
|
|
assert result["status"] == "accepted"
|
|
assert "langchain-ai/open-swe" in result["message"]
|
|
|
|
call_args = bg_tasks.add_task.call_args
|
|
repo_config = call_args[0][2]
|
|
assert repo_config == {"owner": "langchain-ai", "name": "open-swe"}
|