Add 'python/robomp/' from commit '553fd1cfcf59e4c501c54fc81bc083ffd2ca007b'
git-subtree-dir: python/robomp git-subtree-mainline:4f6e70f779git-subtree-split:553fd1cfcf
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""Gated end-to-end smoke test.
|
||||
|
||||
Runs only when ROBOMP_INTEGRATION=1 and `omp` is available on PATH (or via
|
||||
ROBOMP_OMP_COMMAND). Spins up:
|
||||
|
||||
- a local bare git repo with a trivial failing test,
|
||||
- a fake GitHub API via httpx.MockTransport that records comments + PRs,
|
||||
- a real `omp --mode rpc` subprocess driven by `worker.run_task`.
|
||||
|
||||
Asserts that triage_issue produces:
|
||||
- at least one issue comment,
|
||||
- one PR matching the body template,
|
||||
- a pushed branch on the bare repo,
|
||||
- an `opened` row in sqlite.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
INTEGRATION = os.environ.get("ROBOMP_INTEGRATION") == "1"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not INTEGRATION,
|
||||
reason="ROBOMP_INTEGRATION=1 required to run the omp-backed smoke test",
|
||||
)
|
||||
|
||||
|
||||
def _git(cwd: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ | {
|
||||
"GIT_AUTHOR_NAME": "t",
|
||||
"GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@t",
|
||||
}
|
||||
return subprocess.run(["git", *args], cwd=str(cwd), check=check, capture_output=True, text=True, env=env)
|
||||
|
||||
|
||||
def _seed_failing_repo(tmp_path: Path) -> Path:
|
||||
bare = tmp_path / "upstream.git"
|
||||
bare.mkdir()
|
||||
_git(bare.parent, "init", "--initial-branch=main", "--bare", str(bare))
|
||||
seed = tmp_path / "seed"
|
||||
seed.mkdir()
|
||||
_git(seed, "init", "--initial-branch=main")
|
||||
(seed / "test.js").write_text(
|
||||
"const assert = require('assert');\n"
|
||||
"// FIXME: this assertion is wrong; the answer is 4.\n"
|
||||
"assert.strictEqual(2 + 2, 5);\n"
|
||||
)
|
||||
(seed / "README.md").write_text("toy repo\n")
|
||||
_git(seed, "add", ".")
|
||||
_git(seed, "commit", "-m", "init")
|
||||
_git(seed, "remote", "add", "origin", str(bare))
|
||||
_git(seed, "push", "origin", "main")
|
||||
return bare
|
||||
|
||||
|
||||
def test_triage_end_to_end(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from robomp.config import Settings, reset_settings_cache
|
||||
from robomp.db import Database
|
||||
from robomp.github_client import GitHubClient
|
||||
from robomp.sandbox import LocalGitTransport, SandboxManager
|
||||
from robomp.tasks import triage_issue
|
||||
|
||||
bare = _seed_failing_repo(tmp_path)
|
||||
|
||||
monkeypatch.setenv("ROBOMP_GH_PROXY_URL", "http://gh-proxy.invalid:8081")
|
||||
monkeypatch.setenv("ROBOMP_GH_PROXY_HMAC_KEY", "test-hmac-key-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
monkeypatch.setenv("GITHUB_TOKEN", "")
|
||||
monkeypatch.setenv("GITHUB_WEBHOOK_SECRET", "secret")
|
||||
monkeypatch.setenv("ROBOMP_BOT_LOGIN", "robomp-bot")
|
||||
monkeypatch.setenv("ROBOMP_REPO_ALLOWLIST", "octo/widget")
|
||||
monkeypatch.setenv("ROBOMP_WORKSPACE_ROOT", str(tmp_path / "workspaces"))
|
||||
monkeypatch.setenv("ROBOMP_SQLITE_PATH", str(tmp_path / "robomp.sqlite"))
|
||||
monkeypatch.setenv("ROBOMP_LOG_DIR", str(tmp_path / "logs"))
|
||||
monkeypatch.setenv("ROBOMP_TASK_TIMEOUT_SECONDS", "300")
|
||||
reset_settings_cache()
|
||||
cfg = Settings() # type: ignore[call-arg]
|
||||
cfg.ensure_paths()
|
||||
|
||||
# Fake GitHub: capture POSTs, serve repo/issue/comments GETs.
|
||||
comments: list[dict[str, Any]] = []
|
||||
prs: list[dict[str, Any]] = []
|
||||
next_comment_id = [100]
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
path = request.url.path
|
||||
method = request.method
|
||||
if method == "GET" and path == "/repos/octo/widget":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"full_name": "octo/widget",
|
||||
"default_branch": "main",
|
||||
"clone_url": str(bare),
|
||||
"private": False,
|
||||
},
|
||||
)
|
||||
if method == "GET" and path == "/repos/octo/widget/issues/1":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"number": 1,
|
||||
"title": "2+2 should be 4",
|
||||
"body": "Running `node test.js` exits non-zero because the assertion claims 2+2 is 5.",
|
||||
"state": "open",
|
||||
"user": {"login": "alice"},
|
||||
"labels": [],
|
||||
},
|
||||
)
|
||||
if method == "GET" and path == "/repos/octo/widget/issues/1/comments":
|
||||
return httpx.Response(200, json=comments)
|
||||
if method == "POST" and path == "/repos/octo/widget/issues/1/comments":
|
||||
body = json.loads(request.content)
|
||||
next_comment_id[0] += 1
|
||||
comment = {
|
||||
"id": next_comment_id[0],
|
||||
"user": {"login": "robomp-bot"},
|
||||
"body": body["body"],
|
||||
"created_at": "now",
|
||||
}
|
||||
comments.append(comment)
|
||||
return httpx.Response(201, json=comment)
|
||||
if method == "POST" and path == "/repos/octo/widget/pulls":
|
||||
body = json.loads(request.content)
|
||||
pr = {
|
||||
"number": 7,
|
||||
"html_url": "https://example.invalid/octo/widget/pull/7",
|
||||
"head": {"ref": body["head"]},
|
||||
"base": {"ref": body["base"]},
|
||||
"state": "open",
|
||||
"title": body["title"],
|
||||
"body": body["body"],
|
||||
}
|
||||
prs.append(pr)
|
||||
return httpx.Response(201, json=pr)
|
||||
return httpx.Response(404, json={"message": f"unmocked {method} {path}"})
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
|
||||
payload = {
|
||||
"action": "opened",
|
||||
"issue": {
|
||||
"number": 1,
|
||||
"title": "2+2 should be 4",
|
||||
"body": "Running `node test.js` exits non-zero because the assertion claims 2+2 is 5.",
|
||||
"state": "open",
|
||||
"user": {"login": "alice"},
|
||||
"labels": [],
|
||||
},
|
||||
"repository": {
|
||||
"full_name": "octo/widget",
|
||||
"default_branch": "main",
|
||||
"clone_url": str(bare),
|
||||
"private": False,
|
||||
},
|
||||
}
|
||||
|
||||
async def _go() -> None:
|
||||
db = Database(cfg.sqlite_path)
|
||||
github = GitHubClient("ghp_test", transport=transport)
|
||||
sandbox = SandboxManager(cfg.workspace_root)
|
||||
await triage_issue(
|
||||
settings=cfg,
|
||||
db=db,
|
||||
github=github,
|
||||
git_transport=LocalGitTransport(token=None),
|
||||
sandbox=sandbox,
|
||||
payload=payload,
|
||||
delivery_id="smoke-test",
|
||||
)
|
||||
row = db.get_issue("octo/widget#1")
|
||||
assert row is not None, "issue row missing"
|
||||
assert row.state in {"opened"}, f"unexpected state {row.state}"
|
||||
db.close()
|
||||
|
||||
asyncio.run(_go())
|
||||
|
||||
assert prs, "no PR opened"
|
||||
pr = prs[0]
|
||||
for section in ("## Repro", "## Cause", "## Fix", "## Verification"):
|
||||
assert section in pr["body"], f"PR body missing {section}"
|
||||
assert "Fixes #1" in pr["body"]
|
||||
# Branch should be pushed to the bare repo.
|
||||
refs = subprocess.run(
|
||||
["git", "-C", str(bare), "for-each-ref", "--format=%(refname)"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
assert any(r.startswith("refs/heads/farm/") for r in refs.stdout.splitlines()), refs.stdout
|
||||
assert comments, "expected at least one comment"
|
||||
Reference in New Issue
Block a user