Files
billo-release-agent/tests/test_models_build.py
Yaojia Wang f5c2733cfb 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.
2026-03-24 17:38:23 +01:00

149 lines
5.8 KiB
Python

"""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)