@openhands/extensions 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/custom-codereview-guide.md +44 -1
- package/.claude-plugin/marketplace.json +7 -5
- package/.codex-plugin/marketplace.json +7 -5
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/check-extensions.yml +1 -1
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +7 -5
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -3
- package/apps/vscode/README.md +8 -6
- package/apps/vscode/canvas-extension.json +11 -3
- package/apps/vscode/extension.js +60 -12
- package/apps/vscode/scripts/package.mjs +20 -74
- package/automations/bundle-index.js +9 -7
- package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
- package/automations/catalog/github-issue-triage/manifest.json +4 -3
- package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
- package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
- package/integrations/catalog/atlassian-rovo.json +4 -3
- package/integrations/catalog/bitbucket.json +16 -1
- package/integrations/catalog/github.json +1 -0
- package/integrations/catalog/gitlab.json +1 -1
- package/marketplaces/openhands-extensions.json +7 -5
- package/package.json +1 -1
- package/plugins/pr-review/README.md +1 -0
- package/plugins/pr-review/scripts/prompt.py +44 -11
- package/plugins/runtime-api-configs/SKILL.md +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/canvas-extension-api/references/backend-safety.md +3 -3
- package/skills/canvas-extension-api/references/v1-contract.md +7 -4
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
- package/skills/github/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/README.md +73 -14
- package/skills/github-issue-triage/SKILL.md +26 -46
- package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
- package/skills/github-issue-triage/scripts/worker.py +153 -103
- package/skills/github-pr-reviewer/README.md +103 -1
- package/skills/github-pr-reviewer/SKILL.md +35 -8
- package/skills/github-pr-reviewer/references/state-schema.md +8 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
- package/skills/github-pr-reviewer/scripts/main.py +163 -39
- package/skills/github-pr-reviewer/scripts/publication.py +193 -0
- package/skills/github-pr-reviewer/scripts/worker.py +297 -8
- package/skills/github-pr-reviewer/tests/test_main.py +73 -5
- package/skills/gitlab-issue-to-mr/README.md +10 -3
- package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
- package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
- package/skills/index.js +8 -8
- package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/README.md +18 -11
- package/skills/jira-issue-to-pr/SKILL.md +72 -23
- package/skills/jira-issue-to-pr/references/setup.md +43 -12
- package/skills/jira-issue-to-pr/scripts/main.py +136 -26
- package/skills/slack-channel-monitor/README.md +6 -1
- package/skills/slack-channel-monitor/SKILL.md +6 -3
- package/skills/slack-channel-monitor/scripts/main.py +36 -4
- package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/SKILL.md +5 -3
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +173 -6
- package/tests/test_automation_llm_provenance.py +1209 -0
- package/tests/test_github_review_publication.py +422 -0
- package/tests/test_github_reviewer_delivery.py +174 -21
- package/tests/test_github_triage_delivery.py +411 -238
- package/tests/test_gitlab_issue_to_mr.py +284 -0
- package/tests/test_jira_issue_to_pr.py +200 -6
- package/tests/test_pr_review_prompt_injection.py +353 -0
- package/tests/test_slack_channel_monitor.py +32 -1
- package/tests/test_vscode_app.py +8 -2
- package/tests/vscode-app.test.mjs +8 -2
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from unittest.mock import MagicMock, Mock
|
|
7
|
+
from urllib.error import HTTPError
|
|
7
8
|
from uuid import NAMESPACE_URL, uuid5
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
@@ -164,7 +165,7 @@ def test_reviewer_submits_each_labeled_exact_head(tmp_path, monkeypatch):
|
|
|
164
165
|
|
|
165
166
|
assert submit.call_args.kwargs["subject"] == "99:pr:2"
|
|
166
167
|
assert submit.call_args.kwargs["delivery"] == "7:head-2"
|
|
167
|
-
prompt = submit.call_args.kwargs["prompt"]
|
|
168
|
+
prompt = submit.call_args.kwargs["prompt"]("conversation")
|
|
168
169
|
assert "publish the review directly to GitHub" in prompt
|
|
169
170
|
assert "Do not create commit statuses or Checks" in prompt
|
|
170
171
|
assert "FACTORY_GITHUB_REVIEWER_TOKEN" in prompt
|
|
@@ -203,7 +204,7 @@ def test_reviewer_prompt_forbids_republishing_an_unchanged_reviewed_head(
|
|
|
203
204
|
|
|
204
205
|
run.run()
|
|
205
206
|
|
|
206
|
-
prompt = submit.call_args.kwargs["prompt"]
|
|
207
|
+
prompt = submit.call_args.kwargs["prompt"]("conversation")
|
|
207
208
|
assert "already published a review on `head-2`" in prompt
|
|
208
209
|
assert "no one else has commented since that review" in prompt
|
|
209
210
|
assert "Review the head again only when the head has moved" in prompt
|
|
@@ -232,8 +233,9 @@ def test_reviewer_submits_requested_exact_head(tmp_path, monkeypatch, draft):
|
|
|
232
233
|
call = run.dispatcher.deliver.call_args.kwargs
|
|
233
234
|
assert call["subject"] == "99:pr:2"
|
|
234
235
|
assert call["delivery"] == "42:head-2"
|
|
235
|
-
|
|
236
|
-
assert "
|
|
236
|
+
prompt = call["prompt"]("conversation")
|
|
237
|
+
assert "latest review request for `all-hands-bot` event 42" in prompt
|
|
238
|
+
assert "new head requires another reviewer request" in prompt
|
|
237
239
|
|
|
238
240
|
|
|
239
241
|
def _real_dispatcher(dispatcher_class, monkeypatch, conversation):
|
|
@@ -341,7 +343,8 @@ def test_reviewer_ignores_request_for_another_reviewer(tmp_path, monkeypatch):
|
|
|
341
343
|
run.dispatcher.deliver.assert_not_called()
|
|
342
344
|
|
|
343
345
|
|
|
344
|
-
|
|
346
|
+
@pytest.mark.parametrize("missing_issue", [False, True])
|
|
347
|
+
def test_reviewer_event_hands_positive_review_to_maintainer(tmp_path, monkeypatch, missing_issue):
|
|
345
348
|
module, run = _reviewer(tmp_path, monkeypatch)
|
|
346
349
|
_event(monkeypatch, action="submitted")
|
|
347
350
|
run.config["maintainers"] = "neubig, VascoSch92"
|
|
@@ -352,14 +355,26 @@ def test_reviewer_event_hands_positive_review_to_maintainer(tmp_path, monkeypatc
|
|
|
352
355
|
"created_at": "2026-01-01T00:00:00Z",
|
|
353
356
|
"requested_reviewer": {"login": "all-hands-bot"},
|
|
354
357
|
}
|
|
355
|
-
|
|
358
|
+
pr["body"] = "Fixes #7"
|
|
359
|
+
|
|
360
|
+
def gh(method, path, body=None):
|
|
361
|
+
if path == "/issues/7":
|
|
362
|
+
if missing_issue:
|
|
363
|
+
raise HTTPError(path, 404, "missing", {}, None)
|
|
364
|
+
return {"labels": [{"name": "priority:high"}]}
|
|
365
|
+
return {"id": 99} if path == "" else pr
|
|
366
|
+
|
|
367
|
+
run.gh = Mock(side_effect=gh) # GitHub transport boundary.
|
|
356
368
|
run.gh_pages = lambda path: [request] if path.endswith("/events") else _reviews()
|
|
357
369
|
handoff = Mock(return_value="VascoSch92")
|
|
358
370
|
monkeypatch.setattr(module, "request_maintainer_review", handoff)
|
|
359
371
|
|
|
360
372
|
run.run()
|
|
361
373
|
|
|
362
|
-
|
|
374
|
+
if missing_issue:
|
|
375
|
+
handoff.assert_not_called()
|
|
376
|
+
else:
|
|
377
|
+
handoff.assert_called_once_with(run, pr, ["neubig", "VascoSch92"])
|
|
363
378
|
assert all(call.args[0] != "DELETE" for call in run.gh.call_args_list)
|
|
364
379
|
run.dispatcher.deliver.assert_not_called()
|
|
365
380
|
|
|
@@ -379,7 +394,7 @@ def test_reviewer_delayed_submitted_event_completes_prior_request(
|
|
|
379
394
|
"created_at": "2026-01-03T00:00:00Z",
|
|
380
395
|
"requested_reviewer": {"login": "all-hands-bot"},
|
|
381
396
|
}
|
|
382
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr, pr])
|
|
397
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}, pr])
|
|
383
398
|
run.gh_pages = lambda path: (
|
|
384
399
|
[request]
|
|
385
400
|
if path.endswith("/events")
|
|
@@ -388,6 +403,7 @@ def test_reviewer_delayed_submitted_event_completes_prior_request(
|
|
|
388
403
|
handoff = Mock(return_value="VascoSch92")
|
|
389
404
|
monkeypatch.setattr(module, "request_maintainer_review", handoff)
|
|
390
405
|
|
|
406
|
+
pr["body"] = "Fixes #7"
|
|
391
407
|
run.run()
|
|
392
408
|
|
|
393
409
|
handoff.assert_called_once_with(run, pr, ["neubig", "VascoSch92"])
|
|
@@ -403,13 +419,14 @@ def test_reviewer_submitted_event_completes_without_request_event(
|
|
|
403
419
|
_event(monkeypatch, action="submitted", review_id=555)
|
|
404
420
|
run.config["maintainers"] = "neubig"
|
|
405
421
|
pr = {"number": 2, "head": {"sha": "head-2"}, "labels": []}
|
|
406
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr, pr])
|
|
422
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}, pr])
|
|
407
423
|
run.gh_pages = lambda path: (
|
|
408
424
|
[] if path.endswith("/events") else _reviews(review_id=555)
|
|
409
425
|
)
|
|
410
426
|
handoff = Mock(return_value="neubig")
|
|
411
427
|
monkeypatch.setattr(module, "request_maintainer_review", handoff)
|
|
412
428
|
|
|
429
|
+
pr["body"] = "Fixes #7"
|
|
413
430
|
run.run()
|
|
414
431
|
|
|
415
432
|
handoff.assert_called_once_with(run, pr, ["neubig"])
|
|
@@ -889,10 +906,11 @@ def test_reviewer_hands_positive_exact_head_to_maintainer(tmp_path, monkeypatch)
|
|
|
889
906
|
"labels": [{"name": "openhands-review"}],
|
|
890
907
|
}
|
|
891
908
|
run.gh_pages = lambda path: [pr] if path.startswith("/pulls?") else _reviews()
|
|
892
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr, pr, {}])
|
|
909
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}, pr, {}])
|
|
893
910
|
handoff = Mock(return_value="VascoSch92")
|
|
894
911
|
monkeypatch.setattr(module, "request_maintainer_review", handoff)
|
|
895
912
|
|
|
913
|
+
pr["body"] = "Fixes #7"
|
|
896
914
|
run.run()
|
|
897
915
|
|
|
898
916
|
handoff.assert_called_once_with(run, pr, ["neubig", "VascoSch92"])
|
|
@@ -912,13 +930,14 @@ def test_reviewer_retries_failed_handoff_without_clearing_label(tmp_path, monkey
|
|
|
912
930
|
"labels": [{"name": "openhands-review"}],
|
|
913
931
|
}
|
|
914
932
|
run.gh_pages = lambda path: [pr] if path.startswith("/pulls?") else _reviews()
|
|
915
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr])
|
|
933
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}])
|
|
916
934
|
monkeypatch.setattr(
|
|
917
935
|
module,
|
|
918
936
|
"request_maintainer_review",
|
|
919
937
|
Mock(side_effect=RuntimeError("temporary GitHub failure")),
|
|
920
938
|
)
|
|
921
939
|
|
|
940
|
+
pr["body"] = "Fixes #7"
|
|
922
941
|
with pytest.raises(RuntimeError, match="Reviewer scan failed for PRs: #2"):
|
|
923
942
|
run.run()
|
|
924
943
|
|
|
@@ -935,7 +954,7 @@ def test_reviewer_reports_permanent_handoff_error_once(tmp_path, monkeypatch):
|
|
|
935
954
|
"labels": [{"name": "openhands-review"}],
|
|
936
955
|
}
|
|
937
956
|
run.gh_pages = lambda path: [pr] if path.startswith("/pulls?") else _reviews()
|
|
938
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr, {}, pr, {}])
|
|
957
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}, {}, pr, {}])
|
|
939
958
|
monkeypatch.setattr(
|
|
940
959
|
module,
|
|
941
960
|
"request_maintainer_review",
|
|
@@ -946,9 +965,10 @@ def test_reviewer_reports_permanent_handoff_error_once(tmp_path, monkeypatch):
|
|
|
946
965
|
),
|
|
947
966
|
)
|
|
948
967
|
|
|
968
|
+
pr["body"] = "Fixes #7"
|
|
949
969
|
run.run()
|
|
950
970
|
|
|
951
|
-
assert "configuration check" in run.gh.call_args_list[
|
|
971
|
+
assert "configuration check" in run.gh.call_args_list[3].args[2]["body"]
|
|
952
972
|
assert run.gh.call_args_list[-1].args == (
|
|
953
973
|
"DELETE",
|
|
954
974
|
"/issues/2/labels/openhands-review",
|
|
@@ -1009,10 +1029,11 @@ def test_reviewer_hands_scope_stop_to_maintainer(tmp_path, monkeypatch):
|
|
|
1009
1029
|
if path.startswith("/pulls?")
|
|
1010
1030
|
else _reviews(verdict="🛑 MAINTAINER DECISION REQUIRED")
|
|
1011
1031
|
)
|
|
1012
|
-
run.gh = Mock(side_effect=[{"id": 99}, pr, pr, {}])
|
|
1032
|
+
run.gh = Mock(side_effect=[{"id": 99}, pr, {"labels": [{"name": "priority:medium"}]}, pr, {}])
|
|
1013
1033
|
handoff = Mock(return_value="VascoSch92")
|
|
1014
1034
|
monkeypatch.setattr(module, "request_maintainer_review", handoff)
|
|
1015
1035
|
|
|
1036
|
+
pr["body"] = "Fixes #7"
|
|
1016
1037
|
run.run()
|
|
1017
1038
|
|
|
1018
1039
|
handoff.assert_called_once_with(run, pr, ["neubig", "VascoSch92"])
|
|
@@ -2054,7 +2075,9 @@ def test_reviewer_scheduled_scan_reviews_an_outstanding_request(tmp_path, monkey
|
|
|
2054
2075
|
run.dispatcher.deliver.assert_called_once()
|
|
2055
2076
|
call = run.dispatcher.deliver.call_args.kwargs
|
|
2056
2077
|
assert call["delivery"] == "42:head-2"
|
|
2057
|
-
assert "latest review request for `all-hands-bot` event 42" in call["prompt"]
|
|
2078
|
+
assert "latest review request for `all-hands-bot` event 42" in call["prompt"](
|
|
2079
|
+
"conversation"
|
|
2080
|
+
)
|
|
2058
2081
|
|
|
2059
2082
|
|
|
2060
2083
|
@pytest.mark.parametrize("draft", [False, True])
|
|
@@ -2118,7 +2141,9 @@ def test_reviewer_scheduled_scan_reviews_an_unrequested_green_pr(
|
|
|
2118
2141
|
call = run.dispatcher.deliver.call_args.kwargs
|
|
2119
2142
|
assert call["subject"] == "99:pr:3"
|
|
2120
2143
|
assert call["delivery"] == "scan:owner/repo:3:head-3"
|
|
2121
|
-
assert "scheduled scan of open, non-draft pull requests" in call["prompt"]
|
|
2144
|
+
assert "scheduled scan of open, non-draft pull requests" in call["prompt"](
|
|
2145
|
+
"conversation"
|
|
2146
|
+
)
|
|
2122
2147
|
|
|
2123
2148
|
|
|
2124
2149
|
def test_reviewer_scheduled_scan_skips_a_pr_with_a_current_head_review(
|
|
@@ -2264,7 +2289,9 @@ class _DedupeDispatcher:
|
|
|
2264
2289
|
|
|
2265
2290
|
def deliver(self, *, subject, delivery, prompt, head=""):
|
|
2266
2291
|
self.calls.append((subject, delivery))
|
|
2267
|
-
|
|
2292
|
+
# The worker's prompt names the receiving conversation, so render it
|
|
2293
|
+
# the way the real dispatcher does.
|
|
2294
|
+
self.prompts.append(prompt(subject))
|
|
2268
2295
|
self.heads.append(head)
|
|
2269
2296
|
if self.seen.get(subject) == delivery:
|
|
2270
2297
|
return {"disposition": "deduplicated", "conversation_id": subject}
|
|
@@ -2817,13 +2844,13 @@ def test_unrequested_scan_reviews_the_oldest_eligible_prs_under_the_cap(
|
|
|
2817
2844
|
}
|
|
2818
2845
|
|
|
2819
2846
|
|
|
2820
|
-
def
|
|
2847
|
+
def test_unrequested_scan_reconciles_a_completed_review_without_handoff(
|
|
2821
2848
|
tmp_path, monkeypatch
|
|
2822
2849
|
):
|
|
2823
2850
|
"""A head that already carries a completed review is not re-dispatched.
|
|
2824
2851
|
|
|
2825
|
-
The review is
|
|
2826
|
-
|
|
2852
|
+
The review is reconciled without a second conversation or a handoff when
|
|
2853
|
+
no linked issue has an eligible priority.
|
|
2827
2854
|
"""
|
|
2828
2855
|
module, (one,) = _scan_reviewers(tmp_path, monkeypatch, ["owner/one"])
|
|
2829
2856
|
_wire_scan(
|
|
@@ -2851,7 +2878,7 @@ def test_unrequested_scan_reconciles_a_completed_review_and_hands_off(
|
|
|
2851
2878
|
_run_scan(module, [one])
|
|
2852
2879
|
|
|
2853
2880
|
assert one.dispatcher.seen == {}
|
|
2854
|
-
handoff.
|
|
2881
|
+
handoff.assert_not_called()
|
|
2855
2882
|
|
|
2856
2883
|
|
|
2857
2884
|
def test_unrequested_scan_still_gates_a_blocked_pr_past_the_cap(
|
|
@@ -3069,3 +3096,129 @@ def test_a_retried_conversation_uses_a_launch_slot(tmp_path, monkeypatch):
|
|
|
3069
3096
|
intake.drain()
|
|
3070
3097
|
|
|
3071
3098
|
assert started == [1]
|
|
3099
|
+
|
|
3100
|
+
|
|
3101
|
+
@pytest.mark.parametrize("status", [404, 403, 500])
|
|
3102
|
+
def test_linked_issue_lookup_http_errors(tmp_path, monkeypatch, status):
|
|
3103
|
+
_, run = _reviewer(tmp_path, monkeypatch)
|
|
3104
|
+
def gh(method, path):
|
|
3105
|
+
raise HTTPError(path, status, "unavailable", {}, None)
|
|
3106
|
+
run.gh = gh
|
|
3107
|
+
pr = {"body": "Fixes #7"}
|
|
3108
|
+
if status == 404:
|
|
3109
|
+
assert not run._linked_issue_allows_handoff(pr)
|
|
3110
|
+
else:
|
|
3111
|
+
with pytest.raises(HTTPError):
|
|
3112
|
+
run._linked_issue_allows_handoff(pr)
|
|
3113
|
+
|
|
3114
|
+
|
|
3115
|
+
def test_missing_link_does_not_hide_low_priority_link(tmp_path, monkeypatch):
|
|
3116
|
+
_, run = _reviewer(tmp_path, monkeypatch)
|
|
3117
|
+
def gh(method, path):
|
|
3118
|
+
if path == "/issues/7":
|
|
3119
|
+
raise HTTPError(path, 404, "missing", {}, None)
|
|
3120
|
+
return {"labels": [{"name": "priority:low"}]}
|
|
3121
|
+
run.gh = gh
|
|
3122
|
+
assert not run._linked_issue_allows_handoff({"body": "Fixes #7, fixes #8"})
|
|
3123
|
+
|
|
3124
|
+
|
|
3125
|
+
def test_rotating_window_revisits_budget_deferred_heads(tmp_path, monkeypatch):
|
|
3126
|
+
module, run = _reviewer(tmp_path, monkeypatch)
|
|
3127
|
+
monkeypatch.setattr(module.workflow, "_kv_available", lambda: False)
|
|
3128
|
+
prs = [{"number": n, "labels": []} for n in range(27)]
|
|
3129
|
+
reviewed = set(range(10))
|
|
3130
|
+
blocked = {10, 11}
|
|
3131
|
+
visited = set()
|
|
3132
|
+
started = []
|
|
3133
|
+
for _ in range(30):
|
|
3134
|
+
intake = module.ReviewIntake()
|
|
3135
|
+
window = run._rotating_window(prs, "openhands-review")
|
|
3136
|
+
visited.update(pr["number"] for pr in window)
|
|
3137
|
+
for pr in window:
|
|
3138
|
+
n = pr["number"]
|
|
3139
|
+
if n in reviewed or n in blocked:
|
|
3140
|
+
continue
|
|
3141
|
+
def start(n=n):
|
|
3142
|
+
reviewed.add(n)
|
|
3143
|
+
started.append(n)
|
|
3144
|
+
return {"disposition": "created"}
|
|
3145
|
+
intake.register({"priority": 1, "created_at": "", "repository": run.repository,
|
|
3146
|
+
"number": n, "config": {"max_new_per_run": 2}, "start": start})
|
|
3147
|
+
intake.drain()
|
|
3148
|
+
assert visited == set(range(27))
|
|
3149
|
+
assert set(started) == set(range(27)) - set(range(10)) - blocked
|
|
3150
|
+
assert len(started) == len(set(started))
|
|
3151
|
+
|
|
3152
|
+
|
|
3153
|
+
@pytest.mark.parametrize("trigger_kind", ["scheduled", "requested", "submitted"])
|
|
3154
|
+
@pytest.mark.parametrize("scope_stop", [False, True])
|
|
3155
|
+
@pytest.mark.parametrize(
|
|
3156
|
+
"body,issues,expected",
|
|
3157
|
+
[
|
|
3158
|
+
("", {}, False),
|
|
3159
|
+
("Fixes #7", {7: {}}, False),
|
|
3160
|
+
("Fixes #7", {7: {"labels": [{"name": "priority:low"}]}}, False),
|
|
3161
|
+
("Fixes #7", {7: None}, False),
|
|
3162
|
+
("Fixes #7", {7: {"labels": [{"name": "priority:medium"}]}}, True),
|
|
3163
|
+
("Fixes #7", {7: {"labels": [{"name": "priority:high"}]}}, True),
|
|
3164
|
+
(
|
|
3165
|
+
"Fixes #7, fixes #8",
|
|
3166
|
+
{
|
|
3167
|
+
7: {"labels": [{"name": "priority:low"}]},
|
|
3168
|
+
8: {"labels": [{"name": "priority:high"}]},
|
|
3169
|
+
},
|
|
3170
|
+
True,
|
|
3171
|
+
),
|
|
3172
|
+
(
|
|
3173
|
+
"Fixes #7, fixes #8",
|
|
3174
|
+
{7: None, 8: {"labels": [{"name": "priority:medium"}]}},
|
|
3175
|
+
True,
|
|
3176
|
+
),
|
|
3177
|
+
(
|
|
3178
|
+
"Fixes #7",
|
|
3179
|
+
{7: {"labels": [{"name": "priority:high"}], "pull_request": {}}},
|
|
3180
|
+
False,
|
|
3181
|
+
),
|
|
3182
|
+
("Fixes #7", {7: {"labels": [{"name": "priority:normal"}]}}, False),
|
|
3183
|
+
],
|
|
3184
|
+
)
|
|
3185
|
+
def test_priority_gates_real_handoff(
|
|
3186
|
+
tmp_path, monkeypatch, trigger_kind, scope_stop, body, issues, expected
|
|
3187
|
+
):
|
|
3188
|
+
"""Only the GitHub boundary is substituted; completion and selection are real."""
|
|
3189
|
+
module, run = _reviewer(tmp_path, monkeypatch)
|
|
3190
|
+
run.config["maintainers"] = ["maintainer"]
|
|
3191
|
+
verdict = (
|
|
3192
|
+
module.workflow.MAINTAINER_DECISION_VERDICT if scope_stop else "✅ APPROVED"
|
|
3193
|
+
)
|
|
3194
|
+
pr = {
|
|
3195
|
+
"number": 2,
|
|
3196
|
+
"body": body,
|
|
3197
|
+
"head": {"sha": "head-2"},
|
|
3198
|
+
"user": {"login": "author"},
|
|
3199
|
+
}
|
|
3200
|
+
writes = []
|
|
3201
|
+
|
|
3202
|
+
def gh(method, path, data=None):
|
|
3203
|
+
if method == "GET" and path == "/pulls/2":
|
|
3204
|
+
return pr
|
|
3205
|
+
if method == "GET" and path.startswith("/issues/"):
|
|
3206
|
+
issue = issues[int(path.rsplit("/", 1)[1])]
|
|
3207
|
+
if issue is None:
|
|
3208
|
+
raise HTTPError(path, 404, "missing", {}, None)
|
|
3209
|
+
return issue
|
|
3210
|
+
assert method == "POST" and path == "/pulls/2/requested_reviewers"
|
|
3211
|
+
writes.append(data)
|
|
3212
|
+
return {}
|
|
3213
|
+
|
|
3214
|
+
run.gh = gh
|
|
3215
|
+
run.gh_pages = lambda path: (
|
|
3216
|
+
_reviews(verdict=verdict, review_id=42) if path.endswith("/reviews") else []
|
|
3217
|
+
)
|
|
3218
|
+
run.api = lambda *args, **kwargs: {"total_count": 0}
|
|
3219
|
+
trigger = (
|
|
3220
|
+
None if trigger_kind == "scheduled" else {"created_at": "2026-01-01T00:00:00Z"}
|
|
3221
|
+
)
|
|
3222
|
+
submitted = {"id": 42} if trigger_kind == "submitted" else None
|
|
3223
|
+
assert run._finish_completed_review(pr, trigger, submitted_review=submitted)
|
|
3224
|
+
assert writes == ([{"reviewers": ["maintainer"]}] if expected else [])
|