GRM-50: fix: workflow timing, timeouts, status polling, and release classification

This commit is contained in:
2026-06-22 00:44:35 +00:00
parent 789890b9ea
commit 7591c99c02
20 changed files with 333 additions and 78 deletions
+3 -2
View File
@@ -202,16 +202,17 @@ class TestGiteaClient:
)
def test_get_commit_status(self) -> None:
"""Uses combined status endpoint (/status, not /statuses)."""
client = GiteaClient("https://git.example.com", "tok", "owner", "repo")
client._session.request = MagicMock(
return_value=_mock_response([{"context": "CI / quality", "status": "success"}])
return_value=_mock_response({"statuses": [{"context": "CI / quality", "status": "success"}]})
)
result = client.get_commit_status("abc123")
assert result == [{"context": "CI / quality", "status": "success"}]
client._session.request.assert_called_once_with(
"GET",
"https://git.example.com/repos/owner/repo/commits/abc123/statuses",
"https://git.example.com/repos/owner/repo/commits/abc123/status",
timeout=DEFAULT_TIMEOUT,
)
+2
View File
@@ -321,6 +321,8 @@ class TestWaitForCi:
assert wait_for_ci(client, "abc123", max_wait=10) is True
def test_deduplicates_by_latest(self) -> None:
"""Combined endpoint returns one entry per context; if multiple
entries appear, the last one wins (dict comprehension)."""
client = MagicMock()
client.get_commit_status.return_value = [
_status("CI / quality (pull_request)", CI_PENDING, "2026-01-01T00:00:00Z"),
+8
View File
@@ -53,6 +53,14 @@ class TestIsUserFacing:
not user-facing code. Version bumps alone should not trigger releases."""
assert is_user_facing("src/gitea_runner_manager/__init__.py") is False
def test_api_clients_is_not_user_facing(self) -> None:
"""api_clients.py is used only by CI/CD scripts, not by the GRM CLI."""
assert is_user_facing("src/gitea_runner_manager/api_clients.py") is False
def test_review_checklist_is_not_user_facing(self) -> None:
"""REVIEW_CHECKLIST.md is agent infrastructure, not user-facing."""
assert is_user_facing("REVIEW_CHECKLIST.md") is False
def test_docs_are_not_user_facing(self) -> None:
assert is_user_facing("docs/user/getting-started.md") is False
+21 -4
View File
@@ -130,12 +130,13 @@ class TestMain:
assert "VIKUNJA_TOKEN" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_no_task_id_non_release_fails(self) -> None:
"""Non-release commits without GRM-N prefix should fail."""
def test_no_task_id_non_release_warns(self) -> None:
"""Non-release commits without GRM-N prefix should warn, not fail."""
runner = CliRunner()
result = runner.invoke(main, ["fix: resolve bug"])
assert result.exit_code == 1
assert result.exit_code == 0
assert "No task ID" in result.output
assert "Skipping" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_release_commit_without_task_id_skips(self) -> None:
@@ -143,9 +144,25 @@ class TestMain:
runner = CliRunner()
result = runner.invoke(main, ["release: v0.3.2"])
assert result.exit_code == 0
assert "Release commit" in result.output
assert "skipping" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_revert_commit_skips(self) -> None:
"""Revert commits without GRM-N prefix should skip gracefully."""
runner = CliRunner()
result = runner.invoke(main, ["revert: remove v0.6.0 release"])
assert result.exit_code == 0
assert "Infrastructure commit" in result.output
assert "skipping" in result.output
@patch.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
def test_merge_commit_skips(self) -> None:
"""Merge commits without GRM-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.dict("os.environ", {"VIKUNJA_TOKEN": "tok"})
@patch("scripts.ci.post_merge.VikunjaClient")
def test_resolve_failure_propagates(self, mock_client_cls: MagicMock) -> None:
+7
View File
@@ -367,6 +367,13 @@ class TestBuildReviewBody:
body = build_review_body(result)
assert "No issues found" in body
def test_body_contains_checklist_reference(self) -> None:
"""Review body must reference REVIEW_CHECKLIST.md for manual review."""
result = ReviewResult()
body = build_review_body(result)
assert "REVIEW_CHECKLIST.md" in body
assert "--checklist-confirmed" in body
class TestRunReview:
@patch("scripts.ci.pr_review.GiteaClient")
+11
View File
@@ -302,6 +302,17 @@ class TestMain:
assert result.exit_code != 0
assert "master" in result.output
@patch.dict("os.environ", {})
@patch("scripts.ci.release.has_user_facing_changes", return_value=False)
@patch("scripts.ci.release.run_cmd")
def test_dry_run_on_non_master_warns(self, mock_run_cmd: MagicMock, mock_uf: MagicMock) -> None:
"""Dry-run mode should not fail on non-master branches."""
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="feature-branch\n", stderr="")
runner = CliRunner()
result = runner.invoke(main, ["--dry-run"])
assert result.exit_code == 0
assert "Dry-run mode" in result.output
@patch.dict("os.environ", {})
@patch("scripts.ci.release.has_user_facing_changes", return_value=True)
@patch("scripts.ci.release.has_unreleased_changes", return_value=False)
+49 -2
View File
@@ -77,14 +77,61 @@ class TestMain:
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.ci.review_pr.GiteaClient")
def test_successful_approve_review(self, mock_client_cls: MagicMock) -> None:
"""APPROVE requires --checklist-confirmed and substantive body."""
mock_client = MagicMock()
mock_client.create_review.return_value = {"id": 7}
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(main, ["5", "owner/repo", "--event", "APPROVE"])
result = runner.invoke(
main,
[
"5",
"owner/repo",
"--event",
"APPROVE",
"--checklist-confirmed",
"--body",
"All 10 checklist categories verified. Architecture OK, tests pass.",
],
)
assert result.exit_code == 0
assert "Review #7" in result.output
mock_client.create_review.assert_called_once_with("5", event="APPROVE", body="", comments=[])
mock_client.create_review.assert_called_once_with(
"5",
event="APPROVE",
body="All 10 checklist categories verified. Architecture OK, tests pass.",
comments=[],
)
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.ci.review_pr.GiteaClient")
def test_approve_without_checklist_confirmed_fails(self, mock_client_cls: MagicMock) -> None:
"""APPROVE without --checklist-confirmed is rejected."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(
main,
["5", "owner/repo", "--event", "APPROVE", "--body", "Looks good to me"],
)
assert result.exit_code != 0
assert "checklist" in result.output.lower()
mock_client.create_review.assert_not_called()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.ci.review_pr.GiteaClient")
def test_approve_with_trivial_body_fails(self, mock_client_cls: MagicMock) -> None:
"""APPROVE with trivial body (< 20 chars) and no comments is rejected."""
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
runner = CliRunner()
result = runner.invoke(
main,
["5", "owner/repo", "--event", "APPROVE", "--checklist-confirmed", "--body", "LGTM"],
)
assert result.exit_code != 0
assert "substantive" in result.output.lower()
mock_client.create_review.assert_not_called()
@patch.dict("os.environ", {"REPO_TOKEN": "tok"})
@patch("scripts.ci.review_pr.GiteaClient")