From 08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:16:59 +0900 Subject: [PATCH 1/6] test(scheduler): reproduce central target-inventory waste --- tests/test_pr_review_merge_scheduler.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 0e2aba2330..7e10cf555c 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -10866,3 +10866,24 @@ def test_withheld_mutation_guidance_uses_recorded_reason_after_environment_chang assert "workflow GITHUB_TOKEN" in "\n".join( sched.head_mutation_credential_upgrade_summary([decision]) ) + + +def test_central_dispatch_skips_non_authoritative_target_actions_inventory( + monkeypatch, +): + """Central review dispatch must not spend App quota on target old-head runs.""" + monkeypatch.setenv( + "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", + "ContextualWisdomLab/.github", + ) + monkeypatch.setattr( + sched, + "cancel_stale_pr_runs", + lambda *args, **kwargs: pytest.fail( + "central dispatch must not enumerate target Actions runs" + ), + ) + + decision = inspect(make_pr(baseRefName="feature-base"), trigger_reviews=False) + + assert decision.action == "skip" From a42ab0035b953241ad81f01414efad555287540c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:17:30 +0900 Subject: [PATCH 2/6] fix(scheduler): skip central target Actions inventory --- CHANGELOG.md | 5 +++ ...ral-review-target-inventory-suppression.md | 35 +++++++++++++++++++ docs/product-technical-gap-baseline.md | 15 ++++++++ scripts/ci/pr_review_merge_scheduler_core.py | 6 +++- 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 docs/doctoring/central-review-target-inventory-suppression.md diff --git a/CHANGELOG.md b/CHANGELOG.md index df6ee0c9b9..c47c4bda6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,11 @@ ## Proposed +- Skip target-repository old-head Actions inventory when review execution is + centralized. Same-repository stale-run cleanup remains enabled; central + review lifecycle is handled in the configured dispatch repository, avoiding + an unauthoritative API read that can exhaust the cross-repository App quota. + - Run Python Security and Agent Review Runtime Quality CI for stacked pull requests by removing their pull-request base-branch filters. Extend the permanent stacked-workflow contract so all four owner review workflows diff --git a/docs/doctoring/central-review-target-inventory-suppression.md b/docs/doctoring/central-review-target-inventory-suppression.md new file mode 100644 index 0000000000..ecee9edc1a --- /dev/null +++ b/docs/doctoring/central-review-target-inventory-suppression.md @@ -0,0 +1,35 @@ +# Central review target-inventory suppression + +Decision date: **2026-09-07** + +## Problem + +When the trusted reviewer is hosted centrally, target-repository old-head +workflow runs are not the authority for the central current-head verdict. +Enumerating those target runs before dispatch consumes the cross-repository +Actions credential and can exhaust its App quota before useful review work +starts. + +## Decision + +Compare the configured review dispatch repository with the target repository. +If they differ, do not enumerate or cancel target old-head runs from this +decision path. The central reviewer owns its own run lifecycle in the dispatch +repository. If they are the same repository, retain existing stale-run cleanup. + +## Failure scenes + +- Central review of a target repository: no target Actions inventory read occurs. +- Same-repository review: stale old-head runs are still cancelled. +- Repository name casing differs: case-insensitive identity prevents accidental + cross-repository classification. + +## Evidence and follow-up + +RED commit: `08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b`. +Fresh exact-head hosted checks and independent review remain required. + +## Reference + +GitHub. (2026). *REST API endpoints for workflow runs*. +https://docs.github.com/en/rest/actions/workflow-runs diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 1cae019f9a..ded5f53046 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3401,3 +3401,18 @@ same name in another file can carry the opposite safety property.** workflows at exact head `e2204eeb1ec2789ff791036140ba1672995d25f5`; RED commit `890bac2f69ff1a51f774ddf5d6c5d819afed4ac9`; fresh exact-head hosted checks remain required. + + +### Central review target-inventory suppression + +- **Status:** Proposed +- **Owner:** `ContextualWisdomLab/.github` +- **Problem:** Before dispatching a central current-head review, the scheduler + enumerated target-repository old-head Actions runs that are not central + admission authority, spending the cross-repository App quota. +- **Action:** Skip only that target enumeration when the configured review + dispatch repository differs from the target; preserve same-repository + stale-run cleanup. +- **Evidence:** RED commit + `08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b`; fresh exact-head hosted checks + remain required before integration. diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 9adcac3e37..5034ebd29e 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -4270,7 +4270,11 @@ def inspect_pr( pass run(["gh", "pr", "close", str(number), "--repo", repo]) return Decision(number, "close_empty", "base 대비 실제 변경 0건") - cancel_stale_pr_runs(repo, pr, dry_run=dry_run) + # A central reviewer owns run lifecycle in its dispatch repository. + # Target old-head runs are not admission authority, and enumerating them + # spends the cross-repository installation quota before current-head review. + if repository_dispatch_target(repo).casefold() == repo.casefold(): + cancel_stale_pr_runs(repo, pr, dry_run=dry_run) if base_ref != base_branch: # Stacked/cascade PR (base is another feature branch). Org required # workflows are only injected for default-branch-target PRs, so these From bf56886bee5d4e9278eadf90f580a9b3ca920ade Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:09:02 +0900 Subject: [PATCH 3/6] test(scheduler): inherit proven credential fixtures --- tests/test_pr_review_merge_scheduler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 7e10cf555c..5f18a76eb9 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -34,6 +34,8 @@ def workflow_starting_mutation_credential(monkeypatch): workflow-starting credential exactly like the scheduler workflow does. """ monkeypatch.setenv("SCHEDULER_MUTATION_TOKEN_SOURCE", "PR_REVIEW_MERGE_TOKEN") + monkeypatch.setenv("GH_TOKEN", "selected-mutation-token") + monkeypatch.setenv("SCHEDULER_WORKFLOW_TOKEN", "workflow-runner-token") @pytest.fixture(autouse=True) @@ -1785,7 +1787,11 @@ def map(self, func, items): ), ) cancelled = [] - monkeypatch.setattr(sched, "run_github_actions", cancelled.append) + monkeypatch.setattr( + sched, + "run_github_actions", + lambda args, stdin=None: cancelled.append(args), + ) monkeypatch.setattr(sched, "require_github_actions_control_actor", lambda x: None) run_ids = sched.cancel_stale_opencode_runs("owner/repo", "workflow", make_pr(), dry_run=False) @@ -1796,7 +1802,7 @@ def map(self, func, items): def test_force_cancel_failure_logs_reason_and_does_not_raise(monkeypatch, capsys): - def fail_cancel(args): + def fail_cancel(args, stdin=None): raise RuntimeError( "Command failed (1): gh api -X POST " "repos/owner/repo/actions/runs/29263154177/force-cancel; " @@ -1821,7 +1827,7 @@ def fail_cancel(args): def test_force_cancel_multiple_runs_reports_only_failures(monkeypatch): - def maybe_fail(args): + def maybe_fail(args, stdin=None): if "runs/2/force-cancel" in " ".join(args): raise RuntimeError("GitHub returned HTTP 500") return "" From 234d98dec14ae7a91819857f561b78d0d424ec98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 14:16:04 +0900 Subject: [PATCH 4/6] test(scheduler): prove central inventory authority boundary --- .../test_pr2005_central_inventory_boundary.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_pr2005_central_inventory_boundary.py diff --git a/tests/test_pr2005_central_inventory_boundary.py b/tests/test_pr2005_central_inventory_boundary.py new file mode 100644 index 0000000000..29460fb730 --- /dev/null +++ b/tests/test_pr2005_central_inventory_boundary.py @@ -0,0 +1,71 @@ +"""Regression contracts for PR #2005's central review inventory boundary.""" + +from scripts.ci import pr_review_merge_scheduler_core as scheduler_core + + +def test_central_review_filter_preserves_target_security_runs(monkeypatch): + """Central review ownership must not preserve unrelated stale target runs.""" + current_head = "a" * 40 + stale_head = "b" * 40 + workflow_runs = [ + { + "id": 101, + "name": "Required OpenCode Review owner/repo#1@" + stale_head, + "head_sha": stale_head, + "pull_requests": [{"number": 1}], + }, + { + "id": 102, + "name": "Python Security", + "head_sha": stale_head, + "pull_requests": [{"number": 1}], + }, + { + "id": 103, + "name": "CodeQL", + "head_sha": stale_head, + "pull_requests": [{"number": 1}], + }, + ] + monkeypatch.setattr( + scheduler_core, + "active_workflow_runs", + lambda _repo, _statuses: workflow_runs, + ) + + assert scheduler_core.stale_pr_run_ids( + "owner/repo", + {"number": 1, "headRefOid": current_head}, + excluded_workflows=frozenset(scheduler_core.OPENCODE_WORKFLOW_NAMES), + ) == ["102", "103"] + + +def test_central_review_filter_matches_bare_and_rendered_names(monkeypatch): + """Both GitHub workflow names and rendered run names use central authority.""" + current_head = "a" * 40 + stale_head = "b" * 40 + workflow_runs = [ + { + "id": 201, + "name": "OpenCode Review", + "head_sha": stale_head, + "pull_requests": [{"number": 1}], + }, + { + "id": 202, + "name": "OpenCode Review Dispatch owner/repo#1@" + stale_head, + "head_sha": stale_head, + "pull_requests": [{"number": 1}], + }, + ] + monkeypatch.setattr( + scheduler_core, + "active_workflow_runs", + lambda _repo, _statuses: workflow_runs, + ) + + assert scheduler_core.stale_pr_run_ids( + "owner/repo", + {"number": 1, "headRefOid": current_head}, + excluded_workflows=frozenset(scheduler_core.OPENCODE_WORKFLOW_NAMES), + ) == [] From 32a0d66cd1210f6fae1cb675265ce4ce49f63167 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 14:17:53 +0900 Subject: [PATCH 5/6] test(scheduler): prove scoped central cleanup wiring --- .../test_pr2005_central_inventory_boundary.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_pr2005_central_inventory_boundary.py b/tests/test_pr2005_central_inventory_boundary.py index 29460fb730..7056d1d681 100644 --- a/tests/test_pr2005_central_inventory_boundary.py +++ b/tests/test_pr2005_central_inventory_boundary.py @@ -3,6 +3,76 @@ from scripts.ci import pr_review_merge_scheduler_core as scheduler_core +def inspect_stacked_pull_request(repository, dispatch_repository, monkeypatch): + """Return the stale-run cleanup calls for one read-only stacked PR inspection.""" + cleanup_calls = [] + monkeypatch.setenv( + "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", + dispatch_repository, + ) + monkeypatch.setattr( + scheduler_core, + "cancel_stale_pr_runs", + lambda target_repository, + pull_request, + *, + dry_run, + excluded_workflows=frozenset(): cleanup_calls.append( + (target_repository, dry_run, excluded_workflows) + ), + ) + pull_request = { + "number": 1, + "isDraft": False, + "baseRefName": "feature-base", + "headRefOid": "a" * 40, + "files": {"totalCount": 1, "nodes": [{"path": "README.md"}]}, + "reviews": {"nodes": []}, + "reviewThreads": {"nodes": []}, + "statusCheckRollup": {"contexts": {"nodes": []}}, + "autoMergeRequest": None, + } + + scheduler_core.inspect_pr( + repository, + pull_request, + dry_run=True, + trigger_reviews=False, + enable_auto_merge_flag=False, + update_branches=False, + workflow="OpenCode Review", + security_workflow="Strix Security Scan", + base_branch="main", + ) + return cleanup_calls + + +def test_central_dispatch_filters_only_review_workflows(monkeypatch): + """Cross-repository dispatch must retain target cleanup with a narrow filter.""" + cleanup_calls = inspect_stacked_pull_request( + "owner/repo", + "ContextualWisdomLab/.github", + monkeypatch, + ) + + assert cleanup_calls == [ + ("owner/repo", True, frozenset(scheduler_core.OPENCODE_WORKFLOW_NAMES)) + ] + + +def test_same_repository_dispatch_keeps_unfiltered_cleanup_case_insensitively( + monkeypatch, +): + """Repository identity casing must not narrow same-repository cleanup.""" + cleanup_calls = inspect_stacked_pull_request( + "owner/repo", + "OWNER/REPO", + monkeypatch, + ) + + assert cleanup_calls == [("owner/repo", True, frozenset())] + + def test_central_review_filter_preserves_target_security_runs(monkeypatch): """Central review ownership must not preserve unrelated stale target runs.""" current_head = "a" * 40 From 9293c779b9634859acf7a311be4dba1be8b83008 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 14:26:32 +0900 Subject: [PATCH 6/6] fix(scheduler): preserve target-owned stale-run cleanup --- CHANGELOG.md | 10 ++-- ...ral-review-target-inventory-suppression.md | 39 ++++++++++----- docs/product-technical-gap-baseline.md | 22 +++++---- scripts/ci/pr_review_merge_scheduler_core.py | 40 ++++++++++++---- .../test_pr2005_central_inventory_boundary.py | 47 ++++++++++++++----- tests/test_pr_review_merge_scheduler.py | 21 --------- 6 files changed, 111 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47c4bda6b..e09f3dae0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,10 +162,12 @@ ## Proposed -- Skip target-repository old-head Actions inventory when review execution is - centralized. Same-repository stale-run cleanup remains enabled; central - review lifecycle is handled in the configured dispatch repository, avoiding - an unauthoritative API read that can exhaust the cross-repository App quota. +- Keep target-repository old-head Actions inventory and destructive-boundary + revalidation when review execution is centralized, while excluding only + bare or rendered OpenCode workflow names whose lifecycle belongs to the + dispatch repository. Target-owned CodeQL, security, and other direct + pull-request runs remain eligible for proven-old-head cancellation; + same-repository cleanup remains unfiltered. - Run Python Security and Agent Review Runtime Quality CI for stacked pull requests by removing their pull-request base-branch filters. Extend the diff --git a/docs/doctoring/central-review-target-inventory-suppression.md b/docs/doctoring/central-review-target-inventory-suppression.md index ecee9edc1a..c16a38424e 100644 --- a/docs/doctoring/central-review-target-inventory-suppression.md +++ b/docs/doctoring/central-review-target-inventory-suppression.md @@ -1,32 +1,47 @@ -# Central review target-inventory suppression +# Central review workflow-authority filtering Decision date: **2026-09-07** ## Problem -When the trusted reviewer is hosted centrally, target-repository old-head +When the trusted reviewer is hosted centrally, target-repository OpenCode workflow runs are not the authority for the central current-head verdict. -Enumerating those target runs before dispatch consumes the cross-repository -Actions credential and can exhaust its App quota before useful review work -starts. +The first implementation therefore skipped the target repository's entire +old-head Actions inventory. That also preserved stale target-owned CodeQL, +Python Security, and other direct pull-request runs, whose lifecycle remains +the target repository's responsibility. ## Decision -Compare the configured review dispatch repository with the target repository. -If they differ, do not enumerate or cancel target old-head runs from this -decision path. The central reviewer owns its own run lifecycle in the dispatch -repository. If they are the same repository, retain existing stale-run cleanup. +Always retain target-repository stale-run inventory and the existing +destructive-boundary live PR/head revalidation. When the configured review +dispatch repository differs from the target, exclude only bare or rendered +OpenCode workflow names from the target cancellation candidates; the central +reviewer owns those runs in the dispatch repository. Target-owned CodeQL, +security, and other direct workflows remain eligible for proven-old-head +cancellation. When repository identities match case-insensitively, retain +unfiltered cleanup. ## Failure scenes -- Central review of a target repository: no target Actions inventory read occurs. -- Same-repository review: stale old-head runs are still cancelled. +- Central review of a target repository: stale OpenCode names are excluded, but + stale target-owned CodeQL and security runs remain cancellable. +- Rendered GitHub run names such as + `OpenCode Review Dispatch owner/repo#1@` receive the same boundary as + their bare workflow names. +- Same-repository review: stale old-head cleanup remains unfiltered. - Repository name casing differs: case-insensitive identity prevents accidental cross-repository classification. ## Evidence and follow-up -RED commit: `08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b`. +The original RED commit +`08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b` covered only the unsafe broad +suppression. Corrective RED commits +`234d98dec14ae7a91819857f561b78d0d424ec98` and +`32a0d66cd1210f6fae1cb675265ce4ce49f63167` prove the workflow-authority +filter, unrelated target-workflow preservation, central invocation, and +case-insensitive same-repository boundary. Fresh exact-head hosted checks and independent review remain required. ## Reference diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ded5f53046..1a146dd0e5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3403,16 +3403,18 @@ same name in another file can carry the opposite safety property.** hosted checks remain required. -### Central review target-inventory suppression +### Central review workflow-authority filtering - **Status:** Proposed - **Owner:** `ContextualWisdomLab/.github` -- **Problem:** Before dispatching a central current-head review, the scheduler - enumerated target-repository old-head Actions runs that are not central - admission authority, spending the cross-repository App quota. -- **Action:** Skip only that target enumeration when the configured review - dispatch repository differs from the target; preserve same-repository - stale-run cleanup. -- **Evidence:** RED commit - `08a16caa4fdb0d0d86c44bb8cd7aed611beaab7b`; fresh exact-head hosted checks - remain required before integration. +- **Problem:** Broadly skipping target-repository Actions inventory for a + central reviewer also preserved stale target-owned CodeQL, security, and + other direct pull-request runs. +- **Action:** Retain target inventory and live PR/head revalidation. When the + dispatch repository differs, exclude only bare or rendered OpenCode workflow + names from target cancellation; keep same-repository cleanup unfiltered by + case-insensitive repository identity. +- **Evidence:** Corrective RED commits + `234d98dec14ae7a91819857f561b78d0d424ec98` and + `32a0d66cd1210f6fae1cb675265ce4ce49f63167`; focused local contract evidence + is not hosted authority, and fresh exact-head hosted checks remain required. diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 5034ebd29e..08da1eecd9 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -3204,9 +3204,10 @@ def stale_pr_run_ids( pr: dict[str, Any], *, workflow: str | None = None, + excluded_workflows: frozenset[str] = frozenset(), statuses: Sequence[str] = ("queued", "in_progress"), ) -> list[str]: - """Return active run ids for older heads of the same pull request.""" + """Return older-head run ids except workflows owned by another repository.""" raw_head = pr.get("headRefOid") try: head = validate_git_sha(str(raw_head or "")).lower() @@ -3219,7 +3220,13 @@ def stale_pr_run_ids( number = int(pr["number"]) stale: list[str] = [] for run_data in active_workflow_runs(repo, statuses): - if workflow is not None and run_data.get("name") != workflow: + run_name = str(run_data.get("name") or "") + if workflow is not None and run_name != workflow: + continue + if any( + run_name == candidate or run_name.startswith(f"{candidate} ") + for candidate in excluded_workflows + ): continue if str(run_data.get("head_sha") or "").lower() == head: continue @@ -3560,12 +3567,29 @@ def _review_run_still_superseded( def cancel_stale_pr_runs(repo: str, pr: dict[str, Any], *, dry_run: bool) -> list[str]: - """Force-cancel only direct-run candidates still proven stale at the destructive boundary.""" + """Cancel proven-stale direct runs except workflows owned by another repository.""" if dry_run: return [] require_github_actions_control_actor("force-cancel-stale-pr-runs") number = int(pr["number"]) - candidates = [str(run_id) for run_id in stale_pr_run_ids(repo, pr)] + dispatch_repo = repository_dispatch_target(repo) + excluded_workflows = ( + frozenset(OPENCODE_WORKFLOW_NAMES) + if dispatch_repo.casefold() != repo.casefold() + else frozenset() + ) + candidates = [ + str(run_id) + for run_id in ( + stale_pr_run_ids( + repo, + pr, + excluded_workflows=excluded_workflows, + ) + if excluded_workflows + else stale_pr_run_ids(repo, pr) + ) + ] def cancel_one(run_id: str) -> str | None: """Revalidate and cancel one direct workflow-run candidate when still stale.""" @@ -4270,11 +4294,9 @@ def inspect_pr( pass run(["gh", "pr", "close", str(number), "--repo", repo]) return Decision(number, "close_empty", "base 대비 실제 변경 0건") - # A central reviewer owns run lifecycle in its dispatch repository. - # Target old-head runs are not admission authority, and enumerating them - # spends the cross-repository installation quota before current-head review. - if repository_dispatch_target(repo).casefold() == repo.casefold(): - cancel_stale_pr_runs(repo, pr, dry_run=dry_run) + # The target repository still owns CodeQL, security, and other direct PR + # runs. Only central-review names move to the dispatch repository. + cancel_stale_pr_runs(repo, pr, dry_run=dry_run) if base_ref != base_branch: # Stacked/cascade PR (base is another feature branch). Org required # workflows are only injected for default-branch-target PRs, so these diff --git a/tests/test_pr2005_central_inventory_boundary.py b/tests/test_pr2005_central_inventory_boundary.py index 7056d1d681..8d0340a326 100644 --- a/tests/test_pr2005_central_inventory_boundary.py +++ b/tests/test_pr2005_central_inventory_boundary.py @@ -13,12 +13,8 @@ def inspect_stacked_pull_request(repository, dispatch_repository, monkeypatch): monkeypatch.setattr( scheduler_core, "cancel_stale_pr_runs", - lambda target_repository, - pull_request, - *, - dry_run, - excluded_workflows=frozenset(): cleanup_calls.append( - (target_repository, dry_run, excluded_workflows) + lambda target_repository, pull_request, *, dry_run: cleanup_calls.append( + (target_repository, dry_run) ), ) pull_request = { @@ -47,17 +43,15 @@ def inspect_stacked_pull_request(repository, dispatch_repository, monkeypatch): return cleanup_calls -def test_central_dispatch_filters_only_review_workflows(monkeypatch): - """Cross-repository dispatch must retain target cleanup with a narrow filter.""" +def test_central_dispatch_retains_target_cleanup(monkeypatch): + """Cross-repository dispatch must still invoke target-owned stale cleanup.""" cleanup_calls = inspect_stacked_pull_request( "owner/repo", "ContextualWisdomLab/.github", monkeypatch, ) - assert cleanup_calls == [ - ("owner/repo", True, frozenset(scheduler_core.OPENCODE_WORKFLOW_NAMES)) - ] + assert cleanup_calls == [("owner/repo", True)] def test_same_repository_dispatch_keeps_unfiltered_cleanup_case_insensitively( @@ -70,7 +64,36 @@ def test_same_repository_dispatch_keeps_unfiltered_cleanup_case_insensitively( monkeypatch, ) - assert cleanup_calls == [("owner/repo", True, frozenset())] + assert cleanup_calls == [("owner/repo", True)] + + +def test_cancel_stale_pr_runs_applies_central_review_filter_internally(monkeypatch): + """Existing callers keep their signature while cancellation scopes authority.""" + captured_exclusions = [] + monkeypatch.setenv( + "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", + "ContextualWisdomLab/.github", + ) + monkeypatch.setattr( + scheduler_core, + "require_github_actions_control_actor", + lambda _action: None, + ) + + def stale_run_ids(_repo, _pull_request, *, excluded_workflows=frozenset()): + captured_exclusions.append(excluded_workflows) + return [] + + monkeypatch.setattr(scheduler_core, "stale_pr_run_ids", stale_run_ids) + + assert scheduler_core.cancel_stale_pr_runs( + "owner/repo", + {"number": 1, "headRefOid": "a" * 40}, + dry_run=False, + ) == [] + assert captured_exclusions == [ + frozenset(scheduler_core.OPENCODE_WORKFLOW_NAMES) + ] def test_central_review_filter_preserves_target_security_runs(monkeypatch): diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 5f18a76eb9..b79cf24c55 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -10872,24 +10872,3 @@ def test_withheld_mutation_guidance_uses_recorded_reason_after_environment_chang assert "workflow GITHUB_TOKEN" in "\n".join( sched.head_mutation_credential_upgrade_summary([decision]) ) - - -def test_central_dispatch_skips_non_authoritative_target_actions_inventory( - monkeypatch, -): - """Central review dispatch must not spend App quota on target old-head runs.""" - monkeypatch.setenv( - "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", - "ContextualWisdomLab/.github", - ) - monkeypatch.setattr( - sched, - "cancel_stale_pr_runs", - lambda *args, **kwargs: pytest.fail( - "central dispatch must not enumerate target Actions runs" - ), - ) - - decision = inspect(make_pr(baseRefName="feature-base"), trigger_reviews=False) - - assert decision.action == "skip"