codeflash-agent/packages/github-app/tests/test_app.py
Kevin Turcios e41a1bf56a Fix conftest collision between codeflash-api and github-app test suites
Both packages had tests/__init__.py, creating competing `tests`
packages under --import-mode=importlib. Remove both __init__.py files
and change github-app imports from `from tests.helpers` to
`from helpers` via sys.path insertion in conftest.py.
2026-04-23 03:33:58 -05:00

317 lines
9.4 KiB
Python

"""Tests for the FastAPI webhook endpoint and dispatch handlers."""
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from unittest.mock import AsyncMock, patch
from helpers import sign_payload
async def test_health_check(async_client):
resp = await async_client.get("/health")
assert resp.status_code == 200
assert resp.json() == {"status": "ok"}
async def test_webhook_invalid_signature(async_client, pr_payload):
body = json.dumps(pr_payload).encode()
resp = await async_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "pull_request",
"X-Hub-Signature-256": "sha256=invalid",
"X-GitHub-Delivery": "delivery-1",
},
)
assert resp.status_code == 401
async def test_webhook_unknown_event(async_client):
payload = {"action": "test"}
body = json.dumps(payload).encode()
resp = await async_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "unknown_event",
"X-Hub-Signature-256": sign_payload(body),
"X-GitHub-Delivery": "delivery-2",
},
)
assert resp.status_code == 200
assert resp.json()["status"] == "ignored"
async def test_webhook_accepted(async_client, issue_payload, monkeypatch):
dispatched = []
async def fake_dispatch(payload, **kwargs):
dispatched.append(payload["action"])
monkeypatch.setattr(
"github_app.app.EVENT_HANDLERS",
{"issues": fake_dispatch},
)
body = json.dumps(issue_payload).encode()
resp = await async_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "issues",
"X-Hub-Signature-256": sign_payload(body),
"X-GitHub-Delivery": "delivery-3",
},
)
assert resp.status_code == 200
assert resp.json()["status"] == "accepted"
await asyncio.sleep(0.05)
assert dispatched == ["opened"]
async def test_webhook_task_tracking(async_client, issue_payload, monkeypatch):
"""Background tasks are tracked in running_tasks and cleaned up."""
from github_app.app import app
gate = asyncio.Event()
async def slow_handler(payload, **kwargs):
await gate.wait()
monkeypatch.setattr(
"github_app.app.EVENT_HANDLERS",
{"issues": slow_handler},
)
body = json.dumps(issue_payload).encode()
await async_client.post(
"/webhook",
content=body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "issues",
"X-Hub-Signature-256": sign_payload(body),
"X-GitHub-Delivery": "delivery-4",
},
)
await asyncio.sleep(0.01)
assert len(app.state.running_tasks) == 1
gate.set()
await asyncio.sleep(0.05)
assert len(app.state.running_tasks) == 0
async def test_webhook_duplicate_delivery(async_client, issue_payload, monkeypatch):
"""Duplicate delivery IDs are detected and skipped."""
dispatched = []
async def fake_dispatch(payload, **kwargs):
dispatched.append(payload["action"])
monkeypatch.setattr(
"github_app.app.EVENT_HANDLERS",
{"issues": fake_dispatch},
)
body = json.dumps(issue_payload).encode()
headers = {
"Content-Type": "application/json",
"X-GitHub-Event": "issues",
"X-Hub-Signature-256": sign_payload(body),
"X-GitHub-Delivery": "delivery-dup-test",
}
resp1 = await async_client.post("/webhook", content=body, headers=headers)
assert resp1.json()["status"] == "accepted"
await asyncio.sleep(0.05)
resp2 = await async_client.post("/webhook", content=body, headers=headers)
assert resp2.json()["status"] == "duplicate"
assert resp2.json()["delivery"] == "delivery-dup-test"
assert len(dispatched) == 1
async def test_dispatch_issues_writes_context(mock_config, issue_payload):
"""dispatch_issues writes ci-context.json and calls run_agent."""
from github_app.app import dispatch_issues
http_client = AsyncMock()
with (
patch("github_app.app.get_installation_token", return_value="tok"),
patch(
"github_app.app.clone_repo",
return_value=Path("/tmp/repo"),
),
patch("github_app.app._write_ci_context") as write_ctx_mock,
patch("github_app.app.run_agent") as run_agent_mock,
):
run_agent_mock.return_value = "done"
await dispatch_issues(
issue_payload,
config=mock_config,
http_client=http_client,
)
write_ctx_mock.assert_called_once()
ctx_arg = write_ctx_mock.call_args.args[1]
assert ctx_arg["event_type"] == "issues"
assert ctx_arg["action"] == "opened"
assert ctx_arg["number"] == 7
assert ctx_arg["owner"] == "test-owner"
assert ctx_arg["repo"] == "test-repo"
run_agent_mock.assert_called_once_with(
mock_config,
Path("/tmp/repo"),
"tok",
)
async def test_dispatch_pr_writes_context(mock_config, pr_payload):
"""dispatch_pr writes ci-context.json with PR fields."""
from github_app.app import dispatch_pr
http_client = AsyncMock()
with (
patch("github_app.app.get_installation_token", return_value="tok"),
patch(
"github_app.app.clone_repo",
return_value=Path("/tmp/repo"),
),
patch("github_app.app._write_ci_context") as write_ctx_mock,
patch("github_app.app.run_agent") as run_agent_mock,
):
run_agent_mock.return_value = "done"
await dispatch_pr(
pr_payload,
config=mock_config,
http_client=http_client,
)
write_ctx_mock.assert_called_once()
ctx_arg = write_ctx_mock.call_args.args[1]
assert ctx_arg["event_type"] == "pull_request"
assert ctx_arg["action"] == "opened"
assert ctx_arg["number"] == 42
assert ctx_arg["base_ref"] == "main"
assert ctx_arg["head_ref"] == "feature-branch"
run_agent_mock.assert_called_once()
call_kwargs = run_agent_mock.call_args.kwargs
assert call_kwargs["agent"] == "codeflash-deep"
assert "CI run triggered by PR #42" in call_kwargs["prompt"]
assert "feature-branch" in call_kwargs["prompt"]
async def test_dispatch_push_writes_context(mock_config, push_payload):
"""dispatch_push writes ci-context.json with push fields."""
from github_app.app import dispatch_push
http_client = AsyncMock()
with (
patch("github_app.app.get_installation_token", return_value="tok"),
patch(
"github_app.app.clone_repo",
return_value=Path("/tmp/repo"),
),
patch("github_app.app._write_ci_context") as write_ctx_mock,
patch("github_app.app.run_agent") as run_agent_mock,
):
run_agent_mock.return_value = "done"
await dispatch_push(
push_payload,
config=mock_config,
http_client=http_client,
)
write_ctx_mock.assert_called_once()
ctx_arg = write_ctx_mock.call_args.args[1]
assert ctx_arg["event_type"] == "push"
assert ctx_arg["action"] is None
assert ctx_arg["head_sha"] == "abc123"
assert ctx_arg["ref"] == "refs/heads/main"
run_agent_mock.assert_called_once()
async def test_dispatch_push_ignores_non_default_branch(mock_config, push_payload):
"""Push to a non-default branch is ignored."""
from github_app.app import dispatch_push
push_payload["ref"] = "refs/heads/feature-branch"
http_client = AsyncMock()
with (
patch("github_app.app.get_installation_token") as token_mock,
patch("github_app.app.run_agent") as run_agent_mock,
):
await dispatch_push(
push_payload,
config=mock_config,
http_client=http_client,
)
token_mock.assert_not_called()
run_agent_mock.assert_not_called()
async def test_dispatch_issues_ignores_irrelevant_action(mock_config, issue_payload):
"""Actions other than opened/labeled are ignored."""
from github_app.app import dispatch_issues
issue_payload["action"] = "closed"
http_client = AsyncMock()
with patch("github_app.app.run_agent") as run_agent_mock:
await dispatch_issues(
issue_payload,
config=mock_config,
http_client=http_client,
)
run_agent_mock.assert_not_called()
async def test_dispatch_pr_ignores_irrelevant_action(mock_config, pr_payload):
"""Actions other than opened/synchronize are ignored."""
from github_app.app import dispatch_pr
pr_payload["action"] = "closed"
http_client = AsyncMock()
with patch("github_app.app.run_agent") as run_agent_mock:
await dispatch_pr(
pr_payload,
config=mock_config,
http_client=http_client,
)
run_agent_mock.assert_not_called()
async def test_write_ci_context(tmp_path):
"""_write_ci_context creates the .codeflash dir and writes JSON."""
from github_app.app import _write_ci_context
_write_ci_context(str(tmp_path), {"event_type": "issues", "number": 7})
ctx_file = tmp_path / ".codeflash" / "ci-context.json"
assert ctx_file.exists()
data = json.loads(ctx_file.read_text())
assert data["event_type"] == "issues"
assert data["number"] == 7