open-swe/tests/e2e/fakes.py
Ramon Nogueira 747ce4bbe5
feat: add Slack breakout thread tool (#1638)
* feat: add Slack breakout thread tool

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* chore: make fake LLM scripts declarative

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: exclude slack_start_new_thread from plan mode

The breakout tool can dispatch a fresh agent run that starts outside the
current plan-mode state, bypassing the approval flow. Add it to
PLAN_MODE_EXCLUDED_TOOLS so it's hidden alongside the other mutating
tools while planning.

---------

Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-29 13:16:54 -07:00

161 lines
4.8 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]] = []
_pr_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 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]",
"files": files,
"additions": sum(f["additions"] for f in files),
"deletions": sum(f["deletions"] for f in files),
}
PULLS.append(pr)
return pr
def find_pull(number: int) -> dict[str, Any] | None:
return next((p for p in PULLS if p["number"] == number), None)
def reset() -> None:
SLACK_MESSAGES.clear()
PULLS.clear()
_pr_seq[0] = 0
seed_bare_remote()