diff --git a/scripts/reviewer_bot_lib/bootstrap_runtime.py b/scripts/reviewer_bot_lib/bootstrap_runtime.py index d2b750aaa..a1b1c200e 100644 --- a/scripts/reviewer_bot_lib/bootstrap_runtime.py +++ b/scripts/reviewer_bot_lib/bootstrap_runtime.py @@ -53,6 +53,9 @@ def github_api_request(self, *args, **kwargs): def github_api(self, *args, **kwargs): return github_api.github_api(self._runtime_getter(), *args, **kwargs) + def github_graphql_request(self, *args, **kwargs): + return github_api.github_graphql_request(self._runtime_getter(), *args, **kwargs) + def get_github_token(self): return github_api.get_github_token(self._runtime_getter()) diff --git a/scripts/reviewer_bot_lib/runtime.py b/scripts/reviewer_bot_lib/runtime.py index c292be9b1..0fc57ae5d 100644 --- a/scripts/reviewer_bot_lib/runtime.py +++ b/scripts/reviewer_bot_lib/runtime.py @@ -46,6 +46,8 @@ STATE_READ_RETRY_LIMIT_ENV, STATUS_PROJECTION_EPOCH, TRANSITION_PERIOD_DAYS, + AssignmentAttempt, + GitHubApiResult, ) @@ -230,7 +232,9 @@ class ReviewerBotRuntime: BOT_MENTION = BOT_MENTION COMMANDS = COMMANDS FLS_AUDIT_LABEL = FLS_AUDIT_LABEL + AssignmentAttempt = AssignmentAttempt AUTHOR_ASSOCIATION_TRUST_ALLOWLIST = AUTHOR_ASSOCIATION_TRUST_ALLOWLIST + GitHubApiResult = GitHubApiResult REVIEWER_REQUEST_422_TEMPLATE = REVIEWER_REQUEST_422_TEMPLATE REVIEW_FRESHNESS_RUNBOOK_PATH = REVIEW_FRESHNESS_RUNBOOK_PATH REVIEW_DEADLINE_DAYS = REVIEW_DEADLINE_DAYS @@ -365,6 +369,9 @@ def github_api_request(self, *args, **kwargs): def github_api(self, *args, **kwargs): return self.github.github_api(*args, **kwargs) + def github_graphql_request(self, *args, **kwargs): + return self.adapters.github.github_graphql_request(*args, **kwargs) + def collect_touched_item(self, issue_number: int | None) -> None: self.touch_tracker.collect(issue_number) diff --git a/scripts/reviewer_bot_lib/runtime_protocols.py b/scripts/reviewer_bot_lib/runtime_protocols.py index 7e9bb84ab..c3a10ca1a 100644 --- a/scripts/reviewer_bot_lib/runtime_protocols.py +++ b/scripts/reviewer_bot_lib/runtime_protocols.py @@ -487,19 +487,14 @@ class CommentApplicationRuntimeContext(Protocol): BOT_MENTION: str github: CommentGitHubWriteContext - def get_config_value(self, name: str, default: str = "") -> str: ... - @runtime_checkable class CommentRoutingRuntimeContext(Protocol): BOT_NAME: str BOT_MENTION: str - AUTHOR_ASSOCIATION_TRUST_ALLOWLIST: tuple[str, ...] logger: Logger adapters: CommentRoutingAdaptersContext def assert_lock_held(self, context: str) -> None: ... def collect_touched_item(self, issue_number: int | None) -> None: ... - - def github_api(self, *args, **kwargs) -> Any | None: ... diff --git a/tests/contract/reviewer_bot/test_adapter_contract.py b/tests/contract/reviewer_bot/test_adapter_contract.py index d03e1438c..ee07cb737 100644 --- a/tests/contract/reviewer_bot/test_adapter_contract.py +++ b/tests/contract/reviewer_bot/test_adapter_contract.py @@ -9,6 +9,7 @@ from scripts import reviewer_bot from scripts.reviewer_bot_lib import event_inputs, lease_lock, review_state +from scripts.reviewer_bot_lib.config import AssignmentAttempt, GitHubApiResult from scripts.reviewer_bot_lib.runtime import ReviewerBotRuntime, StdErrLogger from scripts.reviewer_bot_lib.runtime_protocols import ( CommentApplicationRuntimeContext, @@ -20,6 +21,7 @@ ReconcileWorkflowRuntimeContext, ) from tests.fixtures.fake_runtime import FakeReviewerBotRuntime +from tests.fixtures.http_responses import FakeGitHubResponse from tests.fixtures.reviewer_bot import make_state @@ -97,6 +99,29 @@ def test_runtime_head_repair_contract_is_runtime_scoped(): assert hints["return"].__name__ == "HeadObservationRepairResult" +def test_bootstrapped_runtime_re_exposes_retained_github_type_homes(): + runtime = reviewer_bot._runtime_bot() + + assert runtime.AssignmentAttempt is AssignmentAttempt + assert runtime.GitHubApiResult is GitHubApiResult + + +def test_bootstrapped_runtime_supports_typed_rest_and_assignment_paths(monkeypatch): + runtime = reviewer_bot._runtime_bot() + monkeypatch.setenv("GITHUB_TOKEN", "token") + runtime.rest_transport = SimpleNamespace( + request=lambda *args, **kwargs: FakeGitHubResponse(201, {"ok": True}, "ok") + ) + + response = runtime.github_api_request("GET", "issues/42") + attempt = runtime.github.request_pr_reviewer_assignment(42, "alice") + + assert isinstance(response, GitHubApiResult) + assert response.ok is True + assert isinstance(attempt, AssignmentAttempt) + assert attempt.success is True + + def _protocol_member_names(protocol_type) -> set[str]: return set(protocol_type.__annotations__) | { name @@ -546,6 +571,9 @@ def test_f2a_runtime_surface_inventory_fixture_records_retained_triples(): capabilities = {entry["capability"]: entry for entry in inventory["capability_triples"]} assert capabilities["comment-event dispatch"]["classification"] == "retained final surface" + assert capabilities["typed REST request result"]["classification"] == "retained final surface" + assert capabilities["typed assignment helper result"]["classification"] == "retained final surface" + assert capabilities["reviewer-board GraphQL metadata read"]["classification"] == "retained final surface" assert "workflow-run dispatch" not in capabilities assert "refresh reviewer review from live preferred review" not in capabilities assert "repair missing reviewer review state" not in capabilities @@ -571,6 +599,13 @@ def test_f2a_runtime_surface_inventory_matches_bootstrap_adapter_examples(): assert capabilities["mandatory approver satisfaction"]["bootstrap_adapter"].endswith( "satisfy_mandatory_approver_requirement" ) + assert capabilities["typed REST request result"]["bootstrap_adapter"].endswith("github_api_request") + assert capabilities["typed assignment helper result"]["bootstrap_adapter"].endswith( + "request_pr_reviewer_assignment" + ) + assert capabilities["reviewer-board GraphQL metadata read"]["bootstrap_adapter"].endswith( + "github_graphql_request" + ) def test_f2c_no_runtime_surface_triples_are_deletion_ready_yet(): diff --git a/tests/contract/reviewer_bot/test_comment_application_contract.py b/tests/contract/reviewer_bot/test_comment_application_contract.py index 73d33cf25..c25e2c5b9 100644 --- a/tests/contract/reviewer_bot/test_comment_application_contract.py +++ b/tests/contract/reviewer_bot/test_comment_application_contract.py @@ -47,8 +47,8 @@ def test_comment_application_stores_pending_privileged_command_from_typed_reques comment_author="dana", comment_body="@guidelines-bot /accept-no-fls-changes", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" - harness.runtime.post_comment = lambda issue_number, body: True + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.post_comment = lambda issue_number, body: True changed = comment_application.process_comment_event( harness.runtime, @@ -77,8 +77,8 @@ def test_comment_application_freezes_pending_privileged_command_metadata_shape_a comment_body="@guidelines-bot /accept-no-fls-changes", ) harness.runtime.set_config_value("STATE_ISSUE_NUMBER", "314") - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" - harness.runtime.post_comment = lambda issue_number, body: posted.append((issue_number, body)) or True + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.post_comment = lambda issue_number, body: posted.append((issue_number, body)) or True changed = comment_application.process_comment_event( harness.runtime, @@ -170,7 +170,7 @@ def test_n1_comment_application_obeys_policy_selected_handler_without_inline_com "handle_queue_command", lambda bot, current_state: calls.append((bot, current_state)) or ("", True), ) - harness.runtime.add_reaction = lambda *args, **kwargs: True + harness.runtime.github.add_reaction = lambda *args, **kwargs: True changed = comment_application.process_comment_event( harness.runtime, diff --git a/tests/contract/reviewer_bot/test_fake_runtime_contract.py b/tests/contract/reviewer_bot/test_fake_runtime_contract.py index 00b0ed4bf..e00117fa4 100644 --- a/tests/contract/reviewer_bot/test_fake_runtime_contract.py +++ b/tests/contract/reviewer_bot/test_fake_runtime_contract.py @@ -155,6 +155,21 @@ def test_fake_runtime_optional_lock_hooks_are_replaceable(monkeypatch): assert calls == ["acquire", "release"] +def test_fake_runtime_exposes_retained_runtime_labels_and_lock_helpers(monkeypatch): + runtime = FakeReviewerBotRuntime(monkeypatch) + + assert runtime.COMMANDS + assert runtime.REVIEW_LABELS + assert hasattr(runtime, "github_graphql_request") + assert hasattr(runtime, "normalize_lock_metadata") + assert hasattr(runtime, "get_state_issue") + assert hasattr(runtime, "get_state_issue_snapshot") + assert hasattr(runtime, "conditional_patch_state_issue") + assert hasattr(runtime, "render_state_issue_body") + assert hasattr(runtime, "get_lock_ref_snapshot") + assert hasattr(runtime, "renew_state_issue_lease_lock") + + def test_fake_runtime_uses_explicit_public_service_fields(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) @@ -322,6 +337,11 @@ def test_f2a_runtime_surface_inventory_matches_fake_runtime_branch_examples(): assert capabilities["privileged accept-no-fls-changes execution"]["fake_runtime_branch"].endswith( "handle_accept_no_fls_changes_command" ) + assert capabilities["typed REST request result"]["fake_runtime_branch"].endswith("GitHubApiResult") + assert capabilities["typed assignment helper result"]["fake_runtime_branch"].endswith("AssignmentAttempt") + assert capabilities["reviewer-board GraphQL metadata read"]["fake_runtime_branch"].endswith( + "github_graphql_request" + ) assert capabilities["mandatory approver satisfaction"]["fake_runtime_branch"].endswith( "satisfy_mandatory_approver_requirement" ) diff --git a/tests/contract/reviewer_bot/test_runtime_protocols.py b/tests/contract/reviewer_bot/test_runtime_protocols.py index bd2d41f4b..607820f04 100644 --- a/tests/contract/reviewer_bot/test_runtime_protocols.py +++ b/tests/contract/reviewer_bot/test_runtime_protocols.py @@ -21,6 +21,11 @@ def _assert_core_runtime_surface(runtime) -> None: assert hasattr(runtime, "get_config_value") + assert hasattr(runtime, "AssignmentAttempt") + assert hasattr(runtime, "GitHubApiResult") + assert hasattr(runtime, "COMMANDS") + assert hasattr(runtime, "REVIEW_LABELS") + assert hasattr(runtime, "github_graphql_request") assert hasattr(runtime, "infra") assert hasattr(runtime, "domain") assert runtime.infra.config is runtime.config @@ -34,6 +39,12 @@ def _assert_core_runtime_surface(runtime) -> None: assert runtime.domain.handlers is runtime.handlers +def test_fake_runtime_default_lock_state_matches_production_contract(monkeypatch): + runtime = FakeReviewerBotRuntime(monkeypatch) + + assert runtime.ACTIVE_LEASE_CONTEXT is None + + def test_runtime_bot_returns_concrete_runtime_object(): runtime = reviewer_bot._runtime_bot() diff --git a/tests/fixtures/app_harness.py b/tests/fixtures/app_harness.py index ec898d644..c3302fef6 100644 --- a/tests/fixtures/app_harness.py +++ b/tests/fixtures/app_harness.py @@ -43,7 +43,18 @@ def stub_save_state(self, func) -> None: self.state_store.stub_save(func) def stub_lock(self, *, acquire=None, release=None, refresh=None) -> None: - self.locks.stub(acquire=acquire, release=release, refresh=refresh) + def wrapped_acquire(): + context = acquire() if acquire is not None else object() + self.runtime.ACTIVE_LEASE_CONTEXT = context if context is not None else object() + return context + + def wrapped_release(): + result = release() if release is not None else True + if result: + self.runtime.ACTIVE_LEASE_CONTEXT = None + return result + + self.locks.stub(acquire=wrapped_acquire, release=wrapped_release, refresh=refresh) def stub_handler(self, name: str, func) -> None: self.handlers.stub(name, func) diff --git a/tests/fixtures/commands_harness.py b/tests/fixtures/commands_harness.py index 3b65b7435..089e414f1 100644 --- a/tests/fixtures/commands_harness.py +++ b/tests/fixtures/commands_harness.py @@ -118,17 +118,17 @@ def capture_comment_side_effects(self): return record_comment_side_effects(self.runtime) def stub_assignees(self, assignees): - self.runtime.get_issue_assignees = lambda issue_number: assignees + self.runtime.github.get_issue_assignees = lambda issue_number: assignees def stub_assignment(self, *, success: bool = True, status_code: int = 201): def attempt(issue_number, username): return AssignmentAttempt(success=success, status_code=status_code) - self.runtime.request_pr_reviewer_assignment = attempt - self.runtime.assign_issue_assignee = attempt + self.runtime.github.request_pr_reviewer_assignment = attempt + self.runtime.github.assign_issue_assignee = attempt def stub_permission(self, status: str) -> None: - self.runtime.get_user_permission_status = lambda username, required_permission="triage": status + self.runtime.github.get_user_permission_status = lambda username, required_permission="triage": status def stub_handler(self, name: str, func) -> None: self.handlers.stub(name, func) @@ -136,7 +136,7 @@ def stub_handler(self, name: str, func) -> None: def automation_runner(self) -> AutomationRunner: runner = AutomationRunner() self._monkeypatch.setattr(automation_module, "run_command", runner.run) - self.runtime.run_command = runner.run + self.runtime.adapters.automation.run_command = runner.run return runner def assignment_request(self, *, issue_number: int): diff --git a/tests/fixtures/comment_routing_harness.py b/tests/fixtures/comment_routing_harness.py index 93035ee26..ad9ace653 100644 --- a/tests/fixtures/comment_routing_harness.py +++ b/tests/fixtures/comment_routing_harness.py @@ -22,6 +22,7 @@ def __init__(self, monkeypatch): self._monkeypatch = monkeypatch self.github = RouteGitHubApi() self.runtime = FakeReviewerBotRuntime(monkeypatch, github=self.github) + self.runtime.ACTIVE_LEASE_CONTEXT = object() self.config = self.runtime.config self.handlers = self.runtime.handlers diff --git a/tests/fixtures/equivalence/runtime_surface/triple_inventory.json b/tests/fixtures/equivalence/runtime_surface/triple_inventory.json index cea35cf96..9a883ef78 100644 --- a/tests/fixtures/equivalence/runtime_surface/triple_inventory.json +++ b/tests/fixtures/equivalence/runtime_surface/triple_inventory.json @@ -85,6 +85,27 @@ "bootstrap_adapter": "scripts/reviewer_bot_lib/bootstrap_runtime.py:_BootstrapCommandAdapterServices.handle_accept_no_fls_changes_command", "fake_runtime_branch": "tests/fixtures/fake_runtime.py:handle_accept_no_fls_changes_command", "classification": "retained final surface" + }, + { + "capability": "typed REST request result", + "runtime_forwarder": "scripts/reviewer_bot_lib/runtime.py:GitHubApiResult", + "bootstrap_adapter": "scripts/reviewer_bot_lib/bootstrap_runtime.py:_BootstrapGitHubServices.github_api_request", + "fake_runtime_branch": "tests/fixtures/fake_runtime.py:GitHubApiResult", + "classification": "retained final surface" + }, + { + "capability": "typed assignment helper result", + "runtime_forwarder": "scripts/reviewer_bot_lib/runtime.py:AssignmentAttempt", + "bootstrap_adapter": "scripts/reviewer_bot_lib/bootstrap_runtime.py:_BootstrapGitHubServices.request_pr_reviewer_assignment", + "fake_runtime_branch": "tests/fixtures/fake_runtime.py:AssignmentAttempt", + "classification": "retained final surface" + }, + { + "capability": "reviewer-board GraphQL metadata read", + "runtime_forwarder": "scripts/reviewer_bot_lib/runtime.py:github_graphql_request", + "bootstrap_adapter": "scripts/reviewer_bot_lib/bootstrap_runtime.py:_BootstrapGitHubServices.github_graphql_request", + "fake_runtime_branch": "tests/fixtures/fake_runtime.py:github_graphql_request", + "classification": "retained final surface" } ] } diff --git a/tests/fixtures/fake_runtime.py b/tests/fixtures/fake_runtime.py index 372aae27d..48031f4ab 100644 --- a/tests/fixtures/fake_runtime.py +++ b/tests/fixtures/fake_runtime.py @@ -22,6 +22,7 @@ AUTHOR_ASSOCIATION_TRUST_ALLOWLIST, BOT_MENTION, BOT_NAME, + COMMANDS, DEFERRED_DISCOVERY_BOOTSTRAP_WINDOW_SECONDS, DEFERRED_DISCOVERY_OVERLAP_SECONDS, EVENT_INTENT_MUTATING, @@ -42,8 +43,11 @@ LOCK_RENEWAL_WINDOW_SECONDS_ENV, LOCK_RETRY_BASE_SECONDS, LOCK_RETRY_BASE_SECONDS_ENV, + REVIEW_DEADLINE_DAYS, REVIEW_FRESHNESS_RUNBOOK_PATH, + REVIEW_LABELS, REVIEWER_REQUEST_422_TEMPLATE, + STATE_ISSUE_NUMBER, STATE_READ_RETRY_BASE_SECONDS, STATE_READ_RETRY_BASE_SECONDS_ENV, STATE_READ_RETRY_LIMIT, @@ -123,11 +127,6 @@ def sync_status_labels_for_items(self, state: dict, issue_numbers): return self._runtime.workflow.sync_status_labels_for_items(state, issue_numbers) -def runtime_instance_override(runtime: "FakeReviewerBotRuntime", name: str): - override = runtime.__dict__.get(name) - return override if callable(override) else None - - class FakeRuntimeGitHubCompatibility: def __init__(self, runtime: "FakeReviewerBotRuntime"): self._runtime = runtime @@ -194,9 +193,6 @@ def __init__(self, runtime: "FakeReviewerBotRuntime"): self._runtime = runtime def maybe_record_head_observation_repair(self, issue_number: int, review_data: dict): - override = runtime_instance_override(self._runtime, "maybe_record_head_observation_repair") - if override is not None: - return override(issue_number, review_data) return lifecycle_module.maybe_record_head_observation_repair(self._runtime, issue_number, review_data) def handle_transition_notice(self, state: dict, issue_number: int, reviewer: str) -> bool: @@ -269,9 +265,6 @@ def reposition_member_as_next(self, state: dict, username: str) -> bool: return queue_module.reposition_member_as_next(state, username) def compute_reviewer_response_state(self, issue_number: int, state: dict, *, issue_snapshot=None): - override = runtime_instance_override(self._runtime, "compute_reviewer_response_state") - if override is not None: - return override(issue_number, state, issue_snapshot=issue_snapshot) return reviews_module.compute_reviewer_response_state(self._runtime, issue_number, state, issue_snapshot=issue_snapshot) @@ -339,47 +332,25 @@ def __init__(self, runtime: "FakeReviewerBotRuntime"): self._runtime = runtime def run_command(self, command, cwd, check=False): - override = runtime_instance_override(self._runtime, "run_command") - if override is not None: - return override(command, cwd, check=check) return automation_module.run_command(command, cwd=cwd, check=check) def summarize_output(self, result, limit: int = 20) -> str: - override = runtime_instance_override(self._runtime, "summarize_output") - if override is not None: - return override(result, limit) return automation_module.summarize_output(result, limit=limit) def list_changed_files(self, repo_root): - override = runtime_instance_override(self._runtime, "list_changed_files") - if override is not None: - return override(repo_root) return automation_module.list_changed_files(repo_root) def get_default_branch(self) -> str: - override = runtime_instance_override(self._runtime, "get_default_branch") - if override is not None: - return override() return automation_module.get_default_branch(self._runtime) def find_open_pr_for_branch_status(self, branch: str): - override = runtime_instance_override(self._runtime, "find_open_pr_for_branch_status") - if override is not None: - return override(branch) return automation_module.find_open_pr_for_branch_status(self._runtime, branch) def create_pull_request(self, branch: str, base: str, issue_number: int): - override = runtime_instance_override(self._runtime, "create_pull_request") - if override is not None: - return override(branch, base, issue_number) return automation_module.create_pull_request(self._runtime, branch, base, issue_number) def fetch_members(self): - override = runtime_instance_override(self._runtime, "fetch_members") - if override is not None: - result = override() - else: - result = self._runtime._fetch_members() + result = self._runtime._fetch_members() if isinstance(result, list): return MemberFetchResult(ok=True, producers=result) return result @@ -391,11 +362,13 @@ def handle_accept_no_fls_changes_command(self, issue_number: int, comment_author class FakeReviewerBotRuntime: BOT_NAME = BOT_NAME BOT_MENTION = BOT_MENTION + COMMANDS = COMMANDS FLS_AUDIT_LABEL = FLS_AUDIT_LABEL AUTHOR_ASSOCIATION_TRUST_ALLOWLIST = AUTHOR_ASSOCIATION_TRUST_ALLOWLIST + REVIEW_LABELS = REVIEW_LABELS REVIEWER_REQUEST_422_TEMPLATE = REVIEWER_REQUEST_422_TEMPLATE REVIEW_FRESHNESS_RUNBOOK_PATH = REVIEW_FRESHNESS_RUNBOOK_PATH - REVIEW_DEADLINE_DAYS = 14 + REVIEW_DEADLINE_DAYS = REVIEW_DEADLINE_DAYS TRANSITION_PERIOD_DAYS = TRANSITION_PERIOD_DAYS DEFERRED_DISCOVERY_OVERLAP_SECONDS = DEFERRED_DISCOVERY_OVERLAP_SECONDS DEFERRED_DISCOVERY_BOOTSTRAP_WINDOW_SECONDS = DEFERRED_DISCOVERY_BOOTSTRAP_WINDOW_SECONDS @@ -417,9 +390,9 @@ def __init__(self, monkeypatch, *, github=None): self.jitter = DeterministicJitter(0.0) self.uuid_source = FixedUuidSource("fake-runtime-uuid") self.logger = RecordingLogger() - self.ACTIVE_LEASE_CONTEXT = object() + self.ACTIVE_LEASE_CONTEXT = None self.config = ConfigBag(monkeypatch) - self.config.set("STATE_ISSUE_NUMBER", "314") + self.config.set("STATE_ISSUE_NUMBER", str(STATE_ISSUE_NUMBER)) self.outputs = OutputCapture() self.deferred_payloads = DeferredPayloadStore() self.github = GitHubStub(github) @@ -502,7 +475,7 @@ def lock_renewal_window_seconds(self) -> int: return int(self.get_config_value(LOCK_RENEWAL_WINDOW_SECONDS_ENV, str(LOCK_RENEWAL_WINDOW_SECONDS)) or 0) def lock_ref_name(self) -> str: - return self.get_config_value(LOCK_REF_NAME_ENV, f"refs/{LOCK_REF_NAME}") + return self.get_config_value(LOCK_REF_NAME_ENV, LOCK_REF_NAME) def lock_ref_bootstrap_branch(self) -> str: return self.get_config_value(LOCK_REF_BOOTSTRAP_BRANCH_ENV, LOCK_REF_BOOTSTRAP_BRANCH) @@ -520,6 +493,49 @@ def load_deferred_payload(self) -> dict: def parse_iso8601_timestamp(self, value: Any): return self.compat.state_lock.parse_iso8601_timestamp(value) + def normalize_lock_metadata(self, lock_meta: dict | None): + return self.compat.state_lock.normalize_lock_metadata(lock_meta) + + def get_state_issue(self): + return self.compat.state_lock.get_state_issue() + + def clear_lock_metadata(self): + return self.compat.state_lock.clear_lock_metadata() + + def get_state_issue_snapshot(self): + return self.compat.state_lock.get_state_issue_snapshot() + + def conditional_patch_state_issue(self, body: str, etag: str | None = None): + return self.compat.state_lock.conditional_patch_state_issue(body, etag) + + def render_state_issue_body(self, state: dict, base_body: str | None = None, *, preserve_state_block: bool = False): + return self.compat.state_lock.render_state_issue_body( + state, + base_body, + preserve_state_block=preserve_state_block, + ) + + def get_state_issue_html_url(self): + return self.compat.state_lock.get_state_issue_html_url() + + def get_lock_ref_display(self): + return self.compat.state_lock.get_lock_ref_display() + + def get_lock_ref_snapshot(self): + return self.compat.state_lock.get_lock_ref_snapshot() + + def build_lock_metadata(self, *args, **kwargs): + return self.compat.state_lock.build_lock_metadata(*args, **kwargs) + + def create_lock_commit(self, parent_sha, tree_sha, lock_meta): + return self.compat.state_lock.create_lock_commit(parent_sha, tree_sha, lock_meta) + + def cas_update_lock_ref(self, new_sha): + return self.compat.state_lock.cas_update_lock_ref(new_sha) + + def lock_is_currently_valid(self, lock_meta: dict, now=None): + return self.compat.state_lock.lock_is_currently_valid(lock_meta, now) + def satisfy_mandatory_approver_requirement(self, state: dict, issue_number: int, approver: str) -> bool: return reviews_module.satisfy_mandatory_approver_requirement(self, state, issue_number, approver) @@ -532,6 +548,9 @@ def save_state(self, state: dict) -> bool: def ensure_state_issue_lease_lock_fresh(self) -> bool: return self.compat.state_lock.ensure_state_issue_lease_lock_fresh() + def renew_state_issue_lease_lock(self, context): + return self.compat.state_lock.renew_state_issue_lease_lock(context) + def acquire_state_issue_lease_lock(self): context = self.compat.state_lock.acquire_state_issue_lease_lock() if context is not None: @@ -671,6 +690,26 @@ def rebuild_pr_approval_state(self, *args, pull_request=None, reviews=None): def get_github_graphql_token(self, *, prefer_board_token: bool = False) -> str: return self.compat.github.get_github_graphql_token(prefer_board_token=prefer_board_token) + def github_graphql_request( + self, + query: str, + variables=None, + *, + token=None, + retry_policy: str = "none", + timeout_seconds: float | None = None, + suppress_error_log: bool = False, + ): + return github_api_module.github_graphql_request( + self, + query, + variables, + token=token, + retry_policy=retry_policy, + timeout_seconds=timeout_seconds, + suppress_error_log=suppress_error_log, + ) + def github_graphql(self, query: str, variables=None, *, token=None): return self.compat.github.github_graphql(query, variables, token=token) @@ -762,15 +801,9 @@ def handle_scheduled_check_result(self, state: dict): return self.handlers.call("handle_scheduled_check_result", state) def github_api(self, method: str, endpoint: str, data=None): - override = runtime_instance_override(self, "github_api") - if override is not None: - return override(method, endpoint, data=data) return self.github.github_api(method, endpoint, data=data) def github_api_request(self, method: str, endpoint: str, data=None, extra_headers=None, **kwargs): - override = runtime_instance_override(self, "github_api_request") - if override is not None: - return override(method, endpoint, data=data, extra_headers=extra_headers, **kwargs) return self.github.github_api_request( method, endpoint, diff --git a/tests/fixtures/focused_fake_services.py b/tests/fixtures/focused_fake_services.py index 6ac089d8c..3e57e39a3 100644 --- a/tests/fixtures/focused_fake_services.py +++ b/tests/fixtures/focused_fake_services.py @@ -147,159 +147,93 @@ def _runtime_required(self): raise AssertionError("GitHubStub runtime not bound") return self._runtime - def _instance_override(self, name: str): - runtime = self._runtime_required() - override = runtime.__dict__.get(name) - return override if callable(override) else None - def get_github_token(self): - override = self._instance_override("get_github_token") - if override is not None: - return override() from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.get_github_token(self._runtime_required()) def get_github_graphql_token(self, *, prefer_board_token=False): - override = self._instance_override("get_github_graphql_token") - if override is not None: - return override(prefer_board_token=prefer_board_token) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.get_github_graphql_token(self._runtime_required(), prefer_board_token=prefer_board_token) def github_graphql(self, query, variables=None, *, token=None): - override = self._instance_override("github_graphql") - if override is not None: - return override(query, variables, token=token) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.github_graphql(self._runtime_required(), query, variables, token=token) def post_comment(self, issue_number: int, body: str): - override = self._instance_override("post_comment") - if override is not None: - return override(issue_number, body) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.post_comment(self._runtime_required(), issue_number, body) def get_repo_labels(self): - override = self._instance_override("get_repo_labels") - if override is not None: - return override() from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.get_repo_labels(self._runtime_required()) def add_label(self, issue_number: int, label: str): - override = self._instance_override("add_label") - if override is not None: - return override(issue_number, label) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.add_label(self._runtime_required(), issue_number, label) def remove_label(self, issue_number: int, label: str): - override = self._instance_override("remove_label") - if override is not None: - return override(issue_number, label) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.remove_label(self._runtime_required(), issue_number, label) def ensure_label_exists(self, label: str, *, color=None, description=None): - override = self._instance_override("ensure_label_exists") - if override is not None: - return override(label, color=color, description=description) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.ensure_label_exists(self._runtime_required(), label, color=color, description=description) def get_issue_assignees(self, issue_number: int): - override = self._instance_override("get_issue_assignees") - if override is not None: - return override(issue_number) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.get_issue_assignees(self._runtime_required(), issue_number) def request_pr_reviewer_assignment(self, issue_number: int, username: str): - override = self._instance_override("request_pr_reviewer_assignment") - if override is not None: - return override(issue_number, username) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.request_pr_reviewer_assignment(self._runtime_required(), issue_number, username) def assign_issue_assignee(self, issue_number: int, username: str): - override = self._instance_override("assign_issue_assignee") - if override is not None: - return override(issue_number, username) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.assign_issue_assignee(self._runtime_required(), issue_number, username) def get_assignment_failure_comment(self, reviewer: str, attempt): - override = self._instance_override("get_assignment_failure_comment") - if override is not None: - return override(reviewer, attempt) return None def add_reaction(self, comment_id: int, reaction: str): - override = self._instance_override("add_reaction") - if override is not None: - return override(comment_id, reaction) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.add_reaction(self._runtime_required(), comment_id, reaction) def remove_issue_assignee(self, issue_number: int, username: str): - override = self._instance_override("remove_issue_assignee") - if override is None: - override = self._instance_override("unassign_reviewer") - if override is not None: - return override(issue_number, username) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.remove_issue_assignee(self._runtime_required(), issue_number, username) def remove_pr_reviewer(self, issue_number: int, username: str): - override = self._instance_override("remove_pr_reviewer") - if override is None: - override = self._instance_override("unassign_reviewer") - if override is not None: - return override(issue_number, username) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.remove_pr_reviewer(self._runtime_required(), issue_number, username) def get_user_permission_status(self, username: str, required_permission="triage"): - override = self._instance_override("get_user_permission_status") - if override is not None: - return override(username, required_permission) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.get_user_permission_status(self._runtime_required(), username, required_permission) def check_user_permission(self, username: str, required_permission="triage"): - override = self._instance_override("check_user_permission") - if override is not None: - return override(username, required_permission) from scripts.reviewer_bot_lib import github_api as github_api_module return github_api_module.check_user_permission(self._runtime_required(), username, required_permission) def get_issue_or_pr_snapshot(self, issue_number: int): - override = self._instance_override("get_issue_or_pr_snapshot") - if override is not None: - return override(issue_number) return self.github_api("GET", f"issues/{issue_number}") def get_pull_request_reviews(self, issue_number: int): - override = self._instance_override("get_pull_request_reviews") - if override is not None: - return override(issue_number) from scripts.reviewer_bot_lib import reviews as reviews_module return reviews_module.get_pull_request_reviews(self._runtime_required(), issue_number) diff --git a/tests/fixtures/reconcile_harness.py b/tests/fixtures/reconcile_harness.py index c1fc7de94..2fba714b3 100644 --- a/tests/fixtures/reconcile_harness.py +++ b/tests/fixtures/reconcile_harness.py @@ -282,7 +282,7 @@ def add_request_failure( ) def stub_head_repair(self, *, changed: bool = False, outcome: str = "unchanged") -> None: - self.runtime.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult( + self.runtime.adapters.review_state.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult( changed=changed, outcome=outcome, ) diff --git a/tests/fixtures/reviewer_bot_recorders.py b/tests/fixtures/reviewer_bot_recorders.py index 9a4a187cc..fcf513334 100644 --- a/tests/fixtures/reviewer_bot_recorders.py +++ b/tests/fixtures/reviewer_bot_recorders.py @@ -11,19 +11,22 @@ class CommentSideEffects: def record_comments(target): comments: list[tuple[int, str]] = [] - target.post_comment = lambda issue_number, body: comments.append((issue_number, body)) or True + github_target = getattr(target, "github", target) + github_target.post_comment = lambda issue_number, body: comments.append((issue_number, body)) or True return comments def record_comment_dicts(target): comments = [] - target.post_comment = lambda issue_number, body: comments.append({"issue_number": issue_number, "body": body}) or True + github_target = getattr(target, "github", target) + github_target.post_comment = lambda issue_number, body: comments.append({"issue_number": issue_number, "body": body}) or True return comments def record_reactions(target): reactions: list[tuple[int, str]] = [] - target.add_reaction = lambda comment_id, reaction: reactions.append((comment_id, reaction)) or True + github_target = getattr(target, "github", target) + github_target.add_reaction = lambda comment_id, reaction: reactions.append((comment_id, reaction)) or True return reactions diff --git a/tests/integration/reviewer_bot/test_accept_no_fls_changes.py b/tests/integration/reviewer_bot/test_accept_no_fls_changes.py index bab0854ed..2cb9c25a4 100644 --- a/tests/integration/reviewer_bot/test_accept_no_fls_changes.py +++ b/tests/integration/reviewer_bot/test_accept_no_fls_changes.py @@ -49,7 +49,7 @@ def fake_list_changed_files(repo_root): observed["cwd"] = repo_root return ["README.md"] - harness.runtime.list_changed_files = fake_list_changed_files + harness.runtime.adapters.automation.list_changed_files = fake_list_changed_files message, success = harness.handle_accept_no_fls_changes(42, "alice", request=request) @@ -78,7 +78,7 @@ def fake_list_changed_files(repo_root): runner.when(["uv", "run", "--locked", "python", "scripts/fls_audit.py", "--summary-only", "--fail-on-impact"]) runner.when(["uv", "run", "--locked", "python", "./make.py", "--update-spec-lock-file"]) - harness.runtime.list_changed_files = fake_list_changed_files + harness.runtime.adapters.automation.list_changed_files = fake_list_changed_files message, success = harness.handle_accept_no_fls_changes(42, "alice", request=request) @@ -100,7 +100,7 @@ def test_accept_no_fls_changes_surfaces_locked_uv_failure_details(monkeypatch, t issue_labels=(FLS_AUDIT_LABEL,), ) harness.stub_permission("granted") - harness.runtime.list_changed_files = lambda repo_root: [] + harness.runtime.adapters.automation.list_changed_files = lambda repo_root: [] runner = harness.automation_runner() runner.when( ["uv", "run", "--locked", "python", "scripts/fls_audit.py", "--summary-only", "--fail-on-impact"], @@ -188,8 +188,8 @@ def fake_list_changed_files(repo_root): runner.when(["git", "push", "origin", "chore/spec-lock-2026-04-06-issue-42"]) monkeypatch.setattr(automation, "datetime", FrozenDateTime) - harness.runtime.list_changed_files = fake_list_changed_files - harness.runtime.get_default_branch = lambda: "main" + harness.runtime.adapters.automation.list_changed_files = fake_list_changed_files + harness.runtime.adapters.automation.get_default_branch = lambda: "main" monkeypatch.setattr( automation, "create_pull_request", diff --git a/tests/integration/reviewer_bot/test_app_reviewer_board_preview.py b/tests/integration/reviewer_bot/test_app_reviewer_board_preview.py index ab336da76..372265139 100644 --- a/tests/integration/reviewer_bot/test_app_reviewer_board_preview.py +++ b/tests/integration/reviewer_bot/test_app_reviewer_board_preview.py @@ -118,7 +118,7 @@ def test_execute_run_preview_reviewer_board_is_read_only(monkeypatch, capsys): harness.stub_save_state(lambda current: (_ for _ in ()).throw(AssertionError("preview should not save state"))) harness.stub_sync_status_labels(lambda current, issue_numbers: (_ for _ in ()).throw(AssertionError("preview should not sync labels"))) monkeypatch.setattr(harness.runtime, "github_graphql", lambda query, variables=None, *, token=None: valid_reviewer_board_metadata()) - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": None, "labels": []} + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": None, "labels": []} result = harness.run_execute() diff --git a/tests/integration/reviewer_bot/test_app_schedule_repairs.py b/tests/integration/reviewer_bot/test_app_schedule_repairs.py index 977fc8002..771c6e7d5 100644 --- a/tests/integration/reviewer_bot/test_app_schedule_repairs.py +++ b/tests/integration/reviewer_bot/test_app_schedule_repairs.py @@ -107,7 +107,7 @@ def fake_save_state(updated_state): harness.stub_save_state(fake_save_state) harness.stub_pass_until(lambda current_state: (current_state, [])) harness.stub_sync_members(lambda current_state: (current_state, [])) - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "labels": [], "pull_request": None} + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "labels": [], "pull_request": None} harness.stub_sync_status_labels(lambda current_state, issue_numbers: (_ for _ in ()).throw(RuntimeError("projection failed"))) output_path = tmp_path / "github-output.txt" monkeypatch.setenv("GITHUB_OUTPUT", str(output_path)) @@ -153,7 +153,7 @@ def fake_sweep(bot, current): "handle_scheduled_check_result", lambda bot, current: maintenance.ScheduleHandlerResult(False, []), ) - harness.runtime.post_comment = lambda issue_number, body: posted_comments.append((issue_number, body)) or True + harness.runtime.github.post_comment = lambda issue_number, body: posted_comments.append((issue_number, body)) or True harness.stub_save_state( lambda current: saved_warning_values.append(current["active_reviews"]["42"]["transition_warning_sent"]) or True ) diff --git a/tests/integration/reviewer_bot/test_comment_routing_integration.py b/tests/integration/reviewer_bot/test_comment_routing_integration.py index 528b955a7..a6ac5ab9b 100644 --- a/tests/integration/reviewer_bot/test_comment_routing_integration.py +++ b/tests/integration/reviewer_bot/test_comment_routing_integration.py @@ -23,7 +23,7 @@ def test_handle_non_pr_issue_comment_creates_pending_privileged_command(monkeypa comment_body="@guidelines-bot /accept-no-fls-changes", ) effects = harness.capture_comment_side_effects() - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" assert comment_routing.handle_comment_event(harness.runtime, state, request) is True pending = state["active_reviews"]["42"]["sidecars"]["pending_privileged_commands"] @@ -32,7 +32,7 @@ def test_handle_non_pr_issue_comment_creates_pending_privileged_command(monkeypa ( 42, "✅ Recorded pending privileged command `accept-no-fls-changes` from trusted live validation. " - "Use the isolated privileged workflow to execute it from issue `#314` state.", + "Use the isolated privileged workflow to execute it from issue `the configured state issue` state.", ) ] assert effects.reactions == [] @@ -65,7 +65,7 @@ def test_closed_non_pr_command_comment_does_not_create_pending_privileged_comman ) effects = harness.capture_comment_side_effects() harness.runtime.set_config_value("ISSUE_LABELS", f'["{FLS_AUDIT_LABEL}"]') - harness.runtime.check_user_permission = lambda username, required_permission="triage": True + harness.runtime.github.check_user_permission = lambda username, required_permission="triage": True assert comment_routing.handle_comment_event(harness.runtime, state, request) is False assert state["active_reviews"] == {} diff --git a/tests/integration/reviewer_bot/test_reconcile_privileged_command.py b/tests/integration/reviewer_bot/test_reconcile_privileged_command.py index ffe158187..d83cd7409 100644 --- a/tests/integration/reviewer_bot/test_reconcile_privileged_command.py +++ b/tests/integration/reviewer_bot/test_reconcile_privileged_command.py @@ -29,8 +29,8 @@ def test_execute_pending_privileged_command_revalidates_live_state(monkeypatch): "created_at": "2026-03-17T10:00:00Z", } harness.set_manual_dispatch(source_event_key="issue_comment:100") - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" monkeypatch.setattr( automation, "handle_accept_no_fls_changes_command", @@ -64,8 +64,8 @@ def test_execute_pending_privileged_command_passes_revalidated_typed_request(mon } harness.set_manual_dispatch(source_event_key="issue_comment:100") monkeypatch.setenv("ISSUE_LABELS", '["stale-label"]') - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" observed = {} @@ -118,8 +118,8 @@ def test_execute_pending_privileged_command_does_not_leak_issue_labels_env(monke } harness.set_manual_dispatch(source_event_key="issue_comment:100") monkeypatch.delenv("ISSUE_LABELS", raising=False) - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" monkeypatch.setattr( automation, "handle_accept_no_fls_changes_command", @@ -152,8 +152,8 @@ def test_execute_pending_privileged_command_fails_closed_without_live_fls_audit_ "created_at": "2026-03-17T10:00:00Z", } harness.set_manual_dispatch(source_event_key="issue_comment:100") - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": "status: awaiting reviewer response"}]} - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": "status: awaiting reviewer response"}]} + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" called = {"handle": 0} monkeypatch.setattr( automation, diff --git a/tests/integration/reviewer_bot/test_reconcile_workflow_run.py b/tests/integration/reviewer_bot/test_reconcile_workflow_run.py index c4511a98b..527ad58c5 100644 --- a/tests/integration/reviewer_bot/test_reconcile_workflow_run.py +++ b/tests/integration/reviewer_bot/test_reconcile_workflow_run.py @@ -236,7 +236,7 @@ def test_handle_workflow_run_event_rebuilds_completion_from_live_review_commit_i ) ], ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True assert review["current_cycle_completion"]["completed"] is False @@ -302,7 +302,7 @@ def test_handle_workflow_run_event_refreshes_stale_stored_reviewer_review_to_cur ), ], ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True accepted = review["reviewer_review"]["accepted"] @@ -341,7 +341,7 @@ def test_deferred_review_comment_reconcile_records_contributor_freshness(monkeyp author_type="User", author_association="CONTRIBUTOR", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True accepted = state["active_reviews"]["42"]["contributor_comment"]["accepted"] @@ -380,7 +380,7 @@ def test_deferred_review_comment_reconcile_records_reviewer_freshness(monkeypatc author_type="User", author_association="MEMBER", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True assert review["reviewer_comment"]["accepted"]["semantic_key"] == "pull_request_review_comment:302" @@ -415,7 +415,7 @@ def test_deferred_review_comment_missing_live_object_preserves_source_time_fresh payload={"message": "missing"}, failure_kind="not_found", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True assert review["reviewer_comment"]["accepted"]["semantic_key"] == "pull_request_review_comment:303" @@ -500,7 +500,7 @@ def test_deferred_comment_reconcile_hydrates_pr_author_context_for_contributor_f author_type="User", author_association="CONTRIBUTOR", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" assert harness.run(state) is True assert state["active_reviews"]["42"]["contributor_comment"]["accepted"]["semantic_key"] == "issue_comment:199" @@ -540,7 +540,7 @@ def test_deferred_comment_reconcile_uses_pr_assignment_semantics_for_claim(monke author_type="User", author_association="MEMBER", ) - harness.runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" claim_contexts = [] def apply_claim_command(bot, state_obj, request, classified, classify_issue_comment_actor=None): @@ -641,7 +641,7 @@ def test_deferred_comment_reconcile_fails_closed_when_comment_classification_dri } ) harness.runtime.add_reaction = lambda *args, **kwargs: True - harness.runtime.post_comment = lambda *args, **kwargs: True + harness.runtime.github.post_comment = lambda *args, **kwargs: True assert harness.run(state) is True assert state["active_reviews"]["42"]["contributor_comment"]["accepted"]["semantic_key"] == "issue_comment:202" diff --git a/tests/unit/reviewer_bot/test_commands.py b/tests/unit/reviewer_bot/test_commands.py index b9240e988..f715c9d8b 100644 --- a/tests/unit/reviewer_bot/test_commands.py +++ b/tests/unit/reviewer_bot/test_commands.py @@ -33,12 +33,12 @@ def test_label_signoff_create_pr_marks_issue_review_complete_without_inline_stat issue_author="dana", is_pull_request=False, ) - harness.runtime.get_repo_labels = lambda: ["sign-off: create pr"] - harness.runtime.add_label = lambda issue_number, label: True + harness.runtime.github.get_repo_labels = lambda: ["sign-off: create pr"] + harness.runtime.github.add_label = lambda issue_number, label: True harness.runtime.sync_status_labels_for_items = lambda *args, **kwargs: pytest.fail( "status sync should run only from app orchestration after save" ) - harness.runtime.add_reaction = lambda *args, **kwargs: True + harness.runtime.github.add_reaction = lambda *args, **kwargs: True posted = harness.capture_posted_comments() assert harness.handle_comment_event(state, request=request) is True @@ -71,13 +71,13 @@ def test_label_signoff_create_pr_on_pr_does_not_mark_issue_complete(monkeypatch) "user": {"login": "dana"}, "pull_request": {}, } - harness.runtime.get_repo_labels = lambda: ["sign-off: create pr"] - harness.runtime.add_label = lambda issue_number, label: True + harness.runtime.github.get_repo_labels = lambda: ["sign-off: create pr"] + harness.runtime.github.add_label = lambda issue_number, label: True harness.runtime.sync_status_labels_for_items = lambda *args, **kwargs: pytest.fail( "status sync should not run for PR sign-off label command" ) - harness.runtime.add_reaction = lambda *args, **kwargs: True - harness.runtime.post_comment = lambda *args, **kwargs: True + harness.runtime.github.add_reaction = lambda *args, **kwargs: True + harness.runtime.github.post_comment = lambda *args, **kwargs: True assert harness.handle_comment_event(state, request=request, trust_context=trust_context) is False assert review["review_completion_source"] is None @@ -102,7 +102,7 @@ def test_assign_command_fails_closed_when_assignees_unavailable(monkeypatch): harness = CommandHarness(monkeypatch) state = make_state() state["queue"] = [{"github": "felix91gr", "name": "Félix Fischer"}] - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_assign(state, 42, "@felix91gr") @@ -118,7 +118,7 @@ def test_assign_command_posts_pr_guidance_on_success(monkeypatch): harness.stub_assignees([]) harness.stub_assignment() posted = [] - harness.runtime.post_comment = lambda issue_number, body: posted.append(body) or True + harness.runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True response, success = harness.handle_assign(state, 42, "@felix91gr", request=request) @@ -135,7 +135,7 @@ def test_claim_command_posts_pr_guidance_on_success(monkeypatch): harness.stub_assignees([]) harness.stub_assignment() posted = [] - harness.runtime.post_comment = lambda issue_number, body: posted.append(body) or True + harness.runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True response, success = harness.handle_claim(state, 42, "felix91gr", request=request) @@ -157,9 +157,9 @@ def test_pass_command_posts_pr_guidance_for_new_reviewer(monkeypatch): request = harness.typed_assignment_request(issue_number=42, issue_author="PLeVasseur", is_pull_request=True) harness.stub_assignees(["alice"]) harness.stub_assignment() - harness.runtime.unassign_reviewer = lambda issue_number, username: True + harness.runtime.github.remove_pr_reviewer = lambda issue_number, username: True posted = [] - harness.runtime.post_comment = lambda issue_number, body: posted.append(body) or True + harness.runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True response, success = harness.handle_pass(state, 42, "alice", None, request=request) @@ -176,7 +176,7 @@ def test_assign_from_queue_posts_guidance_only_once(monkeypatch): harness.stub_assignees([]) harness.stub_assignment() posted = [] - harness.runtime.post_comment = lambda issue_number, body: posted.append(body) or True + harness.runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True response, success = harness.handle_assign_from_queue(state, 42, request=request) @@ -223,7 +223,7 @@ def test_pass_command_fails_closed_when_assignees_unavailable(monkeypatch): state = make_state() review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_pass(state, 42, "alice", None) @@ -238,7 +238,7 @@ def test_away_command_fails_closed_when_assignees_unavailable(monkeypatch): review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None review["current_reviewer"] = "alice" - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_pass_until(state, 42, "alice", "2099-01-01", None) @@ -250,7 +250,7 @@ def test_claim_command_fails_closed_when_assignees_unavailable(monkeypatch): harness = CommandHarness(monkeypatch) state = make_state() state["queue"] = [{"github": "alice", "name": "Alice"}] - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_claim(state, 42, "alice") @@ -261,7 +261,7 @@ def test_claim_command_fails_closed_when_assignees_unavailable(monkeypatch): def test_release_command_fails_closed_when_permission_unavailable(monkeypatch): harness = CommandHarness(monkeypatch) state = make_state() - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" response, success = harness.handle_release(state, 42, "alice", ["@bob"]) @@ -272,7 +272,7 @@ def test_release_command_fails_closed_when_permission_unavailable(monkeypatch): def test_release_command_fails_closed_when_assignees_unavailable(monkeypatch): harness = CommandHarness(monkeypatch) state = make_state() - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_release(state, 42, "alice") @@ -284,7 +284,7 @@ def test_assign_from_queue_command_fails_closed_when_assignees_unavailable(monke harness = CommandHarness(monkeypatch) state = make_state() state["queue"] = [{"github": "alice", "name": "Alice"}] - harness.runtime.get_issue_assignees = lambda issue_number: None + harness.runtime.github.get_issue_assignees = lambda issue_number: None response, success = harness.handle_assign_from_queue(state, 42) @@ -296,7 +296,7 @@ def test_handle_rectify_command_reports_permission_unavailable(monkeypatch): state = make_state() harness = CommandHarness(monkeypatch) monkeypatch.setattr(reconcile, "ensure_review_entry", lambda current, issue_number, create=False: None) - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" message, success, changed = harness.handle_rectify(state, 42, "alice") @@ -309,7 +309,7 @@ def test_handle_rectify_command_reports_permission_denied(monkeypatch): state = make_state() harness = CommandHarness(monkeypatch) monkeypatch.setattr(reconcile, "ensure_review_entry", lambda current, issue_number, create=False: None) - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "denied" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "denied" message, success, changed = harness.handle_rectify(state, 42, "alice") @@ -321,7 +321,7 @@ def test_handle_rectify_command_reports_permission_denied(monkeypatch): def test_validate_accept_no_fls_changes_handoff_distinguishes_permission_unavailable(monkeypatch): harness = CommentRoutingHarness(monkeypatch) harness.runtime.set_config_value("ISSUE_LABELS", f'["{FLS_AUDIT_LABEL}"]') - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" request = harness.request( issue_number=42, is_pull_request=False, @@ -359,7 +359,7 @@ def test_validate_accept_no_fls_changes_handoff_freezes_fail_closed_reason_matri ): harness = CommentRoutingHarness(monkeypatch) harness.runtime.set_config_value("ISSUE_LABELS", str(list(labels)).replace("'", '"')) - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": permission + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": permission request = harness.request( issue_number=42, is_pull_request=is_pull_request, @@ -380,7 +380,7 @@ def test_validate_accept_no_fls_changes_handoff_freezes_fail_closed_reason_matri def test_validate_accept_no_fls_changes_handoff_freezes_success_metadata_shape(monkeypatch): harness = CommentRoutingHarness(monkeypatch) harness.runtime.set_config_value("ISSUE_LABELS", f'["{FLS_AUDIT_LABEL}"]') - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "granted" + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "granted" request = harness.request( issue_number=42, is_pull_request=False, @@ -438,7 +438,7 @@ def test_apply_comment_command_records_privileged_handoff_side_effects(monkeypat assert side_effects.comments == [ ( 42, - "✅ Recorded pending privileged command `accept-no-fls-changes` from trusted live validation. Use the isolated privileged workflow to execute it from issue `#314` state.", + "✅ Recorded pending privileged command `accept-no-fls-changes` from trusted live validation. Use the isolated privileged workflow to execute it from issue `the configured state issue` state.", ) ] assert side_effects.reactions == [] @@ -491,8 +491,8 @@ def test_manual_dispatch_marks_authorization_unavailable_for_pending_privileged_ } } harness.set_manual_dispatch(source_event_key="issue_comment:100") - harness.runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} - harness.runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + harness.runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "labels": [{"name": FLS_AUDIT_LABEL}]} + harness.runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" assert harness.handle_manual_dispatch(state) is True pending = review["sidecars"]["pending_privileged_commands"]["issue_comment:100"] diff --git a/tests/unit/reviewer_bot/test_lifecycle.py b/tests/unit/reviewer_bot/test_lifecycle.py index e132fc40e..47fae20c5 100644 --- a/tests/unit/reviewer_bot/test_lifecycle.py +++ b/tests/unit/reviewer_bot/test_lifecycle.py @@ -19,6 +19,7 @@ def test_handle_pull_request_target_synchronize_returns_true_for_head_only_mutation(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) + runtime.ACTIVE_LEASE_CONTEXT = object() state = make_state() review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None @@ -72,7 +73,7 @@ def test_check_overdue_reviews_skips_transition_after_transition_notice_sent(mon 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" - runtime.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} runtime.get_pull_request_reviews = lambda issue_number: [] assert maintenance.check_overdue_reviews(runtime, state) == [] @@ -85,7 +86,7 @@ def test_handle_transition_notice_records_transition_notice_sent_at_once(monkeyp assert review is not None review["current_reviewer"] = "alice" posted = [] - runtime.post_comment = lambda issue_number, body: posted.append((issue_number, body)) or True + runtime.github.post_comment = lambda issue_number, body: posted.append((issue_number, body)) or True assert lifecycle.handle_transition_notice(runtime, state, 42, "alice") is True assert review["transition_notice_sent_at"] is not None @@ -98,7 +99,7 @@ def test_handle_transition_notice_message_does_not_claim_reassignment(monkeypatc state = make_state() review_state.ensure_review_entry(state, 42, create=True) posted = [] - runtime.post_comment = lambda issue_number, body: posted.append(body) or True + runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True assert lifecycle.handle_transition_notice(runtime, state, 42, "alice") is True assert "reassigned to the next person in the queue" not in posted[0] @@ -149,6 +150,7 @@ def test_reviewer_comment_clears_warning_and_transition_notice_markers(monkeypat def test_scheduled_check_backfills_transition_notice_without_reposting(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) + runtime.ACTIVE_LEASE_CONTEXT = object() state = make_state() review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None @@ -174,9 +176,9 @@ def test_scheduled_check_backfills_transition_notice_without_reposting(monkeypat ], ) runtime.get_pull_request_reviews = lambda issue_number: [] - runtime.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} posted = [] - runtime.post_comment = lambda issue_number, body: posted.append(body) or True + runtime.github.post_comment = lambda issue_number, body: posted.append(body) or True runtime.github_api = lambda method, endpoint, data=None: [ { "id": 99, @@ -313,10 +315,11 @@ def test_maybe_record_head_observation_repair_records_changed_head_once(monkeypa def test_handle_issue_or_pr_opened_fails_closed_when_assignees_unavailable(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) + runtime.ACTIVE_LEASE_CONTEXT = object() state = make_state() runtime.set_config_value("ISSUE_NUMBER", "42") runtime.set_config_value("ISSUE_LABELS", json.dumps(["coding guideline"])) - runtime.get_issue_assignees = lambda issue_number: None + runtime.github.get_issue_assignees = lambda issue_number: None with pytest.raises(RuntimeError, match="Unable to determine assignees"): lifecycle.handle_issue_or_pr_opened(runtime, state) @@ -324,6 +327,7 @@ def test_handle_issue_or_pr_opened_fails_closed_when_assignees_unavailable(monke def test_issue_edit_by_author_records_contributor_freshness(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) + runtime.ACTIVE_LEASE_CONTEXT = object() state = make_state() review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None diff --git a/tests/unit/reviewer_bot/test_maintenance.py b/tests/unit/reviewer_bot/test_maintenance.py index 91dad2968..f79449fb0 100644 --- a/tests/unit/reviewer_bot/test_maintenance.py +++ b/tests/unit/reviewer_bot/test_maintenance.py @@ -18,7 +18,7 @@ def test_scheduled_check_repairs_missing_reviewer_review_state(monkeypatch): review["active_cycle_started_at"] = "2026-03-17T09:00:00Z" bot = FakeReviewerBotRuntime(monkeypatch) bot.ACTIVE_LEASE_CONTEXT = object() - bot.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} + bot.github.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} bot.github_api_request = lambda method, endpoint, data=None, extra_headers=None, **kwargs: GitHubApiResult( 200, {"state": "open", "head": {"sha": "head-1"}} @@ -62,7 +62,7 @@ def test_scheduled_check_records_live_read_failure_and_continues(monkeypatch): bot = FakeReviewerBotRuntime(monkeypatch) bot.ACTIVE_LEASE_CONTEXT = object() bot.collect_touched_item = lambda issue_number: None - bot.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} + bot.github.get_issue_or_pr_snapshot = lambda issue_number: {"pull_request": {}} monkeypatch.setattr(maintenance_schedule, "sweep_deferred_gaps", lambda bot, state: False) monkeypatch.setattr(maintenance_schedule, "check_overdue_reviews", lambda bot, state: overdue_called.append(True) or []) monkeypatch.setattr(maintenance_schedule, "repair_missing_reviewer_review_state", lambda bot, issue_number, review_data: False) @@ -128,7 +128,7 @@ def test_tracked_pr_repair_pass_collects_touched_items_and_clears_review_repair_ "recorded_at": "2026-03-01T00:00:00Z", }) bot = FakeReviewerBotRuntime(monkeypatch) - bot.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} + bot.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} monkeypatch.setattr(maintenance_schedule, "repair_missing_reviewer_review_state", lambda bot, issue_number, review_data: True) monkeypatch.setattr( maintenance_schedule, @@ -169,7 +169,7 @@ def test_scheduled_check_clears_head_observation_repair_marker_after_success(mon }) bot = FakeReviewerBotRuntime(monkeypatch) bot.ACTIVE_LEASE_CONTEXT = object() - bot.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} + bot.github.get_issue_or_pr_snapshot = lambda issue_number: {"number": issue_number, "state": "open", "pull_request": {}, "labels": []} monkeypatch.setattr(maintenance_schedule, "sweep_deferred_gaps", lambda bot, state: False) monkeypatch.setattr(maintenance_schedule, "repair_missing_reviewer_review_state", lambda bot, issue_number, review_data: False) monkeypatch.setattr(maintenance_schedule, "maybe_record_head_observation_repair", lambda bot, issue_number, review_data: lifecycle.HeadObservationRepairResult(changed=False, outcome="unchanged")) diff --git a/tests/unit/reviewer_bot/test_overdue.py b/tests/unit/reviewer_bot/test_overdue.py index 2258a6b0a..6ee8db69f 100644 --- a/tests/unit/reviewer_bot/test_overdue.py +++ b/tests/unit/reviewer_bot/test_overdue.py @@ -19,8 +19,8 @@ def _runtime(monkeypatch, routes=None): runtime = FakeReviewerBotRuntime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) - runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" if routes is not None: runtime.github.stub(routes) return runtime @@ -52,7 +52,7 @@ def test_check_overdue_reviews_consumes_only_stable_reviewer_response_fields(mon anchor_timestamp = iso_z(now - timedelta(days=runtime.REVIEW_DEADLINE_DAYS + 1)) state = make_state() make_tracked_review_state(state, 42, reviewer="alice", assigned_at=anchor_timestamp, active_cycle_started_at=anchor_timestamp) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) runtime.adapters.review_state.compute_reviewer_response_state = lambda issue_number, review_data, **kwargs: { "state": "awaiting_reviewer_response", "anchor_timestamp": anchor_timestamp, @@ -81,7 +81,7 @@ def test_check_overdue_reviews_skips_item_when_snapshot_unavailable(monkeypatch) review["assigned_at"] = "2026-03-01T00:00:00Z" review["last_reviewer_activity"] = "2026-03-01T00:00:00Z" runtime = FakeReviewerBotRuntime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: None + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: None assert maintenance.check_overdue_reviews(runtime, state) == [] @@ -91,7 +91,7 @@ def test_handle_overdue_review_warning_only_records_successful_comment(monkeypat review = review_state.ensure_review_entry(state, 42, create=True) assert review is not None runtime = FakeReviewerBotRuntime(monkeypatch) - runtime.post_comment = lambda issue_number, body: False + runtime.github.post_comment = lambda issue_number, body: False assert maintenance.handle_overdue_review_warning(runtime, state, 42, "alice") is False assert review["transition_warning_sent"] is None diff --git a/tests/unit/reviewer_bot/test_project_board.py b/tests/unit/reviewer_bot/test_project_board.py index d366afeea..c7d9f2e86 100644 --- a/tests/unit/reviewer_bot/test_project_board.py +++ b/tests/unit/reviewer_bot/test_project_board.py @@ -1,6 +1,7 @@ import json from pathlib import Path +from scripts import reviewer_bot from scripts.reviewer_bot_lib import project_board, reviews from scripts.reviewer_bot_lib.config import ( REVIEWER_BOARD_FIELD_NEEDS_ATTENTION, @@ -9,6 +10,8 @@ STATUS_AWAITING_CONTRIBUTOR_RESPONSE_LABEL, ) from tests.fixtures.fake_runtime import FakeReviewerBotRuntime +from tests.fixtures.focused_fake_services import GraphQLTransportStub +from tests.fixtures.http_responses import FakeGitHubResponse from tests.fixtures.reviewer_bot import ( accept_reviewer_comment, accept_reviewer_review, @@ -33,7 +36,7 @@ def test_reviewer_board_preflight_validates_manifest(monkeypatch): runtime = _runtime(monkeypatch) runtime.set_config_value("REVIEWER_BOARD_ENABLED", "true") runtime.set_config_value("REVIEWER_BOARD_TOKEN", "board-token") - runtime.github_graphql = lambda query, variables=None, *, token=None: valid_reviewer_board_metadata() + runtime.graphql_transport.stub_sequence([FakeGitHubResponse(200, valid_reviewer_board_metadata(), "ok")]) preflight = project_board.reviewer_board_preflight(runtime) @@ -42,6 +45,17 @@ def test_reviewer_board_preflight_validates_manifest(monkeypatch): assert preflight.project_id == "PVT_kwDOB" +def test_bootstrapped_runtime_resolves_reviewer_board_metadata(monkeypatch): + runtime = reviewer_bot._runtime_bot() + runtime.set_config_value("REVIEWER_BOARD_TOKEN", "board-token") + runtime.graphql_transport = GraphQLTransportStub() + runtime.graphql_transport.stub_sequence([FakeGitHubResponse(200, valid_reviewer_board_metadata(), "ok")]) + + metadata = project_board.resolve_project_metadata(runtime) + + assert metadata.project_id == "PVT_kwDOB" + + def test_reviewer_board_preflight_is_disabled_without_runtime_flag(monkeypatch): runtime = _runtime(monkeypatch) @@ -56,7 +70,7 @@ def test_preview_board_projection_valid_manifest_yields_preview_output(monkeypat state = make_state() make_tracked_review_state(state, 42, reviewer="alice", assigned_at="2026-03-20T12:34:56Z", active_cycle_started_at="2026-03-20T12:34:56Z") runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") preview = project_board.preview_board_projection_for_item(runtime, state, 42) @@ -71,7 +85,7 @@ def test_preview_board_projection_consumes_only_stable_reviewer_response_fields( state = make_state() make_tracked_review_state(state, 42, reviewer="alice", assigned_at="2026-03-20T12:34:56Z", active_cycle_started_at="2026-03-20T12:34:56Z") runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) runtime.adapters.review_state.compute_reviewer_response_state = lambda issue_number, review_data, **kwargs: { "state": "awaiting_reviewer_response", "anchor_timestamp": "2026-03-21T08:00:00Z", @@ -90,7 +104,7 @@ def test_preview_board_projection_tracked_unassigned_maps_to_unassigned(monkeypa state = make_state() make_tracked_review_state(state, 42) runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") preview = project_board.preview_board_projection_for_item(runtime, state, 42) @@ -104,7 +118,7 @@ def test_preview_board_projection_closed_item_maps_to_archive_intent(monkeypatch state = make_state() make_tracked_review_state(state, 42, reviewer="alice") runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="closed") + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="closed") preview = project_board.preview_board_projection_for_item(runtime, state, 42) @@ -117,7 +131,7 @@ def test_preview_board_projection_closed_item_maps_to_archive_intent(monkeypatch def test_preview_board_projection_open_untracked_maps_to_archive_intent(monkeypatch): state = make_state() runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") preview = project_board.preview_board_projection_for_item(runtime, state, 42) @@ -134,7 +148,7 @@ def test_preview_board_projection_formats_dates_at_day_granularity(monkeypatch): routes = RouteGitHubApi().add_pull_request_snapshot(42, pull_request_payload(42, head_sha="head-1")) routes.add_pull_request_reviews(42, []) runtime = _runtime(monkeypatch, routes) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) runtime.adapters.review_state.compute_reviewer_response_state = lambda issue_number, review_data, **kwargs: { "state": "awaiting_reviewer_response", "anchor_timestamp": "2026-03-21T08:00:00Z", @@ -159,8 +173,8 @@ def test_preview_board_projection_keeps_parity_with_refreshed_live_review_state( ], ) runtime = _runtime(monkeypatch, routes) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) - runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" desired_labels, _ = reviews.project_status_labels_for_item(runtime, 42, state) preview = project_board.preview_board_projection_for_item(runtime, state, 42) @@ -181,7 +195,7 @@ def test_preview_board_projection_marks_projection_repair_as_attention(monkeypat repair_needed={"kind": "projection_failure", "reason": "projection_failed"}, ) runtime = _runtime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open") preview = project_board.preview_board_projection_for_item(runtime, state, 42) diff --git a/tests/unit/reviewer_bot/test_reconcile_replay.py b/tests/unit/reviewer_bot/test_reconcile_replay.py index 3ce43cc1d..58b33acdd 100644 --- a/tests/unit/reviewer_bot/test_reconcile_replay.py +++ b/tests/unit/reviewer_bot/test_reconcile_replay.py @@ -11,11 +11,11 @@ def test_reconcile_active_review_entry_uses_explicit_head_repair_changed_field(m runtime = FakeReviewerBotRuntime(monkeypatch) runtime.set_config_value("IS_PULL_REQUEST", "true") - runtime.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult( + runtime.adapters.review_state.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult( changed=False, outcome="unchanged", ) - runtime.get_pull_request_reviews = lambda issue_number: [] + runtime.github.get_pull_request_reviews = lambda issue_number: [] monkeypatch.setattr(reconcile, "refresh_reviewer_review_from_live_preferred_review", lambda bot, issue_number, review_data, **kwargs: (False, None)) monkeypatch.setattr(reconcile, "_record_review_rebuild", lambda bot, state_obj, issue_number, review_data: False) diff --git a/tests/unit/reviewer_bot/test_reconcile_unit.py b/tests/unit/reviewer_bot/test_reconcile_unit.py index d627347ee..7b99e9dcc 100644 --- a/tests/unit/reviewer_bot/test_reconcile_unit.py +++ b/tests/unit/reviewer_bot/test_reconcile_unit.py @@ -268,8 +268,8 @@ def test_reconcile_active_review_entry_uses_explicit_head_repair_changed_field(m review["current_reviewer"] = "alice" runtime = FakeReviewerBotRuntime(monkeypatch) runtime.set_config_value("IS_PULL_REQUEST", "true") - runtime.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult(changed=False, outcome="unchanged") - runtime.get_pull_request_reviews = lambda issue_number: [] + runtime.adapters.review_state.maybe_record_head_observation_repair = lambda issue_number, review_data: lifecycle.HeadObservationRepairResult(changed=False, outcome="unchanged") + runtime.github.get_pull_request_reviews = lambda issue_number: [] monkeypatch.setattr(reconcile, "refresh_reviewer_review_from_live_preferred_review", lambda bot, issue_number, review_data, **kwargs: (False, None)) monkeypatch.setattr(reconcile, "_record_review_rebuild", lambda bot, state_obj, issue_number, review_data: False) @@ -400,7 +400,7 @@ def test_workflow_run_reconcile_uses_artifact_contract_for_router_no_artifact_su def test_read_reconcile_reviews_rejects_non_list_payload(monkeypatch): runtime = FakeReviewerBotRuntime(monkeypatch) - runtime.get_pull_request_reviews = lambda issue_number: {"unexpected": True} + runtime.github.get_pull_request_reviews = lambda issue_number: {"unexpected": True} with pytest.raises(reconcile.ReconcileReadError, match="payload invalid"): reconcile._read_reconcile_reviews(runtime, 42) diff --git a/tests/unit/reviewer_bot/test_reviewer_response_equivalence.py b/tests/unit/reviewer_bot/test_reviewer_response_equivalence.py index 174b73c38..7828aa688 100644 --- a/tests/unit/reviewer_bot/test_reviewer_response_equivalence.py +++ b/tests/unit/reviewer_bot/test_reviewer_response_equivalence.py @@ -76,7 +76,7 @@ def _build_scenario(monkeypatch, scenario_id: str): [review_payload(10, state="APPROVED", submitted_at="2026-03-17T10:01:00Z", commit_id="head-1", author="bob")], ) runtime = _runtime(monkeypatch, routes) - runtime.get_user_permission_status = lambda username, required_permission="triage": "denied" + runtime.github.get_user_permission_status = lambda username, required_permission="triage": "denied" return runtime, review if scenario_id == "projection_failed_pull_request_unavailable": @@ -96,7 +96,7 @@ def _build_scenario(monkeypatch, scenario_id: str): [review_payload(10, state="APPROVED", submitted_at="2026-03-17T10:01:00Z", commit_id="head-1", author="alice")], ) runtime = _runtime(monkeypatch, routes) - runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" return runtime, review raise AssertionError(f"Unhandled scenario: {scenario_id}") diff --git a/tests/unit/reviewer_bot/test_reviews_live_fetch.py b/tests/unit/reviewer_bot/test_reviews_live_fetch.py index 1dfee945e..9203623a7 100644 --- a/tests/unit/reviewer_bot/test_reviews_live_fetch.py +++ b/tests/unit/reviewer_bot/test_reviews_live_fetch.py @@ -24,8 +24,8 @@ def _runtime(monkeypatch, routes=None): runtime = FakeReviewerBotRuntime(monkeypatch) - runtime.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) - runtime.get_user_permission_status = lambda username, required_permission="push": "granted" + runtime.github.get_issue_or_pr_snapshot = lambda issue_number: issue_snapshot(issue_number, state="open", is_pull_request=True) + runtime.github.get_user_permission_status = lambda username, required_permission="push": "granted" if routes is not None: runtime.github.stub(routes) return runtime @@ -235,7 +235,7 @@ def test_compute_reviewer_response_state_reports_awaiting_write_approval_after_c [review_payload(10, state="APPROVED", submitted_at="2026-03-17T10:01:00Z", commit_id="head-1", author="bob")], ) runtime = _runtime(monkeypatch, routes) - runtime.get_user_permission_status = lambda username, required_permission="triage": "denied" + runtime.github.get_user_permission_status = lambda username, required_permission="triage": "denied" response_state = reviews.compute_reviewer_response_state(runtime, 42, review) @@ -270,7 +270,7 @@ def test_project_status_labels_emits_awaiting_write_approval_only_after_completi [review_payload(10, state="APPROVED", submitted_at="2026-03-17T10:01:00Z", commit_id="head-1", author="bob")], ) runtime = _runtime(monkeypatch, routes) - runtime.get_user_permission_status = lambda username, required_permission="triage": "denied" + runtime.github.get_user_permission_status = lambda username, required_permission="triage": "denied" desired_labels, metadata = reviews.project_status_labels_for_item(runtime, 42, state) @@ -314,7 +314,7 @@ def test_compute_reviewer_response_state_reports_permission_unavailable(monkeypa [review_payload(10, state="APPROVED", submitted_at="2026-03-17T10:01:00Z", commit_id="head-1", author="alice")], ) runtime = _runtime(monkeypatch, routes) - runtime.get_user_permission_status = lambda username, required_permission="triage": "unavailable" + runtime.github.get_user_permission_status = lambda username, required_permission="triage": "unavailable" response_state = reviews.compute_reviewer_response_state(runtime, 42, review) diff --git a/tests/unit/reviewer_bot/test_sweeper_logic.py b/tests/unit/reviewer_bot/test_sweeper_logic.py index f7574aa56..59846001c 100644 --- a/tests/unit/reviewer_bot/test_sweeper_logic.py +++ b/tests/unit/reviewer_bot/test_sweeper_logic.py @@ -67,7 +67,7 @@ def test_sweeper_creates_keyed_deferred_gaps_for_visible_comments_reviews_and_di .add_request("GET", "pulls/42/comments?per_page=100&page=1", status_code=200, payload=[]) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [ + runtime.github.get_pull_request_reviews = lambda issue_number: [ pull_request_review_event(202, submitted_at="2026-03-25T11:00:00Z", state="APPROVED"), pull_request_review_event(303, submitted_at="2026-03-25T09:00:00Z", updated_at="2026-03-25T12:00:00Z", state="DISMISSED"), ] @@ -112,7 +112,7 @@ def test_sweeper_creates_keyed_deferred_gap_for_visible_review_comments(monkeypa ) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [] + runtime.github.get_pull_request_reviews = lambda issue_number: [] assert sweeper.sweep_deferred_gaps(runtime, state) is True gaps = state["active_reviews"]["42"]["sidecars"]["deferred_gaps"] @@ -202,7 +202,7 @@ def test_sweeper_skips_dismissed_reviews_already_reconciled_by_source_event_key( .add_request("GET", "pulls/42/comments?per_page=100&page=1", status_code=200, payload=[]) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(303, submitted_at="2026-03-17T09:00:00Z", updated_at="2026-03-17T12:00:00Z", state="DISMISSED")] + runtime.github.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(303, submitted_at="2026-03-17T09:00:00Z", updated_at="2026-03-17T12:00:00Z", state="DISMISSED")] assert sweeper.sweep_deferred_gaps(runtime, state) is False assert state["active_reviews"]["42"]["sidecars"]["deferred_gaps"] == {} @@ -239,7 +239,7 @@ def test_sweeper_skips_events_already_reconciled_by_source_event_key(monkeypatch .add_request("GET", "pulls/42/comments?per_page=100&page=1", status_code=200, payload=[]) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-17T11:00:00Z", state="APPROVED")] + runtime.github.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-17T11:00:00Z", state="APPROVED")] assert sweeper.sweep_deferred_gaps(runtime, state) is False assert state["active_reviews"]["42"]["sidecars"]["deferred_gaps"] == {} @@ -295,7 +295,7 @@ def test_sweeper_visible_review_repair_refreshes_current_reviewer_activity_witho .add_request("GET", "pulls/42/comments?per_page=100&page=1", status_code=200, payload=[]) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-25T11:00:00Z", state="COMMENTED", commit_id="head-1")] + runtime.github.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-25T11:00:00Z", state="COMMENTED", commit_id="head-1")] assert sweeper.sweep_deferred_gaps(runtime, state) is True assert review["last_reviewer_activity"] == "2026-03-25T11:00:00Z" @@ -332,7 +332,7 @@ def test_visible_review_repair_does_not_clear_transition_warning_for_stale_repla .add_request("GET", "pulls/42/comments?per_page=100&page=1", status_code=200, payload=[]) ) runtime.github.stub(routes) - runtime.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-25T11:00:00Z", state="COMMENTED", commit_id="head-1")] + runtime.github.get_pull_request_reviews = lambda issue_number: [pull_request_review_event(202, submitted_at="2026-03-25T11:00:00Z", state="COMMENTED", commit_id="head-1")] assert sweeper.sweep_deferred_gaps(runtime, state) is True assert review["last_reviewer_activity"] == "2026-03-25T11:00:00Z"