Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/reviewer-bot-tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,56 @@ def fake_api(method, endpoint, data=None):
assert posted == []


def test_main_schedule_reviewer_review_repair_marks_item_for_label_sync(monkeypatch):
monkeypatch.setenv("EVENT_NAME", "schedule")
monkeypatch.setenv("EVENT_ACTION", "")
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
review["current_reviewer"] = "alice"
review["active_cycle_started_at"] = "2026-03-17T09:00:00Z"

synced_issue_numbers = []

monkeypatch.setattr(reviewer_bot, "acquire_state_issue_lease_lock", lambda: None)
monkeypatch.setattr(reviewer_bot, "release_state_issue_lease_lock", lambda: True)
monkeypatch.setattr(reviewer_bot, "load_state", lambda *args, **kwargs: state)
monkeypatch.setattr(reviewer_bot, "process_pass_until_expirations", lambda current: (current, []))
monkeypatch.setattr(reviewer_bot, "sync_members_with_queue", lambda current: (current, []))
monkeypatch.setattr(reviewer_bot.maintenance_module, "sweep_deferred_gaps", lambda bot, current: False)
monkeypatch.setattr(reviewer_bot.maintenance_module, "maybe_record_head_observation_repair", lambda bot, issue_number, review_data: False)
monkeypatch.setattr(reviewer_bot.maintenance_module, "check_overdue_reviews", lambda bot, current: [])
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(
reviewer_bot,
"get_pull_request_reviews",
lambda issue_number: [
{
"id": 10,
"state": "COMMENTED",
"submitted_at": "2026-03-17T10:01:00Z",
"commit_id": "head-1",
"user": {"login": "alice"},
}
],
)
monkeypatch.setattr(reviewer_bot, "save_state", lambda current: True)
monkeypatch.setattr(
reviewer_bot,
"sync_status_labels_for_items",
lambda current, issue_numbers: synced_issue_numbers.extend(issue_numbers) or True,
)

reviewer_bot.main()

assert review["reviewer_review"]["accepted"]["semantic_key"] == "pull_request_review:10"
assert synced_issue_numbers == [42]


def test_main_mutating_event_fails_closed_when_state_unavailable(monkeypatch):
monkeypatch.setenv("EVENT_NAME", "issue_comment")
monkeypatch.setenv("EVENT_ACTION", "created")
Expand Down
169 changes: 168 additions & 1 deletion .github/reviewer-bot-tests/test_reviewer_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def test_pr_comment_direct_path_is_epoch_gated(monkeypatch):
assert reviewer_bot.handle_comment_event(state) is False


def test_check_overdue_reviews_skips_transition_after_transition_notice_sent():
def test_check_overdue_reviews_skips_transition_after_transition_notice_sent(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
Expand All @@ -328,6 +328,12 @@ def test_check_overdue_reviews_skips_transition_after_transition_notice_sent():
review["last_reviewer_activity"] = "2026-03-01T00:00:00Z"
review["transition_warning_sent"] = "2026-03-10T00:00:00Z"
review["transition_notice_sent_at"] = "2026-03-25T00:00:00Z"
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(reviewer_bot, "get_pull_request_reviews", lambda issue_number: [])
assert reviewer_bot.maintenance_module.check_overdue_reviews(reviewer_bot, state) == []


Expand Down Expand Up @@ -449,6 +455,167 @@ def test_scheduled_check_repairs_missing_reviewer_review_state(monkeypatch):
assert review["last_reviewer_activity"] == "2026-03-17T10:01:00Z"


def test_check_overdue_reviews_skips_pr_with_current_head_reviewer_review(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
review["current_reviewer"] = "alice"
review["assigned_at"] = "2026-03-01T00:00:00Z"
review["active_cycle_started_at"] = "2026-03-01T00:00:00Z"
review["reviewer_review"]["accepted"] = {
"semantic_key": "pull_request_review:10",
"timestamp": "2026-03-02T00:00:00Z",
"actor": "alice",
"reviewed_head_sha": "head-1",
"source_precedence": 1,
"payload": {},
}
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(
reviewer_bot,
"github_api",
lambda method, endpoint, data=None: {"head": {"sha": "head-1"}} if endpoint == "pulls/42" else None,
)
monkeypatch.setattr(reviewer_bot, "get_pull_request_reviews", lambda issue_number: [])
monkeypatch.setattr(
reviewer_bot.reviews_module,
"rebuild_pr_approval_state",
lambda bot, issue_number, review_data, **kwargs: ({"completed": False}, {"has_write_approval": False}),
)
assert reviewer_bot.maintenance_module.check_overdue_reviews(reviewer_bot, state) == []


def test_check_overdue_reviews_uses_contributor_comment_timestamp_when_turn_returns_to_reviewer(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
review["current_reviewer"] = "alice"
review["assigned_at"] = "2026-03-01T00:00:00Z"
review["active_cycle_started_at"] = "2026-03-01T00:00:00Z"
review["reviewer_review"]["accepted"] = {
"semantic_key": "pull_request_review:10",
"timestamp": "2026-03-02T00:00:00Z",
"actor": "alice",
"reviewed_head_sha": "head-1",
"source_precedence": 1,
"payload": {},
}
review["contributor_comment"]["accepted"] = {
"semantic_key": "issue_comment:20",
"timestamp": "2026-03-12T00:00:00Z",
"actor": "bob",
"reviewed_head_sha": None,
"source_precedence": 0,
"payload": {},
}
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(
reviewer_bot,
"github_api",
lambda method, endpoint, data=None: {"head": {"sha": "head-1"}} if endpoint == "pulls/42" else None,
)
monkeypatch.setattr(reviewer_bot, "get_pull_request_reviews", lambda issue_number: [])
monkeypatch.setattr(
reviewer_bot.reviews_module,
"rebuild_pr_approval_state",
lambda bot, issue_number, review_data, **kwargs: ({"completed": False}, {"has_write_approval": False}),
)
overdue = reviewer_bot.maintenance_module.check_overdue_reviews(reviewer_bot, state)
assert overdue[0]["issue_number"] == 42
assert overdue[0]["needs_warning"] is True
assert overdue[0]["days_overdue"] == 0


def test_check_overdue_reviews_uses_contributor_revision_timestamp_when_head_changes_after_review(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
review["current_reviewer"] = "alice"
review["assigned_at"] = "2026-03-01T00:00:00Z"
review["active_cycle_started_at"] = "2026-03-01T00:00:00Z"
review["reviewer_review"]["accepted"] = {
"semantic_key": "pull_request_review:10",
"timestamp": "2026-03-02T00:00:00Z",
"actor": "alice",
"reviewed_head_sha": "head-1",
"source_precedence": 1,
"payload": {},
}
review["contributor_revision"]["accepted"] = {
"semantic_key": "pull_request_sync:42:head-2",
"timestamp": "2026-03-12T00:00:00Z",
"actor": None,
"reviewed_head_sha": "head-2",
"source_precedence": 1,
"payload": {},
}
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(
reviewer_bot,
"github_api",
lambda method, endpoint, data=None: {"head": {"sha": "head-2"}} if endpoint == "pulls/42" else None,
)
monkeypatch.setattr(reviewer_bot, "get_pull_request_reviews", lambda issue_number: [])
overdue = reviewer_bot.maintenance_module.check_overdue_reviews(reviewer_bot, state)
assert overdue[0]["issue_number"] == 42
assert overdue[0]["needs_warning"] is True
assert overdue[0]["days_overdue"] == 0


def test_check_overdue_reviews_ignores_same_head_contributor_revision_after_valid_reviewer_review(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
assert review is not None
review["current_reviewer"] = "alice"
review["assigned_at"] = "2026-03-01T00:00:00Z"
review["active_cycle_started_at"] = "2026-03-01T00:00:00Z"
review["reviewer_review"]["accepted"] = {
"semantic_key": "pull_request_review:10",
"timestamp": "2026-03-02T00:00:00Z",
"actor": "alice",
"reviewed_head_sha": "head-1",
"source_precedence": 1,
"payload": {},
}
review["contributor_revision"]["accepted"] = {
"semantic_key": "pull_request_head_observed:42:head-1",
"timestamp": "2026-03-12T00:00:00Z",
"actor": None,
"reviewed_head_sha": "head-1",
"source_precedence": 1,
"payload": {},
}
monkeypatch.setattr(
reviewer_bot,
"get_issue_or_pr_snapshot",
lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []},
)
monkeypatch.setattr(
reviewer_bot,
"github_api",
lambda method, endpoint, data=None: {"head": {"sha": "head-1"}} if endpoint == "pulls/42" else None,
)
monkeypatch.setattr(reviewer_bot, "get_pull_request_reviews", lambda issue_number: [])
monkeypatch.setattr(
reviewer_bot.reviews_module,
"rebuild_pr_approval_state",
lambda bot, issue_number, review_data, **kwargs: ({"completed": False}, {"has_write_approval": False}),
)
assert reviewer_bot.maintenance_module.check_overdue_reviews(reviewer_bot, state) == []


def test_issue_edit_by_author_records_contributor_freshness(monkeypatch):
state = make_state()
review = reviewer_bot.ensure_review_entry(state, 42, create=True)
Expand Down
2 changes: 2 additions & 0 deletions scripts/reviewer_bot_lib/maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ def handle_scheduled_check(bot, state: dict) -> bool:
continue
if bot.reviews_module.repair_missing_reviewer_review_state(bot, issue_number, review_data):
changed = True
bot.collect_touched_item(issue_number)
if maybe_record_head_observation_repair(bot, issue_number, review_data):
changed = True
bot.collect_touched_item(issue_number)
overdue_reviews = check_overdue_reviews(bot, state)
if not overdue_reviews:
return changed
Expand Down
24 changes: 19 additions & 5 deletions scripts/reviewer_bot_lib/overdue.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,23 @@ def check_overdue_reviews(bot, state: dict) -> list[dict]:
if not current_reviewer:
continue

last_activity = review_data.get("last_reviewer_activity")
if not last_activity:
last_activity = review_data.get("assigned_at")
issue_number = int(issue_key)
issue_snapshot = bot.get_issue_or_pr_snapshot(issue_number)
if isinstance(issue_snapshot, dict) and isinstance(issue_snapshot.get("pull_request"), dict):
response_state = bot.reviews_module.compute_reviewer_response_state(
bot,
issue_number,
review_data,
issue_snapshot=issue_snapshot,
)
if response_state.get("state") != "awaiting_reviewer_response":
continue
last_activity = response_state.get("anchor_timestamp")
else:
last_activity = review_data.get("last_reviewer_activity")
if not last_activity:
last_activity = review_data.get("assigned_at")

if not last_activity:
continue

Expand Down Expand Up @@ -52,7 +66,7 @@ def check_overdue_reviews(bot, state: dict) -> list[dict]:
if days_since_warning >= bot.TRANSITION_PERIOD_DAYS:
overdue.append(
{
"issue_number": int(issue_key),
"issue_number": issue_number,
"reviewer": current_reviewer,
"days_overdue": days_since_activity,
"days_since_warning": days_since_warning,
Expand All @@ -65,7 +79,7 @@ def check_overdue_reviews(bot, state: dict) -> list[dict]:
else:
overdue.append(
{
"issue_number": int(issue_key),
"issue_number": issue_number,
"reviewer": current_reviewer,
"days_overdue": days_since_activity - bot.REVIEW_DEADLINE_DAYS,
"days_since_warning": 0,
Expand Down
Loading
Loading