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.
125 lines
4.5 KiB
Python
125 lines
4.5 KiB
Python
"""Tests for versioning module. Written FIRST (TDD RED phase)."""
|
|
|
|
import pytest
|
|
|
|
from release_agent.versioning import (
|
|
calculate_next_version,
|
|
format_version,
|
|
parse_version,
|
|
)
|
|
|
|
|
|
class TestParseVersion:
|
|
"""Tests for parse_version function."""
|
|
|
|
def test_parse_with_v_prefix(self) -> None:
|
|
assert parse_version("v1.2.3") == (1, 2, 3)
|
|
|
|
def test_parse_without_v_prefix(self) -> None:
|
|
assert parse_version("1.2.3") == (1, 2, 3)
|
|
|
|
def test_parse_zeros(self) -> None:
|
|
assert parse_version("v0.0.0") == (0, 0, 0)
|
|
|
|
def test_parse_large_numbers(self) -> None:
|
|
assert parse_version("v10.20.300") == (10, 20, 300)
|
|
|
|
def test_parse_returns_tuple_of_ints(self) -> None:
|
|
result = parse_version("v1.2.3")
|
|
assert isinstance(result, tuple)
|
|
assert len(result) == 3
|
|
assert all(isinstance(x, int) for x in result)
|
|
|
|
def test_parse_invalid_raises_value_error(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
parse_version("invalid")
|
|
|
|
def test_parse_partial_version_raises_value_error(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
parse_version("v1.2")
|
|
|
|
def test_parse_non_numeric_raises_value_error(self) -> None:
|
|
with pytest.raises(ValueError):
|
|
parse_version("va.b.c")
|
|
|
|
|
|
class TestFormatVersion:
|
|
"""Tests for format_version function."""
|
|
|
|
def test_format_basic(self) -> None:
|
|
assert format_version(1, 0, 3) == "v1.0.3"
|
|
|
|
def test_format_zeros(self) -> None:
|
|
assert format_version(0, 0, 0) == "v0.0.0"
|
|
|
|
def test_format_large_numbers(self) -> None:
|
|
assert format_version(10, 20, 300) == "v10.20.300"
|
|
|
|
def test_format_returns_string(self) -> None:
|
|
result = format_version(1, 2, 3)
|
|
assert isinstance(result, str)
|
|
|
|
def test_format_starts_with_v(self) -> None:
|
|
result = format_version(1, 2, 3)
|
|
assert result.startswith("v")
|
|
|
|
|
|
class TestCalculateNextVersion:
|
|
"""Tests for calculate_next_version function."""
|
|
|
|
def test_empty_list_returns_v1_0_0(self) -> None:
|
|
assert calculate_next_version("my-repo", []) == "v1.0.0"
|
|
|
|
def test_single_version_increments_patch(self) -> None:
|
|
assert calculate_next_version("my-repo", ["v1.0.0"]) == "v1.0.1"
|
|
|
|
def test_multiple_versions_uses_highest(self) -> None:
|
|
assert calculate_next_version("my-repo", ["v1.0.3", "v1.0.1"]) == "v1.0.4"
|
|
|
|
def test_different_major_versions(self) -> None:
|
|
assert calculate_next_version("my-repo", ["v2.1.0", "v1.9.9"]) == "v2.1.1"
|
|
|
|
def test_skips_malformed_versions(self) -> None:
|
|
assert calculate_next_version("my-repo", ["invalid", "v1.0.0"]) == "v1.0.1"
|
|
|
|
def test_all_malformed_versions_returns_v1_0_0(self) -> None:
|
|
assert calculate_next_version("my-repo", ["invalid", "bad", "nope"]) == "v1.0.0"
|
|
|
|
def test_repo_name_does_not_affect_result(self) -> None:
|
|
result_a = calculate_next_version("repo-a", ["v1.0.0"])
|
|
result_b = calculate_next_version("repo-b", ["v1.0.0"])
|
|
assert result_a == result_b
|
|
|
|
def test_versions_out_of_order(self) -> None:
|
|
assert calculate_next_version("my-repo", ["v1.0.1", "v1.0.3", "v1.0.2"]) == "v1.0.4"
|
|
|
|
def test_patch_overflow_does_not_occur(self) -> None:
|
|
# Just increments patch - no overflow logic required
|
|
result = calculate_next_version("my-repo", ["v1.0.99"])
|
|
assert result == "v1.0.100"
|
|
|
|
def test_versions_without_v_prefix_skipped(self) -> None:
|
|
# Versions without 'v' prefix are treated as malformed per spec
|
|
result = calculate_next_version("my-repo", ["1.0.0", "v2.0.0"])
|
|
assert result == "v2.0.1"
|
|
|
|
def test_result_format_starts_with_v(self) -> None:
|
|
result = calculate_next_version("my-repo", ["v1.0.0"])
|
|
assert result.startswith("v")
|
|
|
|
def test_result_has_three_parts(self) -> None:
|
|
result = calculate_next_version("my-repo", ["v1.0.0"])
|
|
parts = result[1:].split(".")
|
|
assert len(parts) == 3
|
|
assert all(p.isdigit() for p in parts)
|
|
|
|
def test_v_prefix_with_nonnumeric_parts_skipped(self) -> None:
|
|
# Starts with 'v' but is malformed - should be skipped gracefully
|
|
result = calculate_next_version("my-repo", ["va.b.c", "v1.0.0"])
|
|
assert result == "v1.0.1"
|
|
|
|
def test_v_prefix_partial_version_skipped(self) -> None:
|
|
# Starts with 'v' but only has two parts - should be skipped
|
|
result = calculate_next_version("my-repo", ["v1.0", "v2.0.0"])
|
|
assert result == "v2.0.1"
|