fix: paginate repository-label inventory for gitea_set_issue_labels (#627)

Replace single-page GET labels?limit=100 with api_get_all-backed
_repo_label_id_map so later-page labels (e.g. type:feature,
workflow-hardening) are not falsely rejected during full-set replacement.

Also add post-mutation verification, fix related MCP/CLI label inventory
call sites, and add multi-page regression tests for the #601 reconciler
failure mode.

Closes #627
This commit is contained in:
2026-07-10 13:05:27 -04:00
parent 5347b313ea
commit f32aaaa3b5
6 changed files with 447 additions and 108 deletions
+15 -24
View File
@@ -64,54 +64,45 @@ class TestMarkIssueCLI(unittest.TestCase):
with self.assertRaises(SystemExit):
mark_issue.main(["10", "bogus_action"])
@patch("mark_issue.api_get_all", return_value=[{"id": 101, "name": "status:in-progress"}])
@patch("mark_issue.api_request")
@patch("mark_issue.get_auth_header", return_value=FAKE_AUTH)
def test_successful_start(self, _auth, mock_api):
# First call is GET labels, second is POST label
mock_api.side_effect = [
[{"id": 101, "name": "status:in-progress"}],
[{"name": "status:in-progress"}],
]
def test_successful_start(self, _auth, mock_api, mock_get_all):
mock_api.return_value = [{"name": "status:in-progress"}]
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = mark_issue.main(["15", "start"])
self.assertEqual(rc, 0)
self.assertEqual(mock_api.call_count, 2)
# Verify GET labels call
get_call = mock_api.call_args_list[0]
self.assertEqual(get_call[0][0], "GET")
self.assertIn("/labels?limit=100", get_call[0][1])
mock_get_all.assert_called()
self.assertIn("/labels", mock_get_all.call_args[0][0])
# Verify POST labels call
post_call = mock_api.call_args_list[1]
post_call = mock_api.call_args_list[0]
self.assertEqual(post_call[0][0], "POST")
self.assertIn("/issues/15/labels", post_call[0][1])
self.assertEqual(post_call[0][3], {"labels": [101]})
@patch("mark_issue.api_get_all", return_value=[{"id": 101, "name": "status:in-progress"}])
@patch("mark_issue.api_request")
@patch("mark_issue.get_auth_header", return_value=FAKE_AUTH)
def test_successful_done(self, _auth, mock_api):
# First call is GET labels, second is DELETE label
mock_api.side_effect = [
[{"id": 101, "name": "status:in-progress"}],
None,
]
def test_successful_done(self, _auth, mock_api, _get_all):
mock_api.return_value = None
rc = mark_issue.main(["15", "done"])
self.assertEqual(rc, 0)
self.assertEqual(mock_api.call_count, 2)
self.assertEqual(mock_api.call_count, 1)
# Verify DELETE labels call
delete_call = mock_api.call_args_list[1]
delete_call = mock_api.call_args_list[0]
self.assertEqual(delete_call[0][0], "DELETE")
self.assertIn("/issues/15/labels/101", delete_call[0][1])
@patch("mark_issue.api_get_all", return_value=[{"id": 1, "name": "bug"}])
@patch("mark_issue.api_request")
@patch("mark_issue.get_auth_header", return_value=FAKE_AUTH)
def test_label_not_found(self, _auth, mock_api):
# GET labels returns no status:in-progress label
mock_api.return_value = [{"id": 1, "name": "bug"}]
def test_label_not_found(self, _auth, mock_api, _get_all):
# Paginated inventory returns no status:in-progress label
rc = mark_issue.main(["15", "start"])
self.assertEqual(rc, 1)
mock_api.assert_not_called()
if __name__ == "__main__":