open-swe/tests/e2e/fakes.py

221 lines
6.7 KiB
Python

"""In-memory state + git plumbing behind the fake GitHub and fake Slack.
These stores are the single source of truth that both the real agent code
(via the faked HTTP endpoints) and the mock UIs read from — so what Playwright
sees in the UI is exactly what the agent produced.
"""
from __future__ import annotations
import shutil
import subprocess
import time
from pathlib import Path
from typing import Any
from e2e_env import BARE_REMOTE, BASE_BRANCH, OWNER, REPO
# --- Slack -----------------------------------------------------------------
# (channel, thread_ts) -> list of {user, text, ts, blocks, is_bot}
SLACK_MESSAGES: dict[tuple[str, str], list[dict[str, Any]]] = {}
_slack_seq = [1]
def next_slack_ts() -> str:
_slack_seq[0] += 1
return f"1700000000.{_slack_seq[0]:06d}"
_thread_seq = [0]
def new_thread_ts() -> str:
"""A globally-unique thread ts so every send maps to a fresh LangGraph thread
(the in-mem store persists across restarts, so reused ids would carry state).
Not reset by reset(), so back-to-back tests never collide."""
_thread_seq[0] += 1
return f"{int(time.time())}.{_thread_seq[0]:06d}"
def add_slack_message(
channel: str, thread_ts: str, *, user: str, text: str, blocks: Any = None, is_bot: bool = False
) -> str:
ts = next_slack_ts()
actual_thread_ts = thread_ts or ts
SLACK_MESSAGES.setdefault((channel, actual_thread_ts), []).append(
{
"user": user,
"text": text,
"ts": ts,
"thread_ts": actual_thread_ts,
"blocks": blocks,
"is_bot": is_bot,
}
)
return ts
def slack_thread(channel: str, thread_ts: str) -> list[dict[str, Any]]:
return SLACK_MESSAGES.get((channel, thread_ts), [])
def slack_messages(channel: str) -> list[dict[str, Any]]:
messages: list[dict[str, Any]] = []
for (message_channel, _thread_ts), thread_messages in SLACK_MESSAGES.items():
if message_channel == channel:
messages.extend(thread_messages)
return sorted(messages, key=lambda message: message["ts"])
# --- GitHub ----------------------------------------------------------------
PULLS: list[dict[str, Any]] = []
CHECK_RUNS: list[dict[str, Any]] = []
REVIEW_DISPATCHES: list[dict[str, Any]] = []
EMAIL_MAPPING_LOOKUPS: list[str] = []
_pr_seq = [0]
_check_seq = [0]
def _git(*args: str, cwd: Path | None = None) -> str:
result = subprocess.run(
["git", *args],
cwd=str(cwd) if cwd else None,
capture_output=True,
text=True,
check=True,
)
return result.stdout
def seed_bare_remote() -> None:
"""Create a fresh bare repo (the fake GitHub remote) with one commit on main."""
if BARE_REMOTE.exists():
shutil.rmtree(BARE_REMOTE)
seed_work = BARE_REMOTE.parent / f"seed-{OWNER}-{REPO}"
if seed_work.exists():
shutil.rmtree(seed_work)
seed_work.mkdir(parents=True)
ident = ["-c", "user.email=seed@example.com", "-c", "user.name=Seed"]
_git("init", "-b", BASE_BRANCH, str(seed_work))
(seed_work / "README.md").write_text("# demo\n\nA tiny demo repo.\n")
_git("add", "-A", cwd=seed_work)
_git(*ident, "commit", "-m", "Initial commit", cwd=seed_work)
_git("init", "--bare", "-b", BASE_BRANCH, str(BARE_REMOTE))
_git("remote", "add", "origin", str(BARE_REMOTE), cwd=seed_work)
_git("push", "origin", BASE_BRANCH, cwd=seed_work)
shutil.rmtree(seed_work)
def _diff_files(base: str, head: str) -> list[dict[str, Any]]:
"""Compute changed files for a PR from the pushed branch in the bare remote."""
try:
out = _git("--git-dir", str(BARE_REMOTE), "diff", "--numstat", base, head)
except subprocess.CalledProcessError:
return []
files = []
for line in out.splitlines():
parts = line.split("\t")
if len(parts) == 3:
adds, dels, name = parts
files.append(
{
"filename": name,
"additions": int(adds) if adds.isdigit() else 0,
"deletions": int(dels) if dels.isdigit() else 0,
}
)
return files
def branch_exists(branch: str) -> bool:
"""Check whether a branch exists in the bare remote (the fake GitHub)."""
try:
_git("--git-dir", str(BARE_REMOTE), "rev-parse", "--verify", f"refs/heads/{branch}")
return True
except subprocess.CalledProcessError:
return False
def create_pull(
owner: str, repo: str, *, head: str, base: str, title: str, body: str, draft: bool
) -> dict[str, Any]:
_pr_seq[0] += 1
number = _pr_seq[0]
files = _diff_files(base, head)
pr = {
"number": number,
"owner": owner,
"repo": repo,
"head": head,
"base": base,
"title": title,
"body": body,
"draft": draft,
"state": "open",
"merged": False,
"author": "open-swe[bot]",
"head_sha": f"head-{number:04d}",
"base_sha": f"base-{number:04d}",
"files": files,
"additions": sum(f["additions"] for f in files),
"deletions": sum(f["deletions"] for f in files),
}
PULLS.append(pr)
return pr
def create_review_pull(owner: str, repo: str) -> dict[str, Any]:
pr = create_pull(
owner,
repo,
head="feature/review-me",
base=BASE_BRANCH,
title="Review command fixture",
body="A deterministic pull request for reviewer routing.",
draft=False,
)
pr["author"] = "alice"
return pr
def find_pull(number: int) -> dict[str, Any] | None:
return next((p for p in PULLS if p["number"] == number), None)
def create_check_run(owner: str, repo: str, payload: dict[str, Any]) -> dict[str, Any]:
_check_seq[0] += 1
check = {
"id": _check_seq[0],
"owner": owner,
"repo": repo,
"name": payload.get("name"),
"head_sha": payload.get("head_sha"),
"status": payload.get("status"),
"conclusion": payload.get("conclusion"),
"details_url": payload.get("details_url"),
"output": payload.get("output", {}),
}
CHECK_RUNS.append(check)
return check
def update_check_run(check_run_id: int, payload: dict[str, Any]) -> dict[str, Any] | None:
check = next((item for item in CHECK_RUNS if item["id"] == check_run_id), None)
if check is None:
return None
check.update(
{key: payload[key] for key in ("status", "conclusion", "output") if key in payload}
)
return check
def reset() -> None:
SLACK_MESSAGES.clear()
PULLS.clear()
CHECK_RUNS.clear()
REVIEW_DISPATCHES.clear()
EMAIL_MAPPING_LOOKUPS.clear()
_pr_seq[0] = 0
_check_seq[0] = 0
seed_bare_remote()