|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from copy import deepcopy |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from scripts.reviewer_bot_lib import maintenance_schedule, repair_records, reviews |
| 8 | +from scripts.reviewer_bot_lib import overdue as overdue_lib |
| 9 | +from tests.fixtures.fake_runtime import FakeReviewerBotRuntime |
| 10 | +from tests.fixtures.reviewer_bot import ( |
| 11 | + make_state, |
| 12 | + make_tracked_review_state, |
| 13 | + pull_request_payload, |
| 14 | + review_payload, |
| 15 | +) |
| 16 | +from tests.fixtures.reviewer_bot_builders import accept_reviewer_review |
| 17 | +from tests.fixtures.reviewer_bot_fakes import RouteGitHubApi |
| 18 | + |
| 19 | +pytestmark = pytest.mark.integration |
| 20 | + |
| 21 | + |
| 22 | +def test_scheduled_pr264_legacy_duplicate_no_ping_path_preserves_state(monkeypatch): |
| 23 | + state = make_state() |
| 24 | + review = make_tracked_review_state( |
| 25 | + state, |
| 26 | + 264, |
| 27 | + reviewer="iglesias", |
| 28 | + assigned_at="2026-02-10T17:20:07Z", |
| 29 | + active_cycle_started_at="2026-02-10T17:20:07Z", |
| 30 | + ) |
| 31 | + review["active_head_sha"] = "head-live" |
| 32 | + accept_reviewer_review( |
| 33 | + review, |
| 34 | + semantic_key="pull_request_review:77", |
| 35 | + timestamp="2026-03-18T01:09:05Z", |
| 36 | + actor="iglesias", |
| 37 | + reviewed_head_sha="head-old", |
| 38 | + ) |
| 39 | + sidecars = review.setdefault("sidecars", {}) |
| 40 | + sidecars["reminder_delivery_receipts"] = { |
| 41 | + "legacy:264:9002": { |
| 42 | + "issue_number": 264, |
| 43 | + "reviewer": "iglesias", |
| 44 | + "head_sha": "head-live", |
| 45 | + "cycle_key": "2026-02-10T17:20:07Z", |
| 46 | + "scope_key": "reviewer=iglesias|head=head-live|cycle=2026-02-10T17:20:07Z|anchor=2026-02-10T17:20:07Z", |
| 47 | + "receipt_kind": "legacy_warning_or_reminder", |
| 48 | + "comment_id": 9002, |
| 49 | + "comment_created_at": "2026-04-14T00:44:23Z", |
| 50 | + "state_save_attempted": False, |
| 51 | + "state_save_succeeded": False, |
| 52 | + "result": "not_posted_existing_receipt", |
| 53 | + } |
| 54 | + } |
| 55 | + sidecars["deferred_gaps"] = { |
| 56 | + "pull_request_review:77": { |
| 57 | + "source_event_key": "pull_request_review:77", |
| 58 | + "workflow_name": "Reviewer Bot PR Review Submitted Observer", |
| 59 | + "failure_kind": "diagnostic_only", |
| 60 | + "diagnostic_summary": "retained diagnostic gap", |
| 61 | + } |
| 62 | + } |
| 63 | + sidecars["observer_discovery_watermarks"] = { |
| 64 | + "review_submitted": { |
| 65 | + "workflow_name": "Reviewer Bot PR Review Submitted Observer", |
| 66 | + "last_scan_completed_at": "2026-04-14T01:00:00Z", |
| 67 | + "diagnostics_retained": True, |
| 68 | + } |
| 69 | + } |
| 70 | + sidecars["reconciled_source_events"] = { |
| 71 | + "issue_comment:210": { |
| 72 | + "source_event_key": "issue_comment:210", |
| 73 | + "issue_number": 264, |
| 74 | + "source_event_action": "created", |
| 75 | + "replay_decision": "pass_diagnostic_only", |
| 76 | + "mark_reconciled": False, |
| 77 | + "clear_gap": False, |
| 78 | + "reconciled_at": None, |
| 79 | + "diagnostic_reason": "plain_lgtm_comment_is_diagnostic_only", |
| 80 | + } |
| 81 | + } |
| 82 | + repair_records.store_repair_marker( |
| 83 | + review, |
| 84 | + "status_label_projection", |
| 85 | + { |
| 86 | + "kind": "projection_failure", |
| 87 | + "issue_number": 264, |
| 88 | + "repair_action": "repair-review-status-labels", |
| 89 | + "target_collection_mode": "issue_scoped", |
| 90 | + "status_projection_epoch": "status_projection_v2", |
| 91 | + "before_status_labels": ["status: awaiting reviewer response"], |
| 92 | + "desired_status_labels": ["status: reviewer reassignment needed"], |
| 93 | + "labels_added": ["status: reviewer reassignment needed"], |
| 94 | + "labels_removed": ["status: awaiting reviewer response"], |
| 95 | + "repaired_at": None, |
| 96 | + "result": "blocked", |
| 97 | + }, |
| 98 | + ) |
| 99 | + repair_records.repair_markers(review) |
| 100 | + before_state = deepcopy(state) |
| 101 | + |
| 102 | + comments = [ |
| 103 | + { |
| 104 | + "id": 210, |
| 105 | + "user": {"login": "iglesias"}, |
| 106 | + "created_at": "2026-04-13T23:23:25Z", |
| 107 | + "body": "LGTM", |
| 108 | + }, |
| 109 | + { |
| 110 | + "id": 9001, |
| 111 | + "user": {"login": "github-actions[bot]"}, |
| 112 | + "created_at": "2026-04-13T00:44:23Z", |
| 113 | + "body": "**Review Reminder**\n\ntransition period", |
| 114 | + }, |
| 115 | + { |
| 116 | + "id": 9002, |
| 117 | + "user": {"login": "github-actions[bot]"}, |
| 118 | + "created_at": "2026-04-14T00:44:23Z", |
| 119 | + "body": "**Review Reminder**\n\ntransition period", |
| 120 | + }, |
| 121 | + ] |
| 122 | + pr_payload = { |
| 123 | + **pull_request_payload(264, head_sha="head-live", author="manhatsu"), |
| 124 | + "requested_reviewers": [], |
| 125 | + "assignees": [], |
| 126 | + "labels": [{"name": "status: awaiting reviewer response"}], |
| 127 | + } |
| 128 | + routes = ( |
| 129 | + RouteGitHubApi() |
| 130 | + .add_request( |
| 131 | + "GET", |
| 132 | + "issues/264", |
| 133 | + status_code=200, |
| 134 | + payload={ |
| 135 | + "number": 264, |
| 136 | + "state": "open", |
| 137 | + "pull_request": {}, |
| 138 | + "labels": [{"name": "status: awaiting reviewer response"}], |
| 139 | + }, |
| 140 | + ) |
| 141 | + .add_request("GET", "pulls/264", status_code=200, payload=pr_payload) |
| 142 | + .add_pull_request_reviews( |
| 143 | + 264, |
| 144 | + [ |
| 145 | + review_payload( |
| 146 | + 501, |
| 147 | + state="APPROVED", |
| 148 | + submitted_at="2026-03-18T12:10:42Z", |
| 149 | + commit_id="head-live", |
| 150 | + author="plaindocs", |
| 151 | + ) |
| 152 | + ], |
| 153 | + ) |
| 154 | + .add_request("GET", "issues/264/comments?per_page=100&page=1", status_code=200, payload=comments) |
| 155 | + ) |
| 156 | + bot = FakeReviewerBotRuntime(monkeypatch) |
| 157 | + bot.ACTIVE_LEASE_CONTEXT = object() |
| 158 | + bot.github.stub(routes) |
| 159 | + bot.github.get_user_permission_status = lambda username, required_permission="push": "granted" |
| 160 | + bot.github.post_comment_result = lambda issue_number, body: (_ for _ in ()).throw( |
| 161 | + AssertionError("scheduled PR264 no-ping path must not post reviewer-facing comments") |
| 162 | + ) |
| 163 | + monkeypatch.setattr(maintenance_schedule, "sweep_deferred_gaps", lambda bot, current: False) |
| 164 | + monkeypatch.setattr( |
| 165 | + maintenance_schedule, |
| 166 | + "collect_status_projection_repair_items", |
| 167 | + lambda bot, current: (_ for _ in ()).throw(AssertionError("scheduled PR264 no-ping path broadened repair collection")), |
| 168 | + ) |
| 169 | + monkeypatch.setattr( |
| 170 | + reviews, |
| 171 | + "list_open_items_with_status_labels", |
| 172 | + lambda bot: (_ for _ in ()).throw(AssertionError("scheduled PR264 no-ping path reached broad status-label collection")), |
| 173 | + ) |
| 174 | + reminder_decisions = [] |
| 175 | + original_decide = overdue_lib.decide_overdue_reminder |
| 176 | + |
| 177 | + def record_reminder_decision(*args, **kwargs): |
| 178 | + decision = original_decide(*args, **kwargs) |
| 179 | + reminder_decisions.append(decision) |
| 180 | + return decision |
| 181 | + |
| 182 | + monkeypatch.setattr(overdue_lib, "decide_overdue_reminder", record_reminder_decision) |
| 183 | + |
| 184 | + result = maintenance_schedule.handle_scheduled_check_result(bot, state) |
| 185 | + |
| 186 | + assert result.state_changed is False |
| 187 | + assert result.touched_items == [] |
| 188 | + assert result.closed_cleanup_removed_items == () |
| 189 | + assert state == before_state |
| 190 | + assert reminder_decisions |
| 191 | + assert reminder_decisions[-1].action == "none" |
| 192 | + assert reminder_decisions[-1].reason == "legacy_duplicate_reminders_exhausted" |
| 193 | + assert all(call.method == "GET" for call in routes.request_calls) |
| 194 | + assert not any("labels" in call.endpoint for call in routes.request_calls) |
0 commit comments