Skip to content

Commit dd26318

Browse files
committed
fix: close pr264 semantic closure gap
1 parent cea0936 commit dd26318

5 files changed

Lines changed: 150 additions & 15 deletions

File tree

.github/workflows/reviewer-bot-sweeper-repair.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
required: true
1111
default: check-overdue
1212
type: choice
13-
options: [sync-members, show-state, check-overdue, repair-review-status-labels, preview-reviewer-board]
13+
options: [sync-members, show-state, check-overdue, repair-review-status-labels]
1414
issue_number:
1515
description: Optional issue or PR number for targeted reviewer-board preview
1616
required: false
@@ -70,8 +70,6 @@ jobs:
7070
WORKFLOW_RUN_ID: ${{ github.run_id }}
7171
WORKFLOW_NAME: ${{ github.workflow }}
7272
WORKFLOW_JOB_NAME: ${{ github.job }}
73-
REVIEWER_BOARD_ENABLED: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'preview-reviewer-board' && 'true' || 'false' }}
74-
REVIEWER_BOARD_TOKEN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'preview-reviewer-board' && secrets.REVIEWER_BOARD_TOKEN || '' }}
7573
run: uv run --project "$BOT_SRC_ROOT" reviewer-bot
7674
- name: Workflow summary
7775
run: |

tests/contract/reviewer_bot/test_preview_workflow_contracts.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ def test_preview_workflow_exposes_exact_dispatch_inputs_and_actions():
2727
assert workflow_dispatch["inputs"]["validation_nonce"]["type"] == "string"
2828

2929

30+
def test_preview_workflow_remains_sole_retained_owner_of_preview_actions():
31+
sweeper_text = Path(".github/workflows/reviewer-bot-sweeper-repair.yml").read_text(encoding="utf-8")
32+
sweeper_data = yaml.safe_load(sweeper_text)
33+
sweeper_on_block = sweeper_data.get("on", sweeper_data.get(True))
34+
sweeper_action_input = sweeper_on_block["workflow_dispatch"]["inputs"]["action"]
35+
36+
assert sweeper_action_input["options"] == [
37+
"sync-members",
38+
"show-state",
39+
"check-overdue",
40+
"repair-review-status-labels",
41+
]
42+
assert "REVIEWER_BOARD_ENABLED:" not in sweeper_text
43+
assert "REVIEWER_BOARD_TOKEN:" not in sweeper_text
44+
45+
3046
def test_preview_workflow_run_name_and_env_contract_are_frozen():
3147
text, data, _ = _load_preview_workflow()
3248

tests/contract/reviewer_bot/test_reviewer_board_workflow_contracts.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,21 @@
77
import yaml
88

99

10-
def test_sweeper_repair_workflow_exposes_reviewer_board_preview_dispatch():
10+
def test_sweeper_repair_workflow_removes_reviewer_board_preview_dispatch():
1111
data = yaml.safe_load(Path(".github/workflows/reviewer-bot-sweeper-repair.yml").read_text(encoding="utf-8"))
1212
on_block = data.get("on", data.get(True))
1313
workflow_dispatch = on_block["workflow_dispatch"]
1414
action_input = workflow_dispatch["inputs"]["action"]
15-
assert "preview-reviewer-board" in action_input["options"]
15+
assert "preview-reviewer-board" not in action_input["options"]
1616
issue_number_input = workflow_dispatch["inputs"]["issue_number"]
1717
assert issue_number_input["required"] is False
1818
assert issue_number_input["type"] == "string"
1919

20-
def test_sweeper_repair_workflow_scopes_reviewer_board_env_to_preview_only():
20+
def test_sweeper_repair_workflow_no_longer_exports_reviewer_board_preview_env():
2121
workflow_text = Path(".github/workflows/reviewer-bot-sweeper-repair.yml").read_text(encoding="utf-8")
2222
assert "ISSUE_NUMBER: ${{ github.event.inputs.issue_number }}" in workflow_text
23-
assert (
24-
"REVIEWER_BOARD_ENABLED: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'preview-reviewer-board' && 'true' || 'false' }}"
25-
in workflow_text
26-
)
27-
assert (
28-
"REVIEWER_BOARD_TOKEN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'preview-reviewer-board' && secrets.REVIEWER_BOARD_TOKEN || '' }}"
29-
in workflow_text
30-
)
23+
assert "REVIEWER_BOARD_ENABLED:" not in workflow_text
24+
assert "REVIEWER_BOARD_TOKEN:" not in workflow_text
3125

3226

3327
def test_sweeper_repair_workflow_exports_retained_manual_dispatch_env_contract():

tests/integration/reviewer_bot/test_app_reviewer_board_preview.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
from scripts.reviewer_bot_lib import review_state
66
from scripts.reviewer_bot_lib.config import GitHubApiResult
77
from tests.fixtures.app_harness import AppHarness
8-
from tests.fixtures.reviewer_bot import make_state, valid_reviewer_board_metadata
8+
from tests.fixtures.reviewer_bot import (
9+
make_state,
10+
make_tracked_review_state,
11+
pull_request_payload,
12+
review_payload,
13+
valid_reviewer_board_metadata,
14+
)
15+
from tests.fixtures.reviewer_bot_builders import accept_reviewer_review
16+
from tests.fixtures.reviewer_bot_fakes import RouteGitHubApi
917

1018
pytestmark = pytest.mark.integration
1119

@@ -159,3 +167,88 @@ def test_execute_run_preview_reviewer_board_is_read_only(monkeypatch, capsys):
159167
assert payload["state_save_attempted"] is False
160168
assert payload["tracked_state_mutations_attempted"] is False
161169
assert payload["touched_projection_attempted"] is False
170+
171+
172+
def test_execute_run_preview_reviewer_board_keeps_pr264_alternate_approval_projection(monkeypatch, capsys):
173+
harness = AppHarness(monkeypatch)
174+
harness.set_event(
175+
EVENT_NAME="workflow_dispatch",
176+
EVENT_ACTION="",
177+
MANUAL_ACTION="preview-reviewer-board",
178+
REVIEWER_BOARD_ENABLED="true",
179+
REVIEWER_BOARD_TOKEN="board-token",
180+
ISSUE_NUMBER=264,
181+
VALIDATION_NONCE="board-preview-pr264",
182+
GITHUB_SHA="workflow-head",
183+
)
184+
monkeypatch.setattr(harness.runtime, "_reviewer_board_project_metadata", None, raising=False)
185+
186+
state = make_state()
187+
review = make_tracked_review_state(
188+
state,
189+
264,
190+
reviewer="iglesias",
191+
assigned_at="2026-02-10T17:20:07Z",
192+
active_cycle_started_at="2026-02-10T17:20:07Z",
193+
)
194+
accept_reviewer_review(
195+
review,
196+
semantic_key="pull_request_review:77",
197+
timestamp="2026-03-18T01:09:05Z",
198+
actor="iglesias",
199+
reviewed_head_sha="head-old",
200+
)
201+
202+
routes = RouteGitHubApi().add_request(
203+
"GET",
204+
"issues/264",
205+
status_code=200,
206+
payload={"number": 264, "state": "open", "pull_request": {}, "labels": []},
207+
).add_request(
208+
"GET",
209+
"pulls/264",
210+
status_code=200,
211+
payload={
212+
**pull_request_payload(264, head_sha="head-live", author="manhatsu"),
213+
"requested_reviewers": [],
214+
"labels": [],
215+
},
216+
).add_pull_request_reviews(
217+
264,
218+
[review_payload(501, state="APPROVED", submitted_at="2026-03-18T12:10:42Z", commit_id="head-live", author="plaindocs")],
219+
)
220+
harness.runtime.github.stub(routes)
221+
harness.stub_load_state(lambda *, fail_on_unavailable=False: state)
222+
harness.stub_lock(acquire=lambda: (_ for _ in ()).throw(AssertionError("preview should not acquire lock")))
223+
harness.stub_pass_until(lambda current: (_ for _ in ()).throw(AssertionError("preview should skip pass-until processing")))
224+
harness.stub_sync_members(lambda current: (_ for _ in ()).throw(AssertionError("preview should skip member sync")))
225+
harness.stub_save_state(lambda current: (_ for _ in ()).throw(AssertionError("preview should not save state")))
226+
harness.stub_sync_status_labels(lambda current, issue_numbers: (_ for _ in ()).throw(AssertionError("preview should not sync labels")))
227+
monkeypatch.setattr(harness.runtime, "github_graphql", lambda query, variables=None, *, token=None: valid_reviewer_board_metadata())
228+
harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted"
229+
230+
result = harness.run_execute()
231+
232+
assert result.exit_code == 0
233+
payload = json.loads(capsys.readouterr().out)
234+
assert payload == {
235+
"schema_version": 1,
236+
"preview_action": "preview-reviewer-board",
237+
"issue_number": 264,
238+
"validation_nonce": "board-preview-pr264",
239+
"head_sha": "workflow-head",
240+
"workflow_path": ".github/workflows/reviewer-bot-preview.yml",
241+
"response_state": "awaiting_contributor_response",
242+
"reviewer_authority_outcome": "tracked_reviewer_confirmed",
243+
"suppression_reason": "current_head_alternate_approval_present",
244+
"current_scope_key": "reviewer=iglesias|head=head-live|cycle=2026-02-10T17:20:07Z|anchor=none",
245+
"current_scope_basis": "alternate_current_head_approval",
246+
"would_post_warning": False,
247+
"would_post_transition": False,
248+
"lock_attempted": False,
249+
"state_save_attempted": False,
250+
"tracked_state_mutations_attempted": False,
251+
"touched_projection_attempted": False,
252+
"board_attention": "No",
253+
"board_waiting_since": None,
254+
}

tests/unit/reviewer_bot/test_project_board.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,40 @@ def test_preview_board_projection_keeps_parity_with_refreshed_live_review_state(
216216
assert preview.desired.review_state == REVIEWER_BOARD_OPTION_AWAITING_CONTRIBUTOR
217217

218218

219+
def test_preview_board_projection_keeps_pr264_alternate_approval_boundary(monkeypatch):
220+
state = make_state()
221+
review = make_tracked_review_state(
222+
state,
223+
264,
224+
reviewer="iglesias",
225+
assigned_at="2026-02-10T17:20:07Z",
226+
active_cycle_started_at="2026-02-10T17:20:07Z",
227+
)
228+
accept_reviewer_review(
229+
review,
230+
semantic_key="pull_request_review:77",
231+
timestamp="2026-03-18T01:09:05Z",
232+
actor="iglesias",
233+
reviewed_head_sha="head-old",
234+
source_precedence=1,
235+
)
236+
routes = RouteGitHubApi().add_pull_request_snapshot(264, pull_request_payload(264, head_sha="head-live", author="manhatsu")).add_pull_request_reviews(
237+
264,
238+
[review_payload(501, state="APPROVED", submitted_at="2026-03-18T12:10:42Z", commit_id="head-live", author="plaindocs")],
239+
)
240+
runtime = _runtime(monkeypatch, routes)
241+
runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True)
242+
runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted"
243+
244+
preview = project_board.preview_board_projection_for_item(runtime, state, 264)
245+
246+
assert preview.classification == "open_tracked_assigned"
247+
assert preview.desired is not None
248+
assert preview.desired.review_state == REVIEWER_BOARD_OPTION_AWAITING_CONTRIBUTOR
249+
assert preview.desired.waiting_since is None
250+
assert preview.desired.needs_attention == "No"
251+
252+
219253
def test_preview_board_projection_marks_projection_repair_as_attention(monkeypatch):
220254
state = make_state()
221255
make_tracked_review_state(

0 commit comments

Comments
 (0)