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:
148
tests/test_models_build.py
Normal file
148
tests/test_models_build.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Tests for models/build.py — BuildStatus and ApprovalRecord.
|
||||
|
||||
Written FIRST (TDD RED phase).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from dataclasses import FrozenInstanceError
|
||||
|
||||
from release_agent.models.build import ApprovalRecord, BuildStatus
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BuildStatus tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBuildStatus:
|
||||
"""Tests for BuildStatus frozen dataclass."""
|
||||
|
||||
def test_can_be_created_with_all_fields(self) -> None:
|
||||
bs = BuildStatus(
|
||||
status="completed",
|
||||
result="succeeded",
|
||||
build_url="https://dev.azure.com/org/proj/_build/results?buildId=42",
|
||||
)
|
||||
assert bs.status == "completed"
|
||||
assert bs.result == "succeeded"
|
||||
assert bs.build_url == "https://dev.azure.com/org/proj/_build/results?buildId=42"
|
||||
|
||||
def test_result_can_be_none(self) -> None:
|
||||
bs = BuildStatus(
|
||||
status="inProgress",
|
||||
result=None,
|
||||
build_url="https://dev.azure.com/org/proj/_build/results?buildId=99",
|
||||
)
|
||||
assert bs.result is None
|
||||
|
||||
def test_build_url_can_be_none(self) -> None:
|
||||
bs = BuildStatus(status="notStarted", result=None, build_url=None)
|
||||
assert bs.build_url is None
|
||||
|
||||
def test_is_frozen_status(self) -> None:
|
||||
bs = BuildStatus(status="completed", result="succeeded", build_url=None)
|
||||
with pytest.raises((FrozenInstanceError, AttributeError)):
|
||||
bs.status = "inProgress" # type: ignore[misc]
|
||||
|
||||
def test_is_frozen_result(self) -> None:
|
||||
bs = BuildStatus(status="completed", result="succeeded", build_url=None)
|
||||
with pytest.raises((FrozenInstanceError, AttributeError)):
|
||||
bs.result = "failed" # type: ignore[misc]
|
||||
|
||||
def test_equality(self) -> None:
|
||||
a = BuildStatus(status="completed", result="succeeded", build_url="http://x")
|
||||
b = BuildStatus(status="completed", result="succeeded", build_url="http://x")
|
||||
assert a == b
|
||||
|
||||
def test_inequality_on_status(self) -> None:
|
||||
a = BuildStatus(status="completed", result="succeeded", build_url=None)
|
||||
b = BuildStatus(status="inProgress", result="succeeded", build_url=None)
|
||||
assert a != b
|
||||
|
||||
def test_inequality_on_result(self) -> None:
|
||||
a = BuildStatus(status="completed", result="succeeded", build_url=None)
|
||||
b = BuildStatus(status="completed", result="failed", build_url=None)
|
||||
assert a != b
|
||||
|
||||
def test_repr_contains_status(self) -> None:
|
||||
bs = BuildStatus(status="completed", result="succeeded", build_url=None)
|
||||
assert "completed" in repr(bs)
|
||||
|
||||
def test_status_values_typical(self) -> None:
|
||||
for s in ("notStarted", "inProgress", "completed", "cancelling"):
|
||||
bs = BuildStatus(status=s, result=None, build_url=None)
|
||||
assert bs.status == s
|
||||
|
||||
def test_result_values_typical(self) -> None:
|
||||
for r in ("succeeded", "failed", "canceled", "partiallySucceeded"):
|
||||
bs = BuildStatus(status="completed", result=r, build_url=None)
|
||||
assert bs.result == r
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ApprovalRecord tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestApprovalRecord:
|
||||
"""Tests for ApprovalRecord frozen dataclass."""
|
||||
|
||||
def test_can_be_created_with_all_fields(self) -> None:
|
||||
ar = ApprovalRecord(
|
||||
approval_id="approval-abc-123",
|
||||
stage_name="Sandbox",
|
||||
status="pending",
|
||||
release_id=42,
|
||||
)
|
||||
assert ar.approval_id == "approval-abc-123"
|
||||
assert ar.stage_name == "Sandbox"
|
||||
assert ar.status == "pending"
|
||||
assert ar.release_id == 42
|
||||
|
||||
def test_is_frozen_approval_id(self) -> None:
|
||||
ar = ApprovalRecord(
|
||||
approval_id="abc",
|
||||
stage_name="Sandbox",
|
||||
status="pending",
|
||||
release_id=1,
|
||||
)
|
||||
with pytest.raises((FrozenInstanceError, AttributeError)):
|
||||
ar.approval_id = "xyz" # type: ignore[misc]
|
||||
|
||||
def test_is_frozen_stage_name(self) -> None:
|
||||
ar = ApprovalRecord(
|
||||
approval_id="abc",
|
||||
stage_name="Sandbox",
|
||||
status="pending",
|
||||
release_id=1,
|
||||
)
|
||||
with pytest.raises((FrozenInstanceError, AttributeError)):
|
||||
ar.stage_name = "Production" # type: ignore[misc]
|
||||
|
||||
def test_equality(self) -> None:
|
||||
a = ApprovalRecord(approval_id="x", stage_name="S", status="pending", release_id=1)
|
||||
b = ApprovalRecord(approval_id="x", stage_name="S", status="pending", release_id=1)
|
||||
assert a == b
|
||||
|
||||
def test_inequality_on_approval_id(self) -> None:
|
||||
a = ApprovalRecord(approval_id="x", stage_name="S", status="pending", release_id=1)
|
||||
b = ApprovalRecord(approval_id="y", stage_name="S", status="pending", release_id=1)
|
||||
assert a != b
|
||||
|
||||
def test_status_pending(self) -> None:
|
||||
ar = ApprovalRecord(approval_id="a", stage_name="Stage", status="pending", release_id=10)
|
||||
assert ar.status == "pending"
|
||||
|
||||
def test_status_approved(self) -> None:
|
||||
ar = ApprovalRecord(approval_id="a", stage_name="Stage", status="approved", release_id=10)
|
||||
assert ar.status == "approved"
|
||||
|
||||
def test_status_rejected(self) -> None:
|
||||
ar = ApprovalRecord(approval_id="a", stage_name="Stage", status="rejected", release_id=10)
|
||||
assert ar.status == "rejected"
|
||||
|
||||
def test_repr_contains_stage_name(self) -> None:
|
||||
ar = ApprovalRecord(approval_id="a", stage_name="Production", status="pending", release_id=5)
|
||||
assert "Production" in repr(ar)
|
||||
|
||||
def test_release_id_is_int(self) -> None:
|
||||
ar = ApprovalRecord(approval_id="a", stage_name="S", status="pending", release_id=999)
|
||||
assert isinstance(ar.release_id, int)
|
||||
Reference in New Issue
Block a user