DEVX-142: fix: tea CLI login failure handling, error messages, release retry

This commit is contained in:
emil User
2026-07-16 14:26:15 +00:00
parent cdf3408a35
commit a7a8637244
5 changed files with 220 additions and 18 deletions
+82 -2
View File
@@ -387,8 +387,10 @@ class TestMain:
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.publish_to_pypi")
@patch("devx.ci.publish.build_package")
@patch("time.sleep")
def test_release_failure_raises_click(
self,
mock_sleep: MagicMock,
mock_build: MagicMock,
mock_publish: MagicMock,
mock_tea_cls: MagicMock,
@@ -397,6 +399,7 @@ class TestMain:
mock_tag: MagicMock,
mock_login: MagicMock,
) -> None:
"""Release creation failure after retries raises ClickException."""
mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea.create_release.side_effect = TeaCLIError("server error")
@@ -405,6 +408,8 @@ class TestMain:
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 1
assert "Release creation failed" in result.output
# Retried 3 times (stop_after_attempt(3))
assert mock_tea.create_release.call_count == 3
@patch("devx.ci.publish.subprocess.run")
@patch("devx.ci.publish.get_latest_tag", return_value="v0.1.0")
@@ -496,8 +501,10 @@ class TestMain:
@patch("devx.ci.publish.publish_to_gitea_registry")
@patch("devx.ci.publish.publish_to_pypi")
@patch("devx.ci.publish.build_package")
@patch("time.sleep")
def test_create_release_already_exists_is_idempotent(
self,
mock_sleep: MagicMock,
mock_build: MagicMock,
mock_publish: MagicMock,
mock_gitea_pub: MagicMock,
@@ -507,7 +514,7 @@ class TestMain:
mock_tag: MagicMock,
mock_login: MagicMock,
) -> None:
"""If create_release fails with 'already exists', treat as success."""
"""If create_release fails with 'already exists', treat as success (no retry)."""
mock_tea = MagicMock()
mock_tea.list_releases.side_effect = TeaCLIError("api error")
mock_tea.create_release.side_effect = TeaCLIError("there is already a release for this tag")
@@ -516,6 +523,8 @@ class TestMain:
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code == 0
assert "already exists" in result.output
# "already exists" is caught immediately — no retry
assert mock_tea.create_release.call_count == 1
@patch("devx.ci.publish.subprocess.run")
@patch("devx.ci.publish.get_latest_tag", return_value="v0.1.0")
@@ -526,8 +535,10 @@ class TestMain:
@patch("devx.ci.publish.publish_to_gitea_registry")
@patch("devx.ci.publish.publish_to_pypi")
@patch("devx.ci.publish.build_package")
@patch("time.sleep")
def test_create_release_other_error_raises(
self,
mock_sleep: MagicMock,
mock_build: MagicMock,
mock_publish: MagicMock,
mock_gitea_pub: MagicMock,
@@ -537,7 +548,7 @@ class TestMain:
mock_tag: MagicMock,
mock_login: MagicMock,
) -> None:
"""If create_release fails with a non-'already exists' error, raise."""
"""If create_release fails with a non-'already exists' error, raise after retries."""
mock_tea = MagicMock()
mock_tea.list_releases.side_effect = TeaCLIError("api error")
mock_tea.create_release.side_effect = TeaCLIError("network error")
@@ -546,6 +557,75 @@ class TestMain:
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
assert result.exit_code != 0
assert "Release creation failed" in result.output
# Retried 3 times before giving up
assert mock_tea.create_release.call_count == 3
class TestReleaseRetry:
"""Tests for retry logic on transient release creation failures."""
@patch("devx.ci.publish.subprocess.run")
@patch("devx.ci.publish.get_latest_tag", return_value="v0.1.0")
@patch("devx.gitea_cli.configure_tea_login")
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.build_package")
@patch("time.sleep")
def test_transient_failure_retried_and_succeeds(
self,
mock_sleep: MagicMock,
mock_build: MagicMock,
mock_tea_cls: MagicMock,
mock_notes: MagicMock,
mock_run: MagicMock,
mock_tag: MagicMock,
mock_login: MagicMock,
) -> None:
"""Transient failure on first attempt succeeds on retry."""
mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea.create_release.side_effect = [
TeaCLIError("connection timeout"),
None, # second attempt succeeds
]
mock_tea_cls.return_value = mock_tea
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"])
assert result.exit_code == 0
assert "Gitea release v1.0.0 created" in result.output
assert mock_tea.create_release.call_count == 2
mock_sleep.assert_called() # slept between attempts
@patch("devx.ci.publish.subprocess.run")
@patch("devx.ci.publish.get_latest_tag", return_value="v0.1.0")
@patch("devx.gitea_cli.configure_tea_login")
@patch.dict("os.environ", {"CI_GITEA_TOKEN": "gitea-tok"})
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
@patch("devx.ci.publish.TeaCLI")
@patch("devx.ci.publish.build_package")
@patch("time.sleep")
def test_all_retries_exhausted_raises(
self,
mock_sleep: MagicMock,
mock_build: MagicMock,
mock_tea_cls: MagicMock,
mock_notes: MagicMock,
mock_run: MagicMock,
mock_tag: MagicMock,
mock_login: MagicMock,
) -> None:
"""All 3 retry attempts fail — raises ClickException."""
mock_tea = MagicMock()
mock_tea.list_releases.return_value = []
mock_tea.create_release.side_effect = TeaCLIError("503 service unavailable")
mock_tea_cls.return_value = mock_tea
runner = CliRunner()
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"])
assert result.exit_code == 1
assert "Release creation failed" in result.output
assert mock_tea.create_release.call_count == 3
assert mock_sleep.call_count == 2 # slept between 3 attempts (2 sleeps)
class TestFromTag: