"""Tests for devx.ci.fix_pr_title.""" from __future__ import annotations from unittest.mock import MagicMock, patch import pytest from click.testing import CliRunner from devx.ci.fix_pr_title import cli @pytest.fixture(autouse=True) def _obl_infra_prefix(monkeypatch: pytest.MonkeyPatch) -> None: """Use OBL-INFRA prefix to match infra repo conventions.""" import re monkeypatch.setattr("devx.ci.fix_pr_title.TASK_PREFIX", "OBL-INFRA") monkeypatch.setattr("devx.ci.auto_merge.TASK_PREFIX", "OBL-INFRA") monkeypatch.setattr("devx.ci.auto_merge.PR_TITLE_RE", re.compile(r"^OBL-INFRA-\d+:\s+.+")) monkeypatch.setattr("devx.ci.auto_merge._TASK_ID_PREFIX_RE", re.compile(r"^OBL-INFRA-\d+:\s*")) monkeypatch.setattr("devx.ci._shared.TASK_ID_RE", re.compile(r"OBL-INFRA-\d+")) class TestFixPrTitle: @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") @patch("devx.ci.fix_pr_title.get_vikunja_title_optional") def test_fixes_title_with_vikunja( self, mock_vikunja: MagicMock, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """PR title is updated to match task ID + Vikunja title.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 42, "title": "Fix blackbox exporter", "head": {"ref": "OBL-INFRA-458-blackbox-ipv4"}, } mock_vikunja.return_value = "Fix blackbox exporter IPv4 config" mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "42"]) assert result.exit_code == 0 mock_client.update_pr.assert_called_once_with(42, {"title": "OBL-INFRA-458: Fix blackbox exporter IPv4 config"}) @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") @patch("devx.ci.fix_pr_title.get_vikunja_title_optional") def test_strips_conventional_commit_prefix_when_no_vikunja( self, mock_vikunja: MagicMock, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """When Vikunja task not found, strips conventional-commit prefix from current title.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 10, "title": "fix: platform self-monitoring and fixes", "head": {"ref": "OBL-INFRA-456-platform-fixes"}, } mock_vikunja.return_value = None mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "10"]) assert result.exit_code == 0 mock_client.update_pr.assert_called_once_with( 10, {"title": "OBL-INFRA-456: platform self-monitoring and fixes"} ) @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") @patch("devx.ci.fix_pr_title.get_vikunja_title_optional") def test_already_correct_title_no_update( self, mock_vikunja: MagicMock, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """When PR title is already correct, no update is made.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 5, "title": "OBL-INFRA-100: Fix bug", "head": {"ref": "OBL-INFRA-100-fix-bug"}, } mock_vikunja.return_value = "Fix bug" mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "5"]) assert result.exit_code == 0 mock_client.update_pr.assert_not_called() @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") @patch("devx.ci.fix_pr_title.get_vikunja_title_optional") def test_dry_run_no_update( self, mock_vikunja: MagicMock, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """Dry run shows what would change without updating.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 7, "title": "Fix thing", "head": {"ref": "OBL-INFRA-7-fix-thing"}, } mock_vikunja.return_value = "Fix thing" mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "7", "--dry-run"]) assert result.exit_code == 0 mock_client.update_pr.assert_not_called() @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") def test_no_task_id_in_branch_exits_error( self, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """When branch has no task ID, exits with error.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 1, "title": "Some title", "head": {"ref": "just-a-branch"}, } mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "1"]) assert result.exit_code != 0 mock_client.update_pr.assert_not_called() @patch("devx.ci.fix_pr_title.get_ci_token") def test_no_token_exits_error(self, mock_ci_token: MagicMock) -> None: """When CI token is not set, exits with error.""" mock_ci_token.side_effect = Exception("no token") runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "1"]) assert result.exit_code != 0 @patch("devx.ci.fix_pr_title.get_ci_token") @patch("devx.ci.fix_pr_title.GiteaClient") @patch("devx.ci.fix_pr_title.get_vikunja_title_optional") def test_strips_task_id_prefix_from_vikunja_title( self, mock_vikunja: MagicMock, mock_gitea_cls: MagicMock, mock_ci_token: MagicMock, ) -> None: """When Vikunja title already has task ID prefix, it's stripped to avoid double prefix.""" mock_client = MagicMock() mock_gitea_cls.return_value = mock_client mock_client.get_pr.return_value = { "number": 99, "title": "Fix thing", "head": {"ref": "OBL-INFRA-99-fix-thing"}, } mock_vikunja.return_value = "OBL-INFRA-99: Fix thing" mock_ci_token.return_value = "token" runner = CliRunner() result = runner.invoke(cli, ["--repo", "oblachno/infra", "--pr-number", "99"]) assert result.exit_code == 0 mock_client.update_pr.assert_called_once_with(99, {"title": "OBL-INFRA-99: Fix thing"}) def test_invalid_repo_format_exits_error(self) -> None: """When repo is not in owner/name format, exits with error.""" runner = CliRunner() result = runner.invoke(cli, ["--repo", "invalid", "--pr-number", "1"]) assert result.exit_code != 0