DEVX-136: feat: add fix_pr_title module and update_pr API method
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m0s

This commit was merged in pull request #203.
This commit is contained in:
2026-07-13 23:55:11 +00:00
parent 68f0872134
commit ddfbdec956
32 changed files with 2761 additions and 322 deletions
+38 -17
View File
@@ -3,7 +3,7 @@
import os
import subprocess
import tempfile
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
@@ -70,7 +70,8 @@ class TestMain:
f.write(content)
return path
def test_rejects_task_id_on_feature_branch(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_rejects_task_id_on_feature_branch(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("DEVX-19: feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="DEVX-19"):
runner = CliRunner()
@@ -78,21 +79,24 @@ class TestMain:
assert result.exit_code == 1
assert "task ID" in result.output
def test_accepts_conventional_on_feature_branch(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_accepts_conventional_on_feature_branch(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="DEVX-19"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_accepts_valid_master_commit(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_accepts_valid_master_commit(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("DEVX-19: feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_rejects_master_without_task_id(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_rejects_master_without_task_id(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("feat: add feature")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
@@ -100,7 +104,8 @@ class TestMain:
assert result.exit_code == 1
assert "task ID" in result.output
def test_rejects_master_with_non_conventional_after_task_id(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_rejects_master_with_non_conventional_after_task_id(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("DEVX-19: random message")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="master"):
runner = CliRunner()
@@ -108,7 +113,8 @@ class TestMain:
assert result.exit_code == 1
assert "conventional" in result.output
def test_rejects_non_conventional_on_feature_branch(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_rejects_non_conventional_on_feature_branch(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("random message")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="feature"):
runner = CliRunner()
@@ -116,26 +122,33 @@ class TestMain:
assert result.exit_code == 1
assert "conventional" in result.output
def test_accepts_multiline_conventional(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_accepts_multiline_conventional(self, mock_commit: MagicMock) -> None:
msg_path = self._write_msg("feat: add feature\n\nBody text.\nMore text.")
with patch("devx.ci.validate_commit_msg.get_branch", return_value="feature"):
runner = CliRunner()
result = runner.invoke(main, [msg_path])
assert result.exit_code == 0
def test_usage_message_without_args(self) -> None:
@patch("devx.ci.validate_commit_msg.get_branch")
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_usage_message_without_args(self, mock_commit: MagicMock, mock_branch: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code != 0
def test_branch_override_accepts_master_commit(self) -> None:
@patch("devx.ci.validate_commit_msg.get_branch")
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_branch_override_accepts_master_commit(self, mock_commit: MagicMock, mock_branch: MagicMock) -> None:
"""--branch master overrides branch detection (for CI use)."""
msg_path = self._write_msg("DEVX-19: feat: add feature")
runner = CliRunner()
result = runner.invoke(main, [msg_path, "--branch", "master"])
assert result.exit_code == 0
def test_branch_override_rejects_missing_task_id(self) -> None:
@patch("devx.ci.validate_commit_msg.get_branch")
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_branch_override_rejects_missing_task_id(self, mock_commit: MagicMock, mock_branch: MagicMock) -> None:
"""--branch master still enforces DEVX-N: prefix."""
msg_path = self._write_msg("feat: add feature")
runner = CliRunner()
@@ -143,7 +156,9 @@ class TestMain:
assert result.exit_code == 1
assert "task ID" in result.output
def test_branch_override_feature_accepts_conventional(self) -> None:
@patch("devx.ci.validate_commit_msg.get_branch")
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_branch_override_feature_accepts_conventional(self, mock_commit: MagicMock, mock_branch: MagicMock) -> None:
"""--branch feature still rejects DEVX-N prefix."""
msg_path = self._write_msg("DEVX-19: feat: add feature")
runner = CliRunner()
@@ -166,8 +181,9 @@ class TestCustomPrefix:
f.write(content)
return path
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_master_accepts_proj_prefix(self) -> None:
def test_master_accepts_proj_prefix(self, mock_commit: MagicMock) -> None:
"""Master branch accepts PROJ-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
@@ -188,8 +204,9 @@ class TestCustomPrefix:
importlib.reload(devx.config)
importlib.reload(vcm)
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_master_rejects_devx_prefix_when_proj_configured(self) -> None:
def test_master_rejects_devx_prefix_when_proj_configured(self, mock_commit: MagicMock) -> None:
"""Master branch rejects DEVX-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
@@ -211,8 +228,9 @@ class TestCustomPrefix:
importlib.reload(devx.config)
importlib.reload(vcm)
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
@patch.dict("os.environ", {"DEVX_TASK_PREFIX": "PROJ"})
def test_feature_branch_rejects_proj_prefix(self) -> None:
def test_feature_branch_rejects_proj_prefix(self, mock_commit: MagicMock) -> None:
"""Feature branch rejects PROJ-N: prefix when DEVX_TASK_PREFIX=GRM."""
import importlib
@@ -283,7 +301,9 @@ class TestGitMode:
result = runner.invoke(main, ["--git", "--branch", "master"])
assert result.exit_code != 0
def test_no_file_no_git_raises(self) -> None:
@patch("devx.ci.validate_commit_msg.get_branch")
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_no_file_no_git_raises(self, mock_commit: MagicMock, mock_branch: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, ["--branch", "master"])
assert result.exit_code != 0
@@ -294,7 +314,8 @@ class TestGitMode:
result = get_latest_commit_msg()
assert result == "feat: test\n\nBody"
def test_stdin_input(self) -> None:
@patch("devx.ci.validate_commit_msg.get_latest_commit_msg")
def test_stdin_input(self, mock_commit: MagicMock) -> None:
with patch("devx.ci.validate_commit_msg.get_branch", return_value="feature-branch"):
runner = CliRunner()
result = runner.invoke(main, input="feat: add feature\n", args=["-", "--branch", "feature-branch"])