open-swe/tests/e2e/patches.py
Ramon Nogueira a8c7af4192
test(open-swe): add Playwright E2E for the Slack → PR → web handoff (#1583)
* test(open-swe): add Playwright E2E for the Slack → PR → web handoff

Local, secrets-free end-to-end suite that drives the full happy path through mock Slack/GitHub control panels and the real dashboard UI. Only the LLM and external SaaS HTTP boundaries (GitHub/Slack APIs, OAuth token mint) are faked — the real process_slack_mention, get_agent, deepagents loop, tools, middleware, and dashboard authorization all run under `langgraph dev` with a scripted fake chat model and a local temp-dir sandbox.

- full_flow: a Slack mention runs the agent, which implements a change in the sandbox, opens a PR against a fake GitHub remote, and replies with the PR link in the same thread.
- dashboard: clicking the bot's real "Open in Web" link loads the built ui/ app (served same-origin); the thread owner can continue the conversation, while a different user sees the same thread read-only (no composer).

Wired into Agent CI as a `Playwright E2E` job that runs on pull requests.

* fix(open-swe): serve E2E UI assets via explicit route; pin Playwright

The dashboard E2E served the built ui/ SPA's /assets via app.mount(StaticFiles), but LangGraph's custom-app loader serves APIRoutes and drops sub-app Mounts, so /assets 404'd under `langgraph dev` in CI — the React app never booted and the composer/transcript never rendered. Serve assets via an explicit route instead.

Also pin @playwright/test to the latest (1.61.0) for reproducible runs, and make the owner composer assertion tolerant of either hydration state.

* test(open-swe): record Playwright trace + video on every E2E run

Capture a replayable trace (DOM snapshots, network, console, source) and a screen recording for every test, not just retries, plus a screenshot on failure. The CI job already uploads playwright-report/ and test-results/, so each run now has a downloadable replay; documented how to open it.
2026-06-22 12:54:46 -07:00

72 lines
2.7 KiB
Python

"""Boundary monkeypatches: fake the LLM and the external SaaS endpoints.
Everything patched here is an *external boundary*, not agent logic:
- the LLM (model factory) -> scripted fake
- GitHub App token mint + GitHub REST base URL -> dummy token + fake GitHub
- Slack API base URL -> fake Slack
- the api.github.com/user identity lookup -> offline (falls back to config)
Applied at import of both the graph entrypoint and the HTTP harness (same dev
process), so it runs before the first run regardless of import order. Idempotent.
"""
from __future__ import annotations
import e2e_env # noqa: F401 (sets env before any agent import)
_applied = False
def apply() -> None:
global _applied
if _applied:
return
import importlib
from agent import server
from agent.utils import auth, authorship
from agent.utils import slack as slack_utils
# NB: ``from agent.tools import open_pull_request`` returns the re-exported
# *function* (the tools package __init__ shadows the submodule), so patch the
# actual module object by name instead.
opr = importlib.import_module("agent.tools.open_pull_request")
from e2e_env import FAKE_GITHUB_API, FAKE_SLACK_API
from fake_llm import FakeScriptedChatModel, build_script
def _fake_make_model(model_id: str, **kwargs: object): # noqa: ARG001
return FakeScriptedChatModel(script=build_script())
server.make_model = _fake_make_model
async def _dummy_install_token_with_expiry() -> tuple[str, str | None]:
return "dummy-installation-token", None
async def _dummy_install_token() -> str:
return "dummy-installation-token"
auth.get_github_app_installation_token_with_expiry = _dummy_install_token_with_expiry
opr.get_github_app_installation_token = _dummy_install_token
# Point the real PR/Slack code at the in-process fakes.
opr.GITHUB_API = FAKE_GITHUB_API
slack_utils.SLACK_API_BASE_URL = FAKE_SLACK_API
# Keep the triggering-user identity lookup offline; the real fallback to
# config-derived identity (Slack name/email) still runs.
authorship._identity_from_github_token = lambda _token: None # noqa: SLF001
# OAuth-token store is an external credential boundary. Stub it so a web
# follow-up (dashboard run.start) and PR-as-user resolution have a token;
# the real ownership/authorization checks still run.
from agent.dashboard import profiles, thread_api
async def _dummy_user_token(login: str, **_kwargs: object) -> str: # noqa: ARG001
return "dummy-user-oauth-token"
profiles.get_valid_access_token = _dummy_user_token
thread_api.get_valid_access_token = _dummy_user_token
_applied = True