open-swe/tests/test_open_pull_request.py
Adam Moussa 8a9974c3c4
Some checks failed
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
Build & publish app artifacts / Publish + deploy (dev) (push) Has been cancelled
Build & publish app artifacts / Publish + deploy (prod) (push) Has been cancelled
Infra CD / Infra CI (pre-deploy) (push) Has been cancelled
Infra CD / Deploy open-swe-dev (push) Has been cancelled
Infra CD / Deploy open-swe-prod (push) Has been cancelled
feat: author Slack/dashboard/schedule commits + PRs as the app by default (#57) (#60)
* feat: default Slack/dashboard/schedule PRs + commits to the app identity (#57)

Slack/dashboard/schedule runs now author PRs and run git/gh operations as the
GitHub App seahaven-openswe[bot] by default (matching GitHub-issue runs), so the
self-review 422 is impossible by construction rather than guarded in the prompt.
A profile flag author_prs_as_user restores per-user attribution.

- open_pull_request._resolve_pr_author_token + auth.resolve_github_token: default
  to the installation token for these sources; per-user only when opted in.
- authorship: commit identity -> seahaven-openswe[bot] (numeric noreply;
  accepted Vercel-resolution risk, documented inline).
- self-trigger safety: INTERNAL_BOT_LOGINS + webapp/reviewer_reconcile/reply
  markers recognize seahaven-openswe[bot] (bot-authored events are now ours).

Supersedes the prompt-only guard in #58.

* fix: author commits as the app bot in the default path (SH-IDSPLIT-01)

Security review found the commit identity was NOT actually unified to the bot:
resolve_triggering_user_identity got a 403 from the installation token and fell
back to configurable['github_login'], so commits were still authored as the
triggering user (commit=user, push+PR=bot — a three-way split that missed the
stated goal). Now gate the triggering-user identity resolution on the same
default-bot decision as the token: slack/dashboard/schedule default to the app
bot identity unless author_prs_as_user is set.

* docs(security): record AUTHZ-SLACK-BOT-DEFAULT-001 as an accepted residual (#59)

Single-user deployment; bounded by App-on-pilot + ALLOWED_GITHUB_REPOS lock.
Revisit (add a per-user gate) before expanding users or the App installation.
2026-06-29 14:22:33 -04:00

431 lines
14 KiB
Python

from __future__ import annotations
import asyncio
import sys
from typing import Any
import pytest
import agent.tools.open_pull_request # noqa: F401
opr = sys.modules["agent.tools.open_pull_request"]
def _set_profile(monkeypatch: pytest.MonkeyPatch, *, author_prs_as_user: bool) -> None:
"""Mock the per-user profile lookup that gates per-user PR attribution."""
from agent.dashboard import agent_overrides
profile = {"author_prs_as_user": True} if author_prs_as_user else None
async def fake_load_profile(login: str):
return profile
monkeypatch.setattr(agent_overrides, "load_profile", fake_load_profile)
@pytest.fixture(autouse=True)
def _default_no_optin_profile(monkeypatch: pytest.MonkeyPatch) -> None:
# Default: the author_prs_as_user opt-in is OFF, so PRs author as the app bot.
_set_profile(monkeypatch, author_prs_as_user=False)
class _FakeResponse:
def __init__(self, status_code: int, payload: Any = None, text: str = "") -> None:
self.status_code = status_code
self._payload = payload
self.text = text
def json(self) -> Any:
return self._payload
class _FakeClient:
def __init__(self, *, post: _FakeResponse, get: _FakeResponse | None = None) -> None:
self._post = post
self._get = get
self.post_calls: list[dict[str, Any]] = []
self.get_calls: list[dict[str, Any]] = []
async def __aenter__(self) -> _FakeClient:
return self
async def __aexit__(self, *_exc: object) -> None:
return None
async def post(
self, url: str, *, headers: dict[str, str], json: dict[str, Any]
) -> _FakeResponse:
self.post_calls.append({"url": url, "headers": headers, "json": json})
return self._post
async def get(
self, url: str, *, headers: dict[str, str], params: dict[str, str]
) -> _FakeResponse:
self.get_calls.append({"url": url, "headers": headers, "params": params})
assert self._get is not None
return self._get
class _RoutingClient:
"""Fake httpx client that routes GETs by URL substring."""
def __init__(self, *, post: _FakeResponse, get_routes: dict[str, _FakeResponse]) -> None:
self._post = post
self._get_routes = get_routes
self.post_calls: list[dict[str, Any]] = []
self.get_calls: list[dict[str, Any]] = []
async def __aenter__(self) -> _RoutingClient:
return self
async def __aexit__(self, *_exc: object) -> None:
return None
async def post(
self, url: str, *, headers: dict[str, str], json: dict[str, Any]
) -> _FakeResponse:
self.post_calls.append({"url": url, "headers": headers, "json": json})
return self._post
async def get(
self, url: str, *, headers: dict[str, str], params: dict[str, str] | None = None
) -> _FakeResponse:
self.get_calls.append({"url": url, "headers": headers, "params": params})
for needle, resp in self._get_routes.items():
if needle in url:
return resp
raise AssertionError(f"unexpected GET {url}")
def _install_client(monkeypatch: pytest.MonkeyPatch, client: _FakeClient | _RoutingClient) -> None:
monkeypatch.setattr(opr.httpx, "AsyncClient", lambda **_kwargs: client)
def _set_config(monkeypatch: pytest.MonkeyPatch, configurable: dict[str, Any]) -> None:
monkeypatch.setattr(opr, "get_config", lambda: {"configurable": configurable})
def _open() -> dict[str, Any]:
return asyncio.run(
opr._open_pull_request(
owner="langchain-ai",
repo="open-swe",
head="open-swe/feature",
base="main",
title="feat: x",
body="body",
draft=True,
)
)
def test_defaults_to_bot_for_slack_without_optin(monkeypatch: pytest.MonkeyPatch) -> None:
# DEFAULT (no author_prs_as_user opt-in): slack PRs are opened as the app bot,
# even when a valid per-user token exists — so the PR is attributed to the app.
_set_config(monkeypatch, {"source": "slack", "github_login": "johannes117"})
from agent.dashboard import profiles
async def fail_user_token(login: str, **_kw: Any) -> str | None:
raise AssertionError("user token must not be used without the author_prs_as_user opt-in")
monkeypatch.setattr(profiles, "get_valid_access_token", fail_user_token)
async def fake_bot() -> str | None:
return "bot-tok"
monkeypatch.setattr(opr, "get_github_app_installation_token", fake_bot)
client = _FakeClient(
post=_FakeResponse(
201,
{
"html_url": "https://x/pull/1",
"number": 1,
"user": {"login": "seahaven-openswe[bot]"},
},
)
)
_install_client(monkeypatch, client)
result = _open()
assert result["token_kind"] == "bot"
assert client.post_calls[0]["headers"]["Authorization"] == "Bearer bot-tok"
def test_uses_user_token_for_slack_with_optin(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "slack", "github_login": "johannes117"})
_set_profile(monkeypatch, author_prs_as_user=True)
from agent.dashboard import profiles
async def fake_user_token(login: str, **_kw: Any) -> str | None:
assert login == "johannes117"
return "user-tok"
monkeypatch.setattr(profiles, "get_valid_access_token", fake_user_token)
async def fail_bot() -> str | None:
raise AssertionError("bot token should not be used when a user token exists")
monkeypatch.setattr(opr, "get_github_app_installation_token", fail_bot)
client = _FakeClient(
post=_FakeResponse(
201,
{"html_url": "https://x/pull/1", "number": 1, "user": {"login": "johannes117"}},
)
)
_install_client(monkeypatch, client)
result = _open()
assert result["success"] is True
assert result["created"] is True
assert result["url"] == "https://x/pull/1"
assert result["author"] == "johannes117"
assert result["token_kind"] == "user"
assert client.post_calls[0]["headers"]["Authorization"] == "Bearer user-tok"
assert client.post_calls[0]["json"] == {
"title": "feat: x",
"head": "open-swe/feature",
"base": "main",
"body": "body",
"draft": True,
}
def test_falls_back_to_bot_for_github_source(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "github", "github_login": "johannes117"})
from agent.dashboard import profiles
async def fail_user_token(login: str, **_kw: Any) -> str | None:
raise AssertionError("user token should not be resolved for github source")
monkeypatch.setattr(profiles, "get_valid_access_token", fail_user_token)
async def fake_bot() -> str | None:
return "bot-tok"
monkeypatch.setattr(opr, "get_github_app_installation_token", fake_bot)
client = _FakeClient(
post=_FakeResponse(
201, {"html_url": "https://x/pull/2", "number": 2, "user": {"login": "open-swe[bot]"}}
)
)
_install_client(monkeypatch, client)
result = _open()
assert result["token_kind"] == "bot"
assert client.post_calls[0]["headers"]["Authorization"] == "Bearer bot-tok"
def test_falls_back_to_bot_when_user_token_missing(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "slack", "github_login": "johannes117"})
from agent.dashboard import profiles
async def no_user_token(login: str, **_kw: Any) -> str | None:
return None
monkeypatch.setattr(profiles, "get_valid_access_token", no_user_token)
async def fake_bot() -> str | None:
return "bot-tok"
monkeypatch.setattr(opr, "get_github_app_installation_token", fake_bot)
client = _FakeClient(post=_FakeResponse(201, {"html_url": "u", "number": 3, "user": {}}))
_install_client(monkeypatch, client)
assert _open()["token_kind"] == "bot"
def test_returns_existing_pr_on_422(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "slack", "github_login": "johannes117"})
from agent.dashboard import profiles
monkeypatch.setattr(profiles, "get_valid_access_token", lambda *_a, **_k: _coro("user-tok"))
monkeypatch.setattr(opr, "get_github_app_installation_token", lambda: _coro("bot"))
client = _FakeClient(
post=_FakeResponse(422, text="A pull request already exists"),
get=_FakeResponse(
200, [{"html_url": "https://x/pull/9", "number": 9, "user": {"login": "johannes117"}}]
),
)
_install_client(monkeypatch, client)
result = _open()
assert result["success"] is True
assert result["created"] is False
assert result["number"] == 9
assert client.get_calls[0]["params"] == {
"head": "langchain-ai:open-swe/feature",
"state": "open",
}
def test_error_surfaced_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "slack", "github_login": "johannes117"})
from agent.dashboard import profiles
monkeypatch.setattr(profiles, "get_valid_access_token", lambda *_a, **_k: _coro("user-tok"))
monkeypatch.setattr(opr, "get_github_app_installation_token", lambda: _coro("bot"))
client = _FakeClient(post=_FakeResponse(403, text="Resource not accessible"))
_install_client(monkeypatch, client)
result = _open()
assert result["success"] is False
assert "403" in result["error"]
async def _coro(value: Any) -> Any:
return value
def _open_with_body(body: str) -> dict[str, Any]:
return asyncio.run(
opr._open_pull_request(
owner="langchain-ai",
repo="open-swe",
head="open-swe/feature",
base="main",
title="feat: x",
body=body,
draft=True,
)
)
def _stub_token(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(opr, "_resolve_pr_author_token", lambda: _coro(("tok", "user")))
def test_appends_slack_reference_for_private_repo(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(
monkeypatch,
{
"source": "slack",
"slack_thread": {"channel_id": "C123", "thread_ts": "1700000000.000100"},
},
)
_stub_token(monkeypatch)
monkeypatch.setattr(
opr, "get_slack_permalink", lambda *_a, **_k: _coro("https://slack.example/p1")
)
client = _RoutingClient(
post=_FakeResponse(201, {"html_url": "u", "number": 1, "user": {}}),
get_routes={"/repos/langchain-ai/open-swe": _FakeResponse(200, {"private": True})},
)
_install_client(monkeypatch, client)
_open_with_body("original body")
sent_body = client.post_calls[0]["json"]["body"]
assert sent_body.startswith("original body")
assert "## References" in sent_body
assert "- Slack thread: https://slack.example/p1" in sent_body
def test_no_reference_for_public_repo(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(
monkeypatch,
{
"source": "slack",
"slack_thread": {"channel_id": "C123", "thread_ts": "1700000000.000100"},
},
)
_stub_token(monkeypatch)
monkeypatch.setattr(
opr, "get_slack_permalink", lambda *_a, **_k: _coro("https://slack.example/p1")
)
client = _RoutingClient(
post=_FakeResponse(201, {"html_url": "u", "number": 1, "user": {}}),
get_routes={"/repos/langchain-ai/open-swe": _FakeResponse(200, {"private": False})},
)
_install_client(monkeypatch, client)
_open_with_body("original body")
assert client.post_calls[0]["json"]["body"] == "original body"
def test_appends_linear_reference_for_private_repo(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(
monkeypatch,
{
"source": "linear",
"linear_issue": {"url": "https://linear.app/x/AB-12", "identifier": "AB-12"},
},
)
_stub_token(monkeypatch)
client = _RoutingClient(
post=_FakeResponse(201, {"html_url": "u", "number": 1, "user": {}}),
get_routes={"/repos/langchain-ai/open-swe": _FakeResponse(200, {"private": True})},
)
_install_client(monkeypatch, client)
_open_with_body("body")
sent_body = client.post_calls[0]["json"]["body"]
assert "- Linear ticket: [AB-12](https://linear.app/x/AB-12)" in sent_body
def test_skips_append_when_no_source_context(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(monkeypatch, {"source": "slack"})
_stub_token(monkeypatch)
client = _FakeClient(post=_FakeResponse(201, {"html_url": "u", "number": 1, "user": {}}))
_install_client(monkeypatch, client)
_open_with_body("body")
assert client.post_calls[0]["json"]["body"] == "body"
assert client.get_calls == []
def test_does_not_duplicate_existing_references(monkeypatch: pytest.MonkeyPatch) -> None:
_set_config(
monkeypatch,
{
"source": "slack",
"slack_thread": {"channel_id": "C123", "thread_ts": "1700000000.000100"},
},
)
_stub_token(monkeypatch)
client = _FakeClient(post=_FakeResponse(201, {"html_url": "u", "number": 1, "user": {}}))
_install_client(monkeypatch, client)
_open_with_body("body\n\n## References\n- existing")
assert client.post_calls[0]["json"]["body"] == "body\n\n## References\n- existing"
assert client.get_calls == []
def test_derive_pr_state_prefers_merged() -> None:
assert opr.derive_pr_state(state="closed", merged=True, draft=True) == "merged"
def test_derive_pr_state_closed_over_draft() -> None:
assert opr.derive_pr_state(state="closed", merged=False, draft=True) == "closed"
def test_derive_pr_state_draft() -> None:
assert opr.derive_pr_state(state="open", merged=False, draft=True) == "draft"
def test_derive_pr_state_open() -> None:
assert opr.derive_pr_state(state="open", merged=False, draft=False) == "open"