202 lines
7.1 KiB
Python
202 lines
7.1 KiB
Python
"""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"
|