Skip to content

Commit 84aa729

Browse files
authored
feat(reviewer-bot): add reviewer board preview groundwork (#545)
* feat(reviewer-bot): add reviewer board preview preflight * feat(reviewer-bot): expose reviewer board preview workflow
1 parent 20dfff8 commit 84aa729

10 files changed

Lines changed: 1164 additions & 12 deletions

File tree

.github/reviewer-bot-tests/test_main.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,77 @@ def test_main_show_state_uses_direct_yaml_import(monkeypatch, capsys):
6262
assert "freshness_runtime_epoch" in output
6363

6464

65+
def _valid_reviewer_board_metadata():
66+
return {
67+
"data": {
68+
"organization": {
69+
"projectV2": {
70+
"id": "PVT_kwDOB",
71+
"title": "Reviewer Board",
72+
"fields": {
73+
"nodes": [
74+
{
75+
"__typename": "ProjectV2SingleSelectField",
76+
"id": "field-review-state",
77+
"name": "Review State",
78+
"options": [
79+
{"id": "opt-ar", "name": "Awaiting Reviewer"},
80+
{"id": "opt-ac", "name": "Awaiting Contributor"},
81+
{"id": "opt-aw", "name": "Awaiting Write Approval"},
82+
{"id": "opt-done", "name": "Done"},
83+
{"id": "opt-unassigned", "name": "Unassigned"},
84+
],
85+
},
86+
{
87+
"__typename": "ProjectV2Field",
88+
"dataType": "TEXT",
89+
"id": "field-reviewer",
90+
"name": "Reviewer",
91+
},
92+
{
93+
"__typename": "ProjectV2Field",
94+
"dataType": "DATE",
95+
"id": "field-assigned-at",
96+
"name": "Assigned At",
97+
},
98+
{
99+
"__typename": "ProjectV2Field",
100+
"dataType": "DATE",
101+
"id": "field-waiting-since",
102+
"name": "Waiting Since",
103+
},
104+
{
105+
"__typename": "ProjectV2SingleSelectField",
106+
"id": "field-needs-attention",
107+
"name": "Needs Attention",
108+
"options": [
109+
{"id": "opt-no", "name": "No"},
110+
{"id": "opt-warning", "name": "Warning Sent"},
111+
{"id": "opt-notice", "name": "Transition Notice Sent"},
112+
{"id": "opt-triage", "name": "Triage Approval Required"},
113+
{"id": "opt-repair", "name": "Projection Repair Required"},
114+
],
115+
},
116+
]
117+
},
118+
}
119+
}
120+
}
121+
}
122+
123+
65124
def test_classify_event_intent_cross_repo_review_is_non_mutating_defer(monkeypatch):
66125
monkeypatch.setenv("PR_IS_CROSS_REPOSITORY", "true")
67126
intent = reviewer_bot.classify_event_intent("pull_request_review", "submitted")
68127
assert intent == reviewer_bot.EVENT_INTENT_NON_MUTATING_DEFER
69128

70129

130+
def test_classify_event_intent_preview_reviewer_board_is_non_mutating(monkeypatch):
131+
monkeypatch.setenv("MANUAL_ACTION", "preview-reviewer-board")
132+
intent = reviewer_bot.classify_event_intent("workflow_dispatch", "")
133+
assert intent == reviewer_bot.EVENT_INTENT_NON_MUTATING_READONLY
134+
135+
71136
def test_classify_event_intent_same_repo_review_is_non_mutating_defer(monkeypatch):
72137
intent = reviewer_bot.classify_event_intent("pull_request_review", "submitted")
73138
assert intent == reviewer_bot.EVENT_INTENT_NON_MUTATING_DEFER
@@ -248,6 +313,170 @@ def fake_sync_status_labels_for_items(state, issue_numbers):
248313
]
249314

250315

316+
def test_main_preview_reviewer_board_disabled_is_clean_noop(monkeypatch, capsys):
317+
monkeypatch.setenv("EVENT_NAME", "workflow_dispatch")
318+
monkeypatch.setenv("EVENT_ACTION", "")
319+
monkeypatch.setenv("MANUAL_ACTION", "preview-reviewer-board")
320+
monkeypatch.setenv("REVIEWER_BOARD_ENABLED", "false")
321+
322+
monkeypatch.setattr(reviewer_bot, "load_state", lambda *args, **kwargs: make_state())
323+
monkeypatch.setattr(
324+
reviewer_bot,
325+
"acquire_state_issue_lease_lock",
326+
lambda: (_ for _ in ()).throw(AssertionError("preview should not acquire lock")),
327+
)
328+
monkeypatch.setattr(
329+
reviewer_bot,
330+
"process_pass_until_expirations",
331+
lambda state: (_ for _ in ()).throw(AssertionError("preview should skip pass-until processing")),
332+
)
333+
monkeypatch.setattr(
334+
reviewer_bot,
335+
"sync_members_with_queue",
336+
lambda state: (_ for _ in ()).throw(AssertionError("preview should skip member sync")),
337+
)
338+
monkeypatch.setattr(
339+
reviewer_bot,
340+
"save_state",
341+
lambda state: (_ for _ in ()).throw(AssertionError("preview should not save state")),
342+
)
343+
monkeypatch.setattr(
344+
reviewer_bot,
345+
"sync_status_labels_for_items",
346+
lambda state, issue_numbers: (_ for _ in ()).throw(AssertionError("preview should not sync labels")),
347+
)
348+
349+
reviewer_bot.main()
350+
351+
output = capsys.readouterr().out
352+
assert "Reviewer board preview skipped: reviewer board is disabled." in output
353+
354+
355+
def test_main_preview_reviewer_board_missing_token_fails_clearly(monkeypatch, capsys):
356+
monkeypatch.setenv("EVENT_NAME", "workflow_dispatch")
357+
monkeypatch.setenv("EVENT_ACTION", "")
358+
monkeypatch.setenv("MANUAL_ACTION", "preview-reviewer-board")
359+
monkeypatch.setenv("REVIEWER_BOARD_ENABLED", "true")
360+
monkeypatch.setattr(reviewer_bot, "_reviewer_board_project_metadata", None, raising=False)
361+
362+
monkeypatch.setattr(reviewer_bot, "load_state", lambda *args, **kwargs: make_state())
363+
monkeypatch.setattr(
364+
reviewer_bot,
365+
"acquire_state_issue_lease_lock",
366+
lambda: (_ for _ in ()).throw(AssertionError("preview should not acquire lock")),
367+
)
368+
monkeypatch.setattr(
369+
reviewer_bot,
370+
"process_pass_until_expirations",
371+
lambda state: (_ for _ in ()).throw(AssertionError("preview should skip pass-until processing")),
372+
)
373+
monkeypatch.setattr(
374+
reviewer_bot,
375+
"sync_members_with_queue",
376+
lambda state: (_ for _ in ()).throw(AssertionError("preview should skip member sync")),
377+
)
378+
379+
with pytest.raises(SystemExit) as excinfo:
380+
reviewer_bot.main()
381+
382+
assert excinfo.value.code == 1
383+
assert "REVIEWER_BOARD_TOKEN not set" in capsys.readouterr().err
384+
385+
386+
def test_main_preview_reviewer_board_invalid_manifest_fails_clearly(monkeypatch, capsys):
387+
monkeypatch.setenv("EVENT_NAME", "workflow_dispatch")
388+
monkeypatch.setenv("EVENT_ACTION", "")
389+
monkeypatch.setenv("MANUAL_ACTION", "preview-reviewer-board")
390+
monkeypatch.setenv("REVIEWER_BOARD_ENABLED", "true")
391+
monkeypatch.setenv("REVIEWER_BOARD_TOKEN", "board-token")
392+
monkeypatch.setattr(reviewer_bot, "_reviewer_board_project_metadata", None, raising=False)
393+
394+
monkeypatch.setattr(reviewer_bot, "load_state", lambda *args, **kwargs: make_state())
395+
monkeypatch.setattr(
396+
reviewer_bot,
397+
"acquire_state_issue_lease_lock",
398+
lambda: (_ for _ in ()).throw(AssertionError("preview should not acquire lock")),
399+
)
400+
monkeypatch.setattr(
401+
reviewer_bot,
402+
"github_graphql",
403+
lambda query, variables=None, *, token=None: {
404+
"data": {
405+
"organization": {
406+
"projectV2": {
407+
"id": "PVT_kwDOB",
408+
"title": "Reviewer Board",
409+
"fields": {"nodes": []},
410+
}
411+
}
412+
}
413+
},
414+
)
415+
416+
with pytest.raises(SystemExit) as excinfo:
417+
reviewer_bot.main()
418+
419+
assert excinfo.value.code == 1
420+
assert "Missing reviewer board field: Review State" in capsys.readouterr().err
421+
422+
423+
def test_main_preview_reviewer_board_is_read_only(monkeypatch, capsys):
424+
monkeypatch.setenv("EVENT_NAME", "workflow_dispatch")
425+
monkeypatch.setenv("EVENT_ACTION", "")
426+
monkeypatch.setenv("MANUAL_ACTION", "preview-reviewer-board")
427+
monkeypatch.setenv("REVIEWER_BOARD_ENABLED", "true")
428+
monkeypatch.setenv("REVIEWER_BOARD_TOKEN", "board-token")
429+
monkeypatch.setenv("ISSUE_NUMBER", "42")
430+
monkeypatch.setattr(reviewer_bot, "_reviewer_board_project_metadata", None, raising=False)
431+
432+
state = make_state()
433+
state["status_projection_epoch"] = "status_projection_v1"
434+
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
435+
assert review is not None
436+
review["current_reviewer"] = "alice"
437+
review["assigned_at"] = "2026-03-20T12:34:56Z"
438+
review["active_cycle_started_at"] = "2026-03-20T12:34:56Z"
439+
440+
monkeypatch.setattr(reviewer_bot, "load_state", lambda *args, **kwargs: state)
441+
monkeypatch.setattr(
442+
reviewer_bot,
443+
"acquire_state_issue_lease_lock",
444+
lambda: (_ for _ in ()).throw(AssertionError("preview should not acquire lock")),
445+
)
446+
monkeypatch.setattr(
447+
reviewer_bot,
448+
"process_pass_until_expirations",
449+
lambda current: (_ for _ in ()).throw(AssertionError("preview should skip pass-until processing")),
450+
)
451+
monkeypatch.setattr(
452+
reviewer_bot,
453+
"sync_members_with_queue",
454+
lambda current: (_ for _ in ()).throw(AssertionError("preview should skip member sync")),
455+
)
456+
monkeypatch.setattr(
457+
reviewer_bot,
458+
"save_state",
459+
lambda current: (_ for _ in ()).throw(AssertionError("preview should not save state")),
460+
)
461+
monkeypatch.setattr(
462+
reviewer_bot,
463+
"sync_status_labels_for_items",
464+
lambda current, issue_numbers: (_ for _ in ()).throw(AssertionError("preview should not sync labels")),
465+
)
466+
monkeypatch.setattr(reviewer_bot, "github_graphql", lambda query, variables=None, *, token=None: _valid_reviewer_board_metadata())
467+
monkeypatch.setattr(
468+
reviewer_bot,
469+
"get_issue_or_pr_snapshot",
470+
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": None, "labels": []},
471+
)
472+
473+
reviewer_bot.main()
474+
475+
output = capsys.readouterr().out
476+
assert "classification: open_tracked_assigned" in output
477+
assert "ensure_membership: true" in output
478+
479+
251480
def test_issue_close_then_close_comment_does_not_leave_active_review(monkeypatch):
252481
state = make_state()
253482
review = reviewer_bot.ensure_review_entry(state, 42, create=True)

0 commit comments

Comments
 (0)