feat: initial commit — Billo Release Agent (LangGraph)

LangGraph-based release automation agent with:
- PR discovery (webhook + polling)
- AI code review via Claude Code CLI (subscription-based)
- Auto-create Jira tickets for PRs without ticket ID
- Jira ticket lifecycle management (code review -> staging -> done)
- CI/CD pipeline trigger, polling, and approval gates
- Slack interactive messages with approval buttons
- Per-repo semantic versioning
- PostgreSQL persistence (threads, staging, releases)
- FastAPI API (webhooks, approvals, status, manual triggers)
- Docker Compose deployment

1069 tests, 95%+ coverage.
This commit is contained in:
Yaojia Wang
2026-03-24 17:38:23 +01:00
commit f5c2733cfb
104 changed files with 19721 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
"""Tests for API FastAPI dependencies. Written FIRST (TDD RED phase)."""
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from release_agent.api.dependencies import (
get_db_pool,
get_graphs,
get_settings,
get_staging_store,
get_tool_clients,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_app_with_state(**state_kwargs) -> FastAPI:
"""Return a minimal FastAPI app with app.state attributes set."""
app = FastAPI()
for key, value in state_kwargs.items():
setattr(app.state, key, value)
return app
# ---------------------------------------------------------------------------
# get_settings
# ---------------------------------------------------------------------------
class TestGetSettings:
def test_returns_settings_from_state(self) -> None:
mock_settings = MagicMock()
app = _make_app_with_state(settings=mock_settings)
with TestClient(app) as client:
# We test the dependency directly by simulating a request
request = MagicMock()
request.app = app
result = get_settings(request)
assert result is mock_settings
def test_raises_when_settings_missing(self) -> None:
app = FastAPI() # no state.settings
request = MagicMock()
request.app = app
with pytest.raises(AttributeError):
get_settings(request)
# ---------------------------------------------------------------------------
# get_graphs
# ---------------------------------------------------------------------------
class TestGetGraphs:
def test_returns_graphs_from_state(self) -> None:
mock_graphs = {"pr_completed": MagicMock(), "release": MagicMock()}
app = _make_app_with_state(graphs=mock_graphs)
request = MagicMock()
request.app = app
result = get_graphs(request)
assert result is mock_graphs
def test_raises_when_graphs_missing(self) -> None:
app = FastAPI()
request = MagicMock()
request.app = app
with pytest.raises(AttributeError):
get_graphs(request)
# ---------------------------------------------------------------------------
# get_tool_clients
# ---------------------------------------------------------------------------
class TestGetToolClients:
def test_returns_tool_clients_from_state(self) -> None:
mock_clients = MagicMock()
app = _make_app_with_state(tool_clients=mock_clients)
request = MagicMock()
request.app = app
result = get_tool_clients(request)
assert result is mock_clients
def test_raises_when_tool_clients_missing(self) -> None:
app = FastAPI()
request = MagicMock()
request.app = app
with pytest.raises(AttributeError):
get_tool_clients(request)
# ---------------------------------------------------------------------------
# get_staging_store
# ---------------------------------------------------------------------------
class TestGetStagingStore:
def test_returns_staging_store_from_state(self) -> None:
mock_store = MagicMock()
app = _make_app_with_state(staging_store=mock_store)
request = MagicMock()
request.app = app
result = get_staging_store(request)
assert result is mock_store
def test_raises_when_staging_store_missing(self) -> None:
app = FastAPI()
request = MagicMock()
request.app = app
with pytest.raises(AttributeError):
get_staging_store(request)
# ---------------------------------------------------------------------------
# get_db_pool
# ---------------------------------------------------------------------------
class TestGetDbPool:
def test_returns_db_pool_from_state(self) -> None:
mock_pool = MagicMock()
app = _make_app_with_state(db_pool=mock_pool)
request = MagicMock()
request.app = app
result = get_db_pool(request)
assert result is mock_pool
def test_raises_when_db_pool_missing(self) -> None:
app = FastAPI()
request = MagicMock()
request.app = app
with pytest.raises(AttributeError):
get_db_pool(request)