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

122
tests/test_branch_parser.py Normal file
View File

@@ -0,0 +1,122 @@
"""Tests for branch_parser module. Written FIRST (TDD RED phase)."""
from release_agent.branch_parser import parse_branch, strip_refs_prefix
class TestStripRefsPrefix:
"""Tests for strip_refs_prefix function."""
def test_strips_refs_heads_prefix(self) -> None:
assert strip_refs_prefix("refs/heads/fix/BILL-42_something") == "fix/BILL-42_something"
def test_strips_refs_heads_prefix_feature(self) -> None:
assert strip_refs_prefix("refs/heads/feature/ALLPOST-100_add-feature") == "feature/ALLPOST-100_add-feature"
def test_no_refs_prefix_unchanged(self) -> None:
assert strip_refs_prefix("bug/ALLPOST-4229_fix-review") == "bug/ALLPOST-4229_fix-review"
def test_main_unchanged(self) -> None:
assert strip_refs_prefix("main") == "main"
def test_develop_unchanged(self) -> None:
assert strip_refs_prefix("develop") == "develop"
def test_empty_string(self) -> None:
assert strip_refs_prefix("") == ""
def test_only_refs_heads(self) -> None:
assert strip_refs_prefix("refs/heads/") == ""
def test_refs_tags_not_stripped(self) -> None:
assert strip_refs_prefix("refs/tags/v1.0.0") == "refs/tags/v1.0.0"
class TestParseBranch:
"""Tests for parse_branch function."""
def test_bug_branch_with_ticket(self) -> None:
ticket_id, has_ticket = parse_branch("bug/ALLPOST-4229_fix-review")
assert ticket_id == "ALLPOST-4229"
assert has_ticket is True
def test_feature_branch_with_ticket(self) -> None:
ticket_id, has_ticket = parse_branch("feature/ALLPOST-100_add-feature")
assert ticket_id == "ALLPOST-100"
assert has_ticket is True
def test_refs_heads_fix_branch(self) -> None:
ticket_id, has_ticket = parse_branch("refs/heads/fix/BILL-42_something")
assert ticket_id == "BILL-42"
assert has_ticket is True
def test_feat_branch_short(self) -> None:
ticket_id, has_ticket = parse_branch("feat/MY-1_x")
assert ticket_id == "MY-1"
assert has_ticket is True
def test_chore_without_ticket(self) -> None:
ticket_id, has_ticket = parse_branch("chore/update-dependencies")
assert ticket_id is None
assert has_ticket is False
def test_main_branch(self) -> None:
ticket_id, has_ticket = parse_branch("main")
assert ticket_id is None
assert has_ticket is False
def test_develop_branch(self) -> None:
ticket_id, has_ticket = parse_branch("develop")
assert ticket_id is None
assert has_ticket is False
def test_release_branch(self) -> None:
ticket_id, has_ticket = parse_branch("release/v1.0.3")
assert ticket_id is None
assert has_ticket is False
def test_returns_tuple(self) -> None:
result = parse_branch("main")
assert isinstance(result, tuple)
assert len(result) == 2
def test_ticket_id_type_when_present(self) -> None:
ticket_id, has_ticket = parse_branch("bug/ALLPOST-4229_fix-review")
assert isinstance(ticket_id, str)
assert isinstance(has_ticket, bool)
def test_ticket_id_type_when_absent(self) -> None:
ticket_id, has_ticket = parse_branch("main")
assert ticket_id is None
assert isinstance(has_ticket, bool)
def test_fix_prefix(self) -> None:
ticket_id, has_ticket = parse_branch("fix/PROJ-999_some-fix")
assert ticket_id == "PROJ-999"
assert has_ticket is True
def test_refs_heads_feature_branch(self) -> None:
ticket_id, has_ticket = parse_branch("refs/heads/feature/ALLPOST-100_add-feature")
assert ticket_id == "ALLPOST-100"
assert has_ticket is True
def test_ticket_with_multiple_digits(self) -> None:
ticket_id, has_ticket = parse_branch("feature/ABC-12345_some-long-feature")
assert ticket_id == "ABC-12345"
assert has_ticket is True
def test_branch_without_underscore_separator(self) -> None:
# Branch has ticket pattern but no underscore - still detects ticket
ticket_id, has_ticket = parse_branch("feature/PROJ-100")
assert ticket_id == "PROJ-100"
assert has_ticket is True
def test_empty_string(self) -> None:
ticket_id, has_ticket = parse_branch("")
assert ticket_id is None
assert has_ticket is False
def test_ticket_with_numeric_project_prefix(self) -> None:
ticket_id, has_ticket = parse_branch("feature/AB2-100_feature")
assert ticket_id == "AB2-100"
assert has_ticket is True