scripts/test_gh_pr_watch.py
scripts/test_gh_pr_watch.pyBrowse 6 files
1,959 tokens
8,080 bytes
Token encoding: o200k_base
Snapshot 18d7ace
← Back to SKILL.md
1import argparse2import importlib.util3from pathlib import Path4 5import pytest6 7 8MODULE_PATH = Path(__file__).with_name("gh_pr_watch.py")9MODULE_SPEC = importlib.util.spec_from_file_location("gh_pr_watch", MODULE_PATH)10gh_pr_watch = importlib.util.module_from_spec(MODULE_SPEC)11assert MODULE_SPEC.loader is not None12MODULE_SPEC.loader.exec_module(gh_pr_watch)13 14 15def sample_pr():16 return {17 "number": 123,18 "url": "https://github.com/openai/codex/pull/123",19 "repo": "openai/codex",20 "head_sha": "abc123",21 "head_branch": "feature",22 "state": "OPEN",23 "merged": False,24 "closed": False,25 "mergeable": "MERGEABLE",26 "merge_state_status": "CLEAN",27 "review_decision": "",28 }29 30 31def sample_checks(**overrides):32 checks = {33 "pending_count": 0,34 "failed_count": 0,35 "passed_count": 12,36 "all_terminal": True,37 }38 checks.update(overrides)39 return checks40 41 42def test_collect_snapshot_fetches_review_items_before_ci(monkeypatch, tmp_path):43 call_order = []44 pr = sample_pr()45 46 monkeypatch.setattr(gh_pr_watch, "resolve_pr", lambda *args, **kwargs: pr)47 monkeypatch.setattr(gh_pr_watch, "load_state", lambda path: ({}, True))48 monkeypatch.setattr(49 gh_pr_watch,50 "get_authenticated_login",51 lambda: call_order.append("auth") or "octocat",52 )53 monkeypatch.setattr(54 gh_pr_watch,55 "fetch_new_review_items",56 lambda *args, **kwargs: call_order.append("review") or [],57 )58 monkeypatch.setattr(59 gh_pr_watch,60 "get_pr_checks",61 lambda *args, **kwargs: call_order.append("checks") or [],62 )63 monkeypatch.setattr(64 gh_pr_watch,65 "summarize_checks",66 lambda checks: call_order.append("summarize") or sample_checks(),67 )68 monkeypatch.setattr(69 gh_pr_watch,70 "get_workflow_runs_for_sha",71 lambda *args, **kwargs: call_order.append("workflow") or [],72 )73 monkeypatch.setattr(74 gh_pr_watch,75 "failed_runs_from_workflow_runs",76 lambda *args, **kwargs: call_order.append("failed_runs") or [],77 )78 monkeypatch.setattr(79 gh_pr_watch,80 "failed_jobs_from_workflow_runs",81 lambda *args, **kwargs: call_order.append("failed_jobs") or [],82 )83 monkeypatch.setattr(84 gh_pr_watch,85 "recommend_actions",86 lambda *args, **kwargs: call_order.append("recommend") or ["idle"],87 )88 monkeypatch.setattr(gh_pr_watch, "save_state", lambda *args, **kwargs: None)89 90 args = argparse.Namespace(91 pr="123",92 repo=None,93 state_file=str(tmp_path / "watcher-state.json"),94 max_flaky_retries=3,95 )96 97 gh_pr_watch.collect_snapshot(args)98 99 assert call_order.index("review") < call_order.index("checks")100 assert call_order.index("review") < call_order.index("workflow")101 102 103def test_recommend_actions_prioritizes_review_comments():104 actions = gh_pr_watch.recommend_actions(105 sample_pr(),106 sample_checks(failed_count=1),107 [{"run_id": 99}],108 [],109 [{"kind": "review_comment", "id": "1"}],110 0,111 3,112 )113 114 assert actions == [115 "process_review_comment",116 "diagnose_ci_failure",117 "retry_failed_checks",118 ]119 120 121def test_pending_review_feedback_surfaces_only_after_publication(monkeypatch):122 state = {123 "seen_review_comment_ids": ["20"],124 "seen_review_ids": ["10"],125 }126 review = {127 "id": 10,128 "user": {"login": "octocat"},129 "author_association": "MEMBER",130 "state": "PENDING",131 "body": "Please rename this.",132 "created_at": "2026-06-08T10:00:00Z",133 "submitted_at": None,134 "html_url": "https://github.com/openai/codex/pull/123#pullrequestreview-10",135 }136 review_comment = {137 "id": 20,138 "pull_request_review_id": 10,139 "user": {"login": "octocat"},140 "author_association": "MEMBER",141 "body": "Please rename this.",142 "created_at": "2026-06-08T10:00:00Z",143 "path": "src/example.rs",144 "line": 7,145 "html_url": "https://github.com/openai/codex/pull/123#discussion_r20",146 }147 148 def fake_list(endpoint, **kwargs):149 if endpoint.endswith("/issues/123/comments"):150 return []151 if endpoint.endswith("/pulls/123/comments"):152 return [review_comment]153 if endpoint.endswith("/pulls/123/reviews"):154 return [review]155 raise AssertionError(f"unexpected endpoint: {endpoint}")156 157 monkeypatch.setattr(gh_pr_watch, "gh_api_list_paginated", fake_list)158 159 assert (160 gh_pr_watch.fetch_new_review_items(161 sample_pr(),162 state,163 fresh_state=True,164 authenticated_login="octocat",165 )166 == []167 )168 assert state["seen_review_comment_ids"] == []169 assert state["seen_review_ids"] == []170 171 review["state"] = "COMMENTED"172 review["submitted_at"] = "2026-06-08T10:05:00Z"173 174 published_items = gh_pr_watch.fetch_new_review_items(175 sample_pr(),176 state,177 fresh_state=False,178 authenticated_login="octocat",179 )180 181 assert {(item["kind"], item["id"]) for item in published_items} == {182 ("review", "10"),183 ("review_comment", "20"),184 }185 assert state["seen_review_comment_ids"] == ["20"]186 assert state["seen_review_ids"] == ["10"]187 188 189def test_run_watch_keeps_polling_open_ready_to_merge_pr(monkeypatch):190 sleeps = []191 events = []192 snapshot = {193 "pr": sample_pr(),194 "checks": sample_checks(),195 "failed_runs": [],196 "failed_jobs": [],197 "new_review_items": [],198 "actions": ["ready_to_merge"],199 "retry_state": {200 "current_sha_retries_used": 0,201 "max_flaky_retries": 3,202 },203 }204 205 monkeypatch.setattr(206 gh_pr_watch,207 "collect_snapshot",208 lambda args: (snapshot, Path("/tmp/codex-babysit-pr-state.json")),209 )210 monkeypatch.setattr(211 gh_pr_watch,212 "print_event",213 lambda event, payload: events.append((event, payload)),214 )215 216 class StopWatch(Exception):217 pass218 219 def fake_sleep(seconds):220 sleeps.append(seconds)221 if len(sleeps) >= 2:222 raise StopWatch223 224 monkeypatch.setattr(gh_pr_watch.time, "sleep", fake_sleep)225 226 with pytest.raises(StopWatch):227 gh_pr_watch.run_watch(argparse.Namespace(poll_seconds=30))228 229 assert sleeps == [30, 30]230 assert [event for event, _ in events] == ["snapshot", "snapshot"]231 232 233def test_failed_jobs_include_direct_logs_endpoint(monkeypatch):234 jobs_by_run = {235 99: [236 {237 "id": 555,238 "name": "unit tests",239 "status": "completed",240 "conclusion": "failure",241 "html_url": "https://github.com/openai/codex/actions/runs/99/job/555",242 },243 {244 "id": 556,245 "name": "lint",246 "status": "completed",247 "conclusion": "success",248 },249 ]250 }251 252 monkeypatch.setattr(253 gh_pr_watch,254 "get_jobs_for_run",255 lambda repo, run_id: jobs_by_run[run_id],256 )257 258 failed_jobs = gh_pr_watch.failed_jobs_from_workflow_runs(259 "openai/codex",260 [261 {262 "id": 99,263 "name": "CI",264 "status": "in_progress",265 "conclusion": "",266 "head_sha": "abc123",267 }268 ],269 "abc123",270 )271 272 assert failed_jobs == [273 {274 "run_id": 99,275 "workflow_name": "CI",276 "run_status": "in_progress",277 "run_conclusion": "",278 "job_id": 555,279 "job_name": "unit tests",280 "status": "completed",281 "conclusion": "failure",282 "html_url": "https://github.com/openai/codex/actions/runs/99/job/555",283 "logs_endpoint": "repos/openai/codex/actions/jobs/555/logs",284 }285 ]286 