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
+64 -13
View File
@@ -91,9 +91,14 @@ class TestResolveTaskId:
class TestMain:
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
def test_full_flow(self, mock_client_cls: MagicMock) -> None:
def test_full_flow(
self, mock_client_cls: MagicMock, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
{"id": 267, "identifier": "DEVX-20"},
@@ -109,9 +114,14 @@ class TestMain:
mock_client.post_comment.assert_called_once()
mock_client.update_task.assert_called_once_with(267, done=True)
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
def test_no_commit_sha(self, mock_client_cls: MagicMock) -> None:
def test_no_commit_sha(
self, mock_client_cls: MagicMock, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
{"id": 267, "identifier": "DEVX-20"},
@@ -124,31 +134,47 @@ class TestMain:
args, _ = mock_client.post_comment.call_args
assert "unknown" in args[1]
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": ""}, clear=True)
def test_missing_token_exits(self) -> None:
def test_missing_token_exits(self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, ["DEVX-20: fix: bug"])
assert result.exit_code == 1
assert "VIKUNJA_TOKEN" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_no_task_id_non_release_fails(self) -> None:
def test_no_task_id_non_release_fails(
self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
"""Non-release commits without DEVX-N prefix should fail."""
runner = CliRunner()
result = runner.invoke(main, ["fix: resolve bug"])
assert result.exit_code != 0
assert "No task ID" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_release_commit_without_task_id_skips(self) -> None:
def test_release_commit_without_task_id_skips(
self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
"""Release commits without DEVX-N prefix should skip gracefully."""
runner = CliRunner()
result = runner.invoke(main, ["release: v0.3.2"])
assert result.exit_code == 0
assert "skipping" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_revert_commit_skips(self) -> None:
def test_revert_commit_skips(self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock) -> None:
"""Revert commits without DEVX-N prefix should skip gracefully."""
runner = CliRunner()
result = runner.invoke(main, ["revert: remove v0.6.0 release"])
@@ -156,17 +182,25 @@ class TestMain:
assert "Infrastructure commit" in result.output
assert "skipping" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_merge_commit_skips(self) -> None:
def test_merge_commit_skips(self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock) -> None:
"""Merge commits without DEVX-N prefix should skip gracefully."""
runner = CliRunner()
result = runner.invoke(main, ["Merge pull request #42"])
assert result.exit_code == 0
assert "Infrastructure commit" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
def test_resolve_failure_fails(self, mock_client_cls: MagicMock) -> None:
def test_resolve_failure_fails(
self, mock_client_cls: MagicMock, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
"""Missing Vikunja task is a fatal error — every PR must have a task."""
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = []
@@ -176,9 +210,14 @@ class TestMain:
assert result.exit_code != 0
assert "Could not find" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
def test_post_comment_failure_fails(self, mock_client_cls: MagicMock) -> None:
def test_post_comment_failure_fails(
self, mock_client_cls: MagicMock, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
"""Vikunja API errors should fail — the task was not updated."""
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
@@ -191,9 +230,14 @@ class TestMain:
assert result.exit_code != 0
assert "Vikunja API error" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
def test_mark_done_failure_fails(self, mock_client_cls: MagicMock) -> None:
def test_mark_done_failure_fails(
self, mock_client_cls: MagicMock, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock
) -> None:
"""Vikunja API errors should fail — the task was not updated."""
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
@@ -235,11 +279,14 @@ class TestGetGitCommitSha:
class TestFromGit:
@patch("devx.ci.post_merge.subprocess.run")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="abc123")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="DEVX-20: fix: bug")
def test_from_git(self, mock_msg: MagicMock, mock_sha: MagicMock, mock_client_cls: MagicMock) -> None:
def test_from_git(
self, mock_msg: MagicMock, mock_sha: MagicMock, mock_client_cls: MagicMock, mock_subproc: MagicMock
) -> None:
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
{"id": 267, "identifier": "DEVX-20"},
@@ -250,12 +297,13 @@ class TestFromGit:
assert result.exit_code == 0
assert "updated and marked done" in result.output
@patch("devx.ci.post_merge.subprocess.run")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("devx.ci.post_merge.VikunjaClient")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="abc123")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="DEVX-20: fix: bug")
def test_from_git_with_explicit_sha(
self, mock_msg: MagicMock, mock_sha: MagicMock, mock_client_cls: MagicMock
self, mock_msg: MagicMock, mock_sha: MagicMock, mock_client_cls: MagicMock, mock_subproc: MagicMock
) -> None:
mock_client = MagicMock()
mock_client.list_project_tasks.return_value = [
@@ -266,8 +314,11 @@ class TestFromGit:
result = runner.invoke(main, ["--from-git", "--commit-sha", "explicit_sha"])
assert result.exit_code == 0
@patch("devx.ci.post_merge.subprocess.run")
@patch("devx.ci.post_merge._get_git_commit_message", return_value="msg")
@patch("devx.ci.post_merge._get_git_commit_sha", return_value="sha")
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_no_msg_and_no_from_git(self) -> None:
def test_no_msg_and_no_from_git(self, mock_msg: MagicMock, mock_sha: MagicMock, mock_subproc: MagicMock) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
assert result.exit_code != 0