diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml
index 4ab0c1b8..9ddcf198 100644
--- a/.github/workflows/pr-review-merge-scheduler.yml
+++ b/.github/workflows/pr-review-merge-scheduler.yml
@@ -94,7 +94,8 @@ concurrency:
github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) ||
github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) ||
github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) ||
- github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) ||
+ github.event_name == 'repository_dispatch' && github.event.client_payload.target_repository != '' && github.event.client_payload.pr_number > 0 && format('target-{0}-pr-{1}', github.event.client_payload.target_repository, github.event.client_payload.pr_number) ||
+ github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number > 0 && format('pr-{0}', github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && github.run_id ||
github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}
@@ -227,6 +228,70 @@ jobs:
echo "token=$app_token"
} >>"$GITHUB_OUTPUT"
+ - name: Bind targeted central dispatch to live pull request metadata
+ id: dispatch_target
+ if: >-
+ github.event_name == 'repository_dispatch' &&
+ github.event.client_payload.target_repository != ''
+ env:
+ GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
+ MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
+ TARGET_REPOSITORY: ${{ github.event.client_payload.target_repository }}
+ TARGET_PR_NUMBER: ${{ github.event.client_payload.pr_number }}
+ run: |
+ set -euo pipefail
+
+ if [ "$GITHUB_REPOSITORY" != "ContextualWisdomLab/.github" ]; then
+ echo "::error::Cross-repository targeted dispatch is only allowed from ContextualWisdomLab/.github."
+ exit 1
+ fi
+ if [[ ! "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9._-]+$ ]]; then
+ echo "::error::target_repository must name a ContextualWisdomLab repository."
+ exit 1
+ fi
+ if [[ ! "$TARGET_PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
+ echo "::error::pr_number must be a positive integer for targeted dispatch."
+ exit 1
+ fi
+ if [ "$MUTATION_TOKEN_SOURCE" = "github-token" ]; then
+ echo "::error::Targeted central dispatch has no sibling-repository credential. Configure PR_REVIEW_MERGE_TOKEN, OPENCODE_APPROVE_TOKEN, or the OpenCode app token exchange."
+ exit 1
+ fi
+
+ pull_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")"
+ live_state="$(jq -r '.state // empty' <<<"$pull_json")"
+ base_repository="$(jq -r '.base.repo.full_name // empty' <<<"$pull_json")"
+ base_branch="$(jq -r '.base.ref // empty' <<<"$pull_json")"
+ base_sha="$(jq -r '.base.sha // empty' <<<"$pull_json")"
+ head_sha="$(jq -r '.head.sha // empty' <<<"$pull_json")"
+
+ if [ "$live_state" != "open" ] || [ "$base_repository" != "$TARGET_REPOSITORY" ]; then
+ echo "::error::Targeted dispatch must resolve to an open PR whose base repository exactly matches target_repository."
+ exit 1
+ fi
+ if [[ ! "$base_branch" =~ ^[A-Za-z0-9._/-]+$ ]] ||
+ [[ "$base_branch" == "HEAD" ]] || [[ "$base_branch" == /* ]] ||
+ [[ "$base_branch" == *".."* ]] || [[ "$base_branch" == *"@{"* ]] ||
+ [[ "$base_branch" == *"//"* ]] ||
+ [[ "$base_branch" == .* ]] || [[ "$base_branch" == */.* ]] ||
+ [[ "$base_branch" == */ ]] || [[ "$base_branch" == *. ]]; then
+ echo "::error::Live PR base ref is not safe for targeted scheduler arguments."
+ exit 1
+ fi
+ if [[ ! "$base_sha" =~ ^[0-9a-fA-F]{40}$ ]] || [[ ! "$head_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
+ echo "::error::Live PR metadata did not provide exact 40-character base and head SHAs."
+ exit 1
+ fi
+
+ {
+ echo "repository=$TARGET_REPOSITORY"
+ echo "pr_number=$TARGET_PR_NUMBER"
+ echo "base_branch=$base_branch"
+ echo "base_sha=$base_sha"
+ echo "head_sha=$head_sha"
+ } >>"$GITHUB_OUTPUT"
+ echo "Targeted scheduler bound to ${TARGET_REPOSITORY}#${TARGET_PR_NUMBER} at head ${head_sha} (base ${base_branch}@${base_sha})."
+
- name: Resolve trusted scheduler source ref
id: trusted_source
env:
@@ -406,21 +471,34 @@ jobs:
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
SCHEDULER_ACTIONS_TOKEN: ${{ github.token }}
+ # A targeted central run controls only central Actions. The app/PAT in
+ # GH_TOKEN reads and mutates the sibling PR, while github.token inspects
+ # and dispatches the central required workflows without making a
+ # doomed sibling Actions API request first.
+ SCHEDULER_ACTIONS_REPOSITORY: ${{ steps.dispatch_target.outputs.repository != '' && github.repository || '' }}
# Same-repository dispatch credential: when this scheduler runs inside
# ContextualWisdomLab/.github (the repository the required workflows are
# dispatched on), the runner token can dispatch them without any
# cross-repository PAT. The scheduler only uses it when
# GITHUB_REPOSITORY equals the dispatch repository.
SCHEDULER_DISPATCH_TOKEN: ${{ github.token }}
- SCHEDULER_READ_TOKEN: ${{ github.token }}
+ SCHEDULER_READ_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.scheduler_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github
+ # Bound the aggregate central model-review queue across all target
+ # repositories. Per-run REVIEW_DISPATCH_LIMIT alone cannot prevent a
+ # large set of concurrent scheduler runs from saturating Actions.
+ ACTIVE_OPENCODE_REVIEW_LIMIT: ${{ vars.ACTIVE_OPENCODE_REVIEW_LIMIT || '16' }}
SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }}
+ SCHEDULER_TARGET_REPOSITORY: ${{ steps.dispatch_target.outputs.repository || github.repository }}
+ SCHEDULER_TARGET_BASE_BRANCH: ${{ steps.dispatch_target.outputs.base_branch || env.DEFAULT_BRANCH }}
+ SCHEDULER_TARGET_PR_NUMBER: ${{ steps.dispatch_target.outputs.pr_number || env.PULL_REQUEST_NUMBER }}
+ SCHEDULER_TARGET_HEAD_SHA: ${{ steps.dispatch_target.outputs.head_sha || '' }}
run: |
set -euo pipefail
project_flow="$PROJECT_FLOW_INPUT"
if [ -z "$project_flow" ]; then
- case "$DEFAULT_BRANCH" in
+ case "$SCHEDULER_TARGET_BASE_BRANCH" in
main|master) project_flow="github-flow" ;;
develop) project_flow="git-flow" ;;
*) project_flow="github-flow" ;;
@@ -435,8 +513,8 @@ jobs:
branch_update_limit="1"
fi
args=(
- --repo "$GITHUB_REPOSITORY"
- --base-branch "$DEFAULT_BRANCH"
+ --repo "$SCHEDULER_TARGET_REPOSITORY"
+ --base-branch "$SCHEDULER_TARGET_BASE_BRANCH"
--max-prs "$MAX_PRS"
--project-flow "$project_flow"
--review-workflow "Required OpenCode Review"
@@ -444,8 +522,11 @@ jobs:
--branch-update-limit "$branch_update_limit"
--stale-opencode-minutes "$STALE_OPENCODE_MINUTES"
)
- if [ -n "$PULL_REQUEST_NUMBER" ]; then
- args+=(--pr-number "$PULL_REQUEST_NUMBER")
+ if [ -n "$SCHEDULER_TARGET_PR_NUMBER" ]; then
+ args+=(--pr-number "$SCHEDULER_TARGET_PR_NUMBER")
+ fi
+ if [ -n "$SCHEDULER_TARGET_HEAD_SHA" ]; then
+ args+=(--expected-head-sha "$SCHEDULER_TARGET_HEAD_SHA")
fi
if [ "$DRY_RUN" = "true" ]; then
args+=(--dry-run)
@@ -671,6 +752,7 @@ jobs:
SCHEDULER_DISPATCH_TOKEN: ${{ github.token }}
SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.sweep_app_token.outputs.available == 'true' && 'opencode-app' || 'github-token' }}
SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github
+ ACTIVE_OPENCODE_REVIEW_LIMIT: ${{ vars.ACTIVE_OPENCODE_REVIEW_LIMIT || '16' }}
SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH: ${{ (secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '') && 'true' || 'false' }}
run: |
set -euo pipefail
diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt
index 4cf45203..655e090c 100644
--- a/requirements-strix-ci-hashes.txt
+++ b/requirements-strix-ci-hashes.txt
@@ -1501,9 +1501,9 @@ protobuf==6.33.6 \
# grpc-google-iam-v1
# grpcio-status
# proto-plus
-pyasn1==0.6.3 \
- --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \
- --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde
+pyasn1==0.6.4 \
+ --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \
+ --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b
# via pyasn1-modules
pyasn1-modules==0.4.2 \
--hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py
index 5b024c84..a392e791 100644
--- a/scripts/ci/noema_review_gate.py
+++ b/scripts/ci/noema_review_gate.py
@@ -5,6 +5,7 @@
import argparse
import base64
+import http.client
import ipaddress
import json
import os
@@ -29,6 +30,10 @@
"opencode-review-control-v1",
)
REVIEW_BODY_HEAD_SHA_RE = re.compile(r"Head SHA:\s*`([0-9a-fA-F]{40})`")
+NOEMA_REVIEW_MARKER_HEAD_RE = re.compile(
+ r"")]}),
"noema",
)
+ assert noema.existing_noema_review(
+ make_pr(
+ reviews={
+ "nodes": [
+ review(
+ login="NoEmA",
+ body="",
+ )
+ ]
+ }
+ ),
+ "NOEMA",
+ )
assert not noema.existing_noema_review(make_pr(reviews={"nodes": [review("DISMISSED", login="noema")]}), "noema")
assert not noema.existing_noema_review(make_pr(reviews={"nodes": [review(commit="old", login="noema")]}), "noema")
+def test_existing_noema_review_rejects_mismatched_body_and_marker_heads():
+ current_head = "a" * 40
+ previous_head = "b" * 40
+ mismatched_body = review(
+ commit=current_head,
+ login="noema",
+ body=(
+ f"- Head SHA: `{previous_head}`\n"
+ f""
+ ),
+ )
+ mismatched_marker = review(
+ commit=current_head,
+ login="noema",
+ body=(
+ f"- Head SHA: `{current_head}`\n"
+ f""
+ ),
+ )
+ exact = review(
+ commit=current_head,
+ login="noema",
+ body=(
+ f"- Head SHA: `{current_head}`\n"
+ f""
+ ),
+ )
+ def pr(item):
+ return make_pr(headRefOid=current_head, reviews={"nodes": [item]})
+
+ assert not noema.existing_noema_review(pr(mismatched_body), "noema")
+ assert not noema.existing_noema_review(pr(mismatched_marker), "noema")
+ assert not noema.existing_noema_review(
+ pr(
+ review(
+ commit=current_head,
+ login="noema",
+ body=f"- Head SHA: `{current_head}`\nResult: APPROVE",
+ )
+ ),
+ "noema",
+ )
+ assert noema.existing_noema_review(pr(exact), "noema")
+ assert not noema.existing_noema_review(
+ pr(review(commit=current_head, login="attacker", body=exact["body"])),
+ "noema",
+ )
+ assert not noema.existing_noema_review(pr(exact), "")
+
+
def test_current_actor_fetch_diff_and_json_extraction(monkeypatch):
- monkeypatch.setattr(noema, "run", lambda *args, **kwargs: "noema\n")
+ monkeypatch.setattr(noema, "run", lambda *args, **kwargs: "NoEmA\n")
assert noema.current_actor() == "noema"
monkeypatch.setattr(noema, "run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("no gh")))
assert noema.current_actor() == ""
@@ -297,11 +360,12 @@ def test_review_context_reports_omitted_files_and_missing_codegraph(monkeypatch,
class FakeResponse:
- """Small context-manager response for urllib monkeypatches."""
+ """Small response object for pinned HTTPS transport tests."""
- def __init__(self, payload):
+ def __init__(self, payload, status=200):
"""Store a JSON-serializable response payload."""
self.payload = payload
+ self.status = status
def __enter__(self):
"""Return the response for with-statement use."""
@@ -325,98 +389,213 @@ def test_call_llm_handles_configuration_and_verdicts(monkeypatch):
monkeypatch.setenv("NOEMA_LLM_API_URL", "file:///etc/passwd")
monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret")
- with pytest.raises(ValueError, match="URL scheme must be http or https"):
+ with pytest.raises(ValueError, match="must use https"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat")
- monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret")
- monkeypatch.setenv("NOEMA_LLM_MODEL", "review-model")
+ response = {
+ "value": FakeResponse(
+ {"choices": [{"message": {"content": '{"decision":"approve","summary":"ok","findings":[]}'}}]}
+ )
+ }
seen = {}
- def fake_urlopen(request, timeout):
- seen["url"] = request.full_url
- seen["body"] = json.loads(request.data.decode("utf-8"))
- return FakeResponse({"choices": [{"message": {"content": '{"decision":"approve","summary":"ok","findings":[]}'}}]})
+ class FakeConnection:
+ def __init__(self, hostname, port, pinned_ips, *, timeout):
+ seen.update(hostname=hostname, port=port, pinned_ips=pinned_ips, timeout=timeout)
- # Since we replaced urlopen with build_opener, we mock build_opener
- class FakeOpener:
- def __init__(self, call_func):
- self.call_func = call_func
- def open(self, request, timeout=None):
- return self.call_func(request, timeout)
+ def request(self, method, target, *, body, headers):
+ seen.update(
+ method=method,
+ target=target,
+ body=json.loads(body.decode("utf-8")),
+ headers=headers,
+ )
+
+ def getresponse(self):
+ return response["value"]
+
+ def close(self):
+ seen["closed"] = True
+
+ def public_dns(host, port, *args, **kwargs):
+ return [
+ (noema.socket.AF_INET, noema.socket.SOCK_STREAM, 6, "", ("8.8.8.8", port)),
+ (noema.socket.AF_INET, noema.socket.SOCK_STREAM, 6, "", ("8.8.8.8", port)),
+ ]
- monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *args: FakeOpener(fake_urlopen))
+ monkeypatch.setattr(noema.socket, "getaddrinfo", public_dns)
+ monkeypatch.setattr(noema, "PinnedHTTPSConnection", FakeConnection)
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat;v=1?mode=review")
+ monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret")
+ monkeypatch.setenv("NOEMA_LLM_MODEL", "review-model")
verdict = noema.call_llm("owner/repo", 1, pr, "diff", True, "extra review context")
assert verdict["decision"] == "approve"
- assert seen["url"] == "https://llm.example.test/chat"
+ assert seen["hostname"] == "llm.example.test"
+ assert seen["port"] == 443
+ assert seen["pinned_ips"] == ("8.8.8.8",)
+ assert seen["target"] == "/chat;v=1?mode=review"
+ assert seen["headers"]["authorization"] == "Bearer secret"
assert seen["body"]["model"] == "review-model"
assert "extra review context" in seen["body"]["messages"][1]["content"]
+ assert seen["closed"] is True
- def fake_urlopen_defer(request, timeout=None):
- return FakeResponse({"choices": [{"message": {"content": '{"decision":"defer"}'}}]})
-
- monkeypatch.setattr(
- noema.urllib.request,
- "build_opener",
- lambda *args: FakeOpener(fake_urlopen_defer)
+ response["value"] = FakeResponse(
+ {"choices": [{"message": {"content": '{"decision":"defer"}'}}]}
)
with pytest.raises(RuntimeError, match="unsupported decision"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test case-insensitive valid URL
+ response["value"] = FakeResponse(
+ {"choices": [{"message": {"content": '{"decision":"approve","summary":"ok","findings":[]}'}}]}
+ )
monkeypatch.setenv("NOEMA_LLM_API_URL", "HTTPS://llm.example.test/chat")
- monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *args: FakeOpener(fake_urlopen))
assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve"
- # Test invalid scheme (and no original URL in error)
- monkeypatch.setenv("NOEMA_LLM_API_URL", "file:///etc/passwd")
- with pytest.raises(ValueError, match="URL scheme must be http or https"):
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "http://llm.example.test/chat")
+ with pytest.raises(ValueError, match="must use https"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test localhost rejection
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http://localhost/chat")
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://localhost/chat")
with pytest.raises(ValueError, match="URL cannot target localhost"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test missing hostname
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http:///chat")
- with pytest.raises(ValueError, match="URL must have a valid hostname"):
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https:///chat")
+ with pytest.raises(ValueError, match="NOEMA_LLM_API_URL must have a valid hostname"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test internal IP rejection
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http://169.254.169.254/chat")
- with pytest.raises(ValueError, match="URL cannot target internal IP addresses"):
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat path")
+ with pytest.raises(ValueError, match="NOEMA_LLM_API_URL cannot contain whitespace"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- import socket
- original_getaddrinfo = socket.getaddrinfo
-
- # Test DNS resolution bypass
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http://resolved-to-local.example.com/chat")
- def fake_getaddrinfo(host, port, *args, **kwargs):
- if host == "resolved-to-local.example.com":
- return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 0))]
- return original_getaddrinfo(host, port, *args, **kwargs)
- monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo)
- with pytest.raises(ValueError, match="URL cannot target internal IP addresses"):
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://169.254.169.254/chat")
+ monkeypatch.setattr(
+ noema.socket,
+ "getaddrinfo",
+ lambda host, port, *args, **kwargs: [
+ (noema.socket.AF_INET, noema.socket.SOCK_STREAM, 6, "", ("169.254.169.254", port))
+ ],
+ )
+ with pytest.raises(
+ ValueError,
+ match="NOEMA_LLM_API_URL cannot target non-public IP addresses",
+ ):
noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test unresolved hostname does not break
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http://unresolved.example.com/chat")
- def fake_getaddrinfo_error(host, port, *args, **kwargs):
- raise socket.gaierror("Name or service not known")
- monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo_error)
- monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *args: FakeOpener(fake_urlopen))
- assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve"
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://unresolved.example.com/chat")
+ monkeypatch.setattr(
+ noema.socket,
+ "getaddrinfo",
+ lambda *args, **kwargs: (_ for _ in ()).throw(noema.socket.gaierror("not found")),
+ )
+ with pytest.raises(ValueError, match="could not be resolved"):
+ noema.call_llm("owner/repo", 1, pr, "diff", False)
- # Test invalid IP string from getaddrinfo (unlikely but theoretically possible)
- monkeypatch.setenv("NOEMA_LLM_API_URL", "http://weird-dns.example.com/chat")
- def fake_getaddrinfo_invalid_ip(host, port, *args, **kwargs):
- if host == "weird-dns.example.com":
- return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("not_an_ip", 0))]
- return original_getaddrinfo(host, port, *args, **kwargs)
- monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo_invalid_ip)
- assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve"
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://weird-dns.example.com/chat")
+ monkeypatch.setattr(
+ noema.socket,
+ "getaddrinfo",
+ lambda host, port, *args, **kwargs: [
+ (noema.socket.AF_INET, noema.socket.SOCK_STREAM, 6, "", ("not_an_ip", port))
+ ],
+ )
+ with pytest.raises(ValueError, match="invalid IP"):
+ noema.call_llm("owner/repo", 1, pr, "diff", False)
+
+ monkeypatch.setattr(noema.socket, "getaddrinfo", lambda *args, **kwargs: [])
+ with pytest.raises(ValueError, match="no addresses"):
+ noema.call_llm("owner/repo", 1, pr, "diff", False)
+
+ monkeypatch.setattr(noema.socket, "getaddrinfo", public_dns)
+ for unsafe_url, message in (
+ ("https://user:password@llm.example.test/chat", "user information"),
+ ("https://llm.example.test/chat#fragment", "fragment"),
+ ("https://llm.example.test:bad/chat", "invalid port"),
+ ):
+ monkeypatch.setenv("NOEMA_LLM_API_URL", unsafe_url)
+ with pytest.raises(ValueError, match=message):
+ noema.call_llm("owner/repo", 1, pr, "diff", False)
+
+ monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat")
+ response["value"] = FakeResponse({"error": "redirect denied"}, status=302)
+ with pytest.raises(RuntimeError, match="HTTP 302"):
+ noema.call_llm("owner/repo", 1, pr, "diff", False)
+
+
+def test_pinned_https_connection_uses_numeric_peer_and_original_sni(monkeypatch):
+ """The transport must never resolve the validated hostname a second time."""
+ seen = {}
+
+ class FakeSocket:
+ def setsockopt(self, *args):
+ seen["setsockopt"] = args
+
+ def close(self):
+ seen["closed"] = True
+
+ fake_socket = FakeSocket()
+
+ def fake_create_connection(address, timeout, source_address):
+ seen.update(address=address, timeout=timeout, source_address=source_address)
+ return fake_socket
+
+ class FakeContext:
+ def wrap_socket(self, sock, *, server_hostname):
+ seen.update(wrapped_socket=sock, server_hostname=server_hostname)
+ return sock
+
+ monkeypatch.setattr(noema.socket, "create_connection", fake_create_connection)
+ connection = noema.PinnedHTTPSConnection("llm.example.test", 443, "8.8.8.8", timeout=12)
+ connection._context = FakeContext()
+ connection.connect()
+ connection.close()
+
+ assert seen["address"] == ("8.8.8.8", 443)
+ assert seen["server_hostname"] == "llm.example.test"
+ assert seen["wrapped_socket"] is fake_socket
+ assert seen["closed"] is True
+
+
+def test_pinned_https_connection_falls_back_across_validated_addresses(monkeypatch):
+ """A failed numeric peer must fall back without another DNS lookup."""
+ attempts = []
+
+ class FakeSocket:
+ def setsockopt(self, *args):
+ return None
+
+ def close(self):
+ return None
+
+ successful_socket = FakeSocket()
+
+ def fake_create_connection(address, timeout, source_address):
+ attempts.append(address)
+ if address[0] == "8.8.8.8":
+ raise OSError("first peer unavailable")
+ return successful_socket
+
+ class FakeContext:
+ def wrap_socket(self, sock, *, server_hostname):
+ assert server_hostname == "llm.example.test"
+ return sock
+
+ monkeypatch.setattr(noema.socket, "create_connection", fake_create_connection)
+ monkeypatch.setattr(
+ noema.socket,
+ "getaddrinfo",
+ lambda *args, **kwargs: pytest.fail("pinned transport must not resolve DNS again"),
+ )
+ connection = noema.PinnedHTTPSConnection(
+ "llm.example.test",
+ 443,
+ ("8.8.8.8", "1.1.1.1"),
+ timeout=12,
+ )
+ connection._context = FakeContext()
+ connection.connect()
+ connection.close()
+
+ assert attempts == [("8.8.8.8", 443), ("1.1.1.1", 443)]
def test_noema_redirect_handler_rejects_redirects():
@@ -450,7 +629,7 @@ def raise_gaierror(host, port, *args, **kwargs):
raise socket.gaierror("Name or service not known")
monkeypatch.setattr(socket, "getaddrinfo", raise_gaierror)
- with pytest.raises(ValueError, match="must start with http:// or https://"):
+ with pytest.raises(ValueError, match="control characters"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
@@ -462,7 +641,7 @@ def test_call_llm_rejects_non_http_parsed_scheme(monkeypatch):
parsed = noema.urllib.parse.ParseResult("file", "llm.example.test", "/chat", "", "", "")
monkeypatch.setattr(noema.urllib.parse, "urlparse", lambda _: parsed)
- with pytest.raises(ValueError, match="URL scheme must be http or https"):
+ with pytest.raises(ValueError, match="https scheme"):
noema.call_llm("owner/repo", 1, pr, "diff", False)
@@ -521,6 +700,7 @@ def test_inspect_and_review_skip_paths(monkeypatch):
(make_pr(reviews={"nodes": [review(body=marker_body)]}, reviewThreads={"nodes": [{"isResolved": False, "isOutdated": False}]}), "noema"),
(make_pr(reviews={"nodes": [review(body=marker_body)]}, statusCheckRollup={"contexts": {"nodes": [{"__typename": "StatusContext", "context": "ci", "state": "FAILURE"}]}}), "noema"),
(clean_pr, "opencode-agent"),
+ (clean_pr, ""),
]
for pr, actor in cases:
calls.clear()
diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py
index c8eb1d34..fd2e34ec 100644
--- a/tests/test_opencode_agent_contract.py
+++ b/tests/test_opencode_agent_contract.py
@@ -1433,7 +1433,15 @@ def test_merge_scheduler_uses_escalating_mutation_credentials():
assert "secrets.PR_REVIEW_MERGE_TOKEN" in workflow
assert "secrets.OPENCODE_APPROVE_TOKEN" in workflow
assert "steps.scheduler_app_token.outputs.token" in workflow
- assert "SCHEDULER_READ_TOKEN: ${{ github.token }}" in workflow
+ assert (
+ "SCHEDULER_READ_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || "
+ "secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || "
+ "github.token }}"
+ ) in workflow
+ assert "SCHEDULER_ACTIONS_REPOSITORY:" in workflow
+ assert workflow.count(
+ "ACTIVE_OPENCODE_REVIEW_LIMIT: ${{ vars.ACTIVE_OPENCODE_REVIEW_LIMIT || '16' }}"
+ ) == 2
assert "SCHEDULER_MUTATION_TOKEN_SOURCE" in workflow
assert 'default: "1"' in workflow
assert 'review_dispatch_limit="-1"' in workflow
diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py
index 1fe10da7..47e99911 100644
--- a/tests/test_pr_review_merge_scheduler.py
+++ b/tests/test_pr_review_merge_scheduler.py
@@ -1,5 +1,7 @@
import json
import sys
+import threading
+from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
import pytest
@@ -349,6 +351,79 @@ def fake_run(args, stdin=None):
assert sleeps == [1, 2]
+def test_gh_graphql_retries_truncated_cli_json_errors(monkeypatch):
+ """A truncated GitHub response must retry before falling back to REST."""
+ calls = []
+ sleeps = []
+
+ def fake_run(args, stdin=None):
+ calls.append((args, stdin))
+ if len(calls) < 4:
+ raise RuntimeError(
+ "Command failed (1): gh api graphql\nunexpected end of JSON input"
+ )
+ return '{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false}}}}}'
+
+ monkeypatch.setattr(sched, "run", fake_run)
+ monkeypatch.setattr(sched.time, "sleep", lambda seconds: sleeps.append(seconds))
+
+ payload = sched.gh_graphql("query", owner="owner", name="repo", pageSize=25)
+
+ assert payload["data"]["repository"]["pullRequests"]["nodes"] == []
+ assert len(calls) == 4
+ assert sleeps == [1, 2, 4]
+
+
+def test_gh_graphql_retries_truncated_success_stdout(monkeypatch):
+ """Invalid JSON on stdout is transient even when gh exits successfully."""
+ responses = [
+ '{"data":{"repository":',
+ '{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false}}}}}',
+ ]
+ sleeps = []
+
+ monkeypatch.setattr(sched, "run", lambda args, stdin=None: responses.pop(0))
+ monkeypatch.setattr(sched.time, "sleep", lambda seconds: sleeps.append(seconds))
+
+ payload = sched.gh_graphql("query", owner="owner", name="repo", pageSize=25)
+
+ assert payload["data"]["repository"]["pullRequests"]["nodes"] == []
+ assert sleeps == [1]
+
+
+def test_gh_graphql_does_not_retry_structurally_invalid_json(monkeypatch):
+ """A complete non-JSON response fails immediately instead of hiding its cause."""
+ calls = []
+ sleeps = []
+
+ def fake_run(args, stdin=None):
+ calls.append((args, stdin))
+ return "proxy authentication required"
+
+ monkeypatch.setattr(sched, "run", fake_run)
+ monkeypatch.setattr(sched.time, "sleep", lambda seconds: sleeps.append(seconds))
+
+ with pytest.raises(RuntimeError, match="invalid JSON") as exc_info:
+ sched.gh_graphql("query", owner="owner")
+
+ assert isinstance(exc_info.value.__cause__, json.JSONDecodeError)
+ assert len(calls) == 1
+ assert sleeps == []
+
+
+def test_gh_graphql_preserves_final_json_decode_cause(monkeypatch):
+ """A final invalid response must retain the parser exception for diagnostics."""
+ monkeypatch.setattr(sched, "run", lambda args, stdin=None: '{"data":')
+ monkeypatch.setattr(sched.time, "sleep", lambda seconds: None)
+
+ with pytest.raises(RuntimeError, match="invalid JSON") as exc_info:
+ sched.gh_graphql("query", owner="owner")
+
+ assert isinstance(exc_info.value.__cause__, json.JSONDecodeError)
+ assert "line 1 column 9" in str(exc_info.value)
+ assert "char 8" in str(exc_info.value)
+
+
def test_gh_graphql_retries_transient_http2_stream_cancel(monkeypatch):
calls = []
sleeps = []
@@ -1642,8 +1717,32 @@ def fake_run(args, stdin=None):
sched.dispatch_opencode_review("owner/repo", "OpenCode Review", required_workflow_pr, dry_run=False)
sched.dispatch_strix_evidence("owner/repo", "Strix Security Scan", required_workflow_pr, dry_run=False)
assert calls[:2] == [
- ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", "-f", "status=queued", "-F", "per_page=100"],
- ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", "-f", "status=in_progress", "-F", "per_page=100"],
+ [
+ "gh",
+ "api",
+ "--method",
+ "GET",
+ "repos/owner/repo/actions/runs",
+ "-f",
+ "status=queued",
+ "-F",
+ "per_page=100",
+ "-F",
+ "page=1",
+ ],
+ [
+ "gh",
+ "api",
+ "--method",
+ "GET",
+ "repos/owner/repo/actions/runs",
+ "-f",
+ "status=in_progress",
+ "-F",
+ "per_page=100",
+ "-F",
+ "page=1",
+ ],
]
assert calls[2:] == [
["gh", "api", "-X", "POST", "repos/owner/repo/dispatches", "--input", "-"],
@@ -1942,6 +2041,19 @@ def test_stacked_pr_waits_when_opencode_dispatch_is_already_active(monkeypatch):
assert stacked.reason == "stacked PR onto develop; same-head OpenCode workflow run is already active"
+def test_stacked_pr_waits_when_central_opencode_capacity_is_full(monkeypatch):
+ monkeypatch.setattr(
+ sched,
+ "dispatch_opencode_review",
+ lambda repo, workflow, pr, dry_run: "capacity_reached",
+ )
+
+ stacked = inspect(make_pr(baseRefName="develop"))
+
+ assert stacked.action == "wait"
+ assert stacked.reason == "stacked PR onto develop; central OpenCode active-run capacity is full"
+
+
def test_cross_repo_dispatch_wait_reason_can_be_explicitly_enabled(monkeypatch):
monkeypatch.setenv("SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY", "ContextualWisdomLab/.github")
monkeypatch.delenv("SCHEDULER_ALLOW_CROSS_REPO_REPOSITORY_DISPATCH", raising=False)
@@ -1995,6 +2107,128 @@ def test_scheduler_dispatch_env_is_noop_without_distinct_token(monkeypatch):
assert sched.scheduler_dispatch_env() is None
+def test_actions_repository_scope_skips_inaccessible_sibling_queries(monkeypatch, capsys):
+ """A central runner token must not query sibling Actions before dispatch."""
+ monkeypatch.setenv(
+ "SCHEDULER_ACTIONS_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+ monkeypatch.setattr(
+ sched,
+ "run_github_actions",
+ lambda *args, **kwargs: pytest.fail("out-of-scope Actions API call"),
+ )
+
+ assert sched.active_workflow_runs("ContextualWisdomLab/scopeweave") == []
+ captured = capsys.readouterr()
+ assert "Actions run inspection skipped for ContextualWisdomLab/scopeweave" in captured.err
+ assert captured.out == ""
+
+
+def test_actions_repository_scope_keeps_central_run_inspection(monkeypatch):
+ """The same scope still permits deduplication against central workflows."""
+ calls = []
+ monkeypatch.setenv(
+ "SCHEDULER_ACTIONS_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+ monkeypatch.setattr(
+ sched,
+ "run_github_actions",
+ lambda args, stdin=None: calls.append(args) or json.dumps({"workflow_runs": []}),
+ )
+
+ assert sched.active_workflow_runs("ContextualWisdomLab/.github") == []
+ assert len(calls) == 2
+ assert all("repos/ContextualWisdomLab/.github/actions/runs" in call for call in calls)
+
+
+def test_actions_repository_scope_is_case_insensitive(monkeypatch):
+ """GitHub repository identity must not depend on owner/name casing."""
+ monkeypatch.setenv(
+ "SCHEDULER_ACTIONS_REPOSITORY",
+ "contextualwisdomlab/.GITHUB",
+ )
+
+ assert sched.scheduler_actions_repository_in_scope("ContextualWisdomLab/.github")
+
+
+def test_actions_repository_scope_names_invalid_configuration(monkeypatch):
+ """An invalid scoped Actions repository must identify its configuration key."""
+ monkeypatch.setenv("SCHEDULER_ACTIONS_REPOSITORY", "invalid-repository")
+
+ with pytest.raises(RuntimeError, match="SCHEDULER_ACTIONS_REPOSITORY") as exc_info:
+ sched.scheduler_actions_repository_in_scope("ContextualWisdomLab/.github")
+
+ assert "invalid-repository" in str(exc_info.value)
+
+
+def test_active_workflow_runs_paginates_each_status(monkeypatch):
+ """Capacity accounting must include active runs beyond the first 100."""
+ calls = []
+
+ def fake_run(args, stdin=None):
+ calls.append(args)
+ status = args[args.index("-f") + 1].split("=", 1)[1]
+ page = int(args[-1].split("=", 1)[1])
+ if page == 1:
+ return json.dumps({"workflow_runs": [{"id": f"{status}-{index}"} for index in range(100)]})
+ return json.dumps({"workflow_runs": [{"id": f"{status}-last"}]})
+
+ monkeypatch.setattr(sched, "run_github_actions", fake_run)
+
+ runs = sched.active_workflow_runs("ContextualWisdomLab/.github")
+
+ assert len(runs) == 202
+ assert [call[-1] for call in calls] == ["page=1", "page=2", "page=1", "page=2"]
+
+
+def test_active_workflow_runs_stops_at_exact_total_count(monkeypatch):
+ """An exact 100-run page must not trigger a redundant second API request."""
+ calls = []
+
+ def fake_run(args, stdin=None):
+ calls.append(args)
+ status = args[args.index("-f") + 1].split("=", 1)[1]
+ return json.dumps(
+ {
+ "total_count": 100,
+ "workflow_runs": [
+ {"id": f"{status}-{index}"} for index in range(100)
+ ],
+ }
+ )
+
+ monkeypatch.setattr(sched, "run_github_actions", fake_run)
+
+ runs = sched.active_workflow_runs("ContextualWisdomLab/.github")
+
+ assert len(runs) == 200
+ assert [call[-1] for call in calls] == ["page=1", "page=1"]
+
+
+def test_actions_repository_scope_refuses_sibling_job_rerun(monkeypatch):
+ """A central runner token must not rerun a sibling repository job."""
+ monkeypatch.setenv(
+ "SCHEDULER_ACTIONS_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+
+ with pytest.raises(
+ RuntimeError,
+ match=(
+ "retry failed job refused for ContextualWisdomLab/scopeweave; "
+ "the Actions credential is scoped to ContextualWisdomLab/.github"
+ ),
+ ):
+ sched.rerun_actions_job(
+ "ContextualWisdomLab/scopeweave",
+ "42",
+ dry_run=False,
+ action="retry failed job",
+ )
+
+
def test_run_github_dispatch_uses_dispatch_token_env(monkeypatch):
calls = []
monkeypatch.setenv("GH_TOKEN", "mutation-token")
@@ -2065,7 +2299,19 @@ def fake_run(args, stdin=None):
)
assert result == "already_running"
- assert ["gh", "api", "--method", "GET", "repos/owner/repo/actions/runs", "-f", "status=queued", "-F", "per_page=100"] in calls
+ assert [
+ "gh",
+ "api",
+ "--method",
+ "GET",
+ "repos/owner/repo/actions/runs",
+ "-f",
+ "status=queued",
+ "-F",
+ "per_page=100",
+ "-F",
+ "page=1",
+ ] in calls
assert ["gh", "api", "-X", "POST", "repos/owner/repo/actions/runs/9001/force-cancel"] in calls
assert not any("9002/force-cancel" in " ".join(call) for call in calls)
assert not any("9003/force-cancel" in " ".join(call) for call in calls)
@@ -2124,6 +2370,170 @@ def fake_run(args, stdin=None):
assert not any(call[:3] == ["gh", "workflow", "run"] for call in calls)
+def test_active_central_opencode_dispatch_refs_filters_unrelated_runs(monkeypatch):
+ central_runs = [
+ {
+ "id": 9200,
+ "name": "Required OpenCode Review owner/repo#1@" + "a" * 40,
+ "display_title": "Required OpenCode Review owner/repo#1@" + "a" * 40,
+ "event": "repository_dispatch",
+ },
+ {
+ "id": 9201,
+ "name": "Required OpenCode Review",
+ "display_title": "Required OpenCode Review owner/other#2@" + "b" * 40,
+ "event": "repository_dispatch",
+ },
+ {
+ "id": 9202,
+ "name": "Required OpenCode Review",
+ "display_title": "ordinary pull request check",
+ "event": "pull_request_target",
+ },
+ {
+ "id": 9203,
+ "name": "Strix Security Scan owner/repo#1@" + "a" * 40,
+ "display_title": "Strix Security Scan owner/repo#1@" + "a" * 40,
+ "event": "repository_dispatch",
+ },
+ ]
+ monkeypatch.setenv(
+ "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+ monkeypatch.setattr(
+ sched,
+ "active_workflow_runs",
+ lambda repo, statuses=("queued", "in_progress"): central_runs,
+ )
+
+ assert sched.active_central_opencode_dispatch_refs(
+ "owner/repo",
+ "OpenCode Review",
+ ) == [
+ ("ContextualWisdomLab/.github", "9200"),
+ ("ContextualWisdomLab/.github", "9201"),
+ ]
+
+
+def test_central_dispatch_capacity_is_cached_and_reserved_per_heartbeat(monkeypatch):
+ """One heartbeat performs one central run read and counts accepted dispatches."""
+ calls = 0
+
+ def fake_active_runs(repo, statuses=("queued", "in_progress")):
+ nonlocal calls
+ calls += 1
+ assert repo == "ContextualWisdomLab/.github"
+ assert statuses == ("queued", "in_progress")
+ return [
+ {
+ "id": 9200,
+ "name": "Required OpenCode Review",
+ "display_title": "Required OpenCode Review owner/repo#1@" + "a" * 40,
+ "event": "repository_dispatch",
+ }
+ ]
+
+ monkeypatch.setenv(
+ "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+ monkeypatch.setattr(sched, "active_workflow_runs", fake_active_runs)
+
+ with sched.central_opencode_dispatch_cache():
+ first = sched.active_central_opencode_dispatch_refs(
+ "owner/repo", "OpenCode Review"
+ )
+ second = sched.active_central_opencode_dispatch_refs(
+ "owner/other", "OpenCode Review"
+ )
+ sched.reserve_central_opencode_dispatch_capacity(
+ "owner/repo", "OpenCode Review"
+ )
+ reserved = sched.active_central_opencode_dispatch_refs(
+ "owner/repo", "OpenCode Review"
+ )
+
+ assert first == second == [("ContextualWisdomLab/.github", "9200")]
+ assert len(reserved) == 2
+ assert reserved[-1][1].startswith("heartbeat-reservation-")
+ assert calls == 1
+
+
+def test_central_dispatch_capacity_cache_is_isolated_between_threads(monkeypatch):
+ """Concurrent scheduler contexts must not share mutable heartbeat caches."""
+ barrier = threading.Barrier(2)
+
+ def fake_active_runs(repo, statuses=("queued", "in_progress")):
+ del statuses
+ barrier.wait(timeout=30)
+ return [
+ {
+ "id": threading.current_thread().name,
+ "name": "Required OpenCode Review",
+ "event": "repository_dispatch",
+ }
+ ]
+
+ monkeypatch.setenv(
+ "SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY",
+ "ContextualWisdomLab/.github",
+ )
+ monkeypatch.setattr(sched, "active_workflow_runs", fake_active_runs)
+
+ def cached_refs():
+ with sched.central_opencode_dispatch_cache():
+ first = sched.active_central_opencode_dispatch_refs(
+ "owner/repo", "OpenCode Review"
+ )
+ second = sched.active_central_opencode_dispatch_refs(
+ "owner/repo", "OpenCode Review"
+ )
+ return first, second
+
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ results = list(executor.map(lambda _: cached_refs(), range(2)))
+
+ assert all(first == second for first, second in results)
+ assert results[0][0] != results[1][0]
+
+
+def test_dispatch_opencode_review_defers_when_central_capacity_is_full(monkeypatch, capsys):
+ monkeypatch.setenv("GITHUB_ACTIONS", "true")
+ monkeypatch.setenv("GH_TOKEN", "workflow-token")
+ monkeypatch.setenv("ACTIVE_OPENCODE_REVIEW_LIMIT", "2")
+ monkeypatch.setattr(sched, "active_opencode_run_refs", lambda *args, **kwargs: ([], []))
+ monkeypatch.setattr(sched, "force_cancel_workflow_run_refs", lambda refs: None)
+ monkeypatch.setattr(
+ sched,
+ "active_central_opencode_dispatch_refs",
+ lambda *args, **kwargs: [("central/repo", "1"), ("central/repo", "2")],
+ )
+ monkeypatch.setattr(
+ sched,
+ "run_github_dispatch",
+ lambda *args, **kwargs: pytest.fail("capacity guard must prevent dispatch"),
+ )
+
+ result = sched.dispatch_opencode_review(
+ "owner/repo",
+ "OpenCode Review",
+ make_pr(baseRefOid="b" * 40, headRefOid="a" * 40),
+ dry_run=False,
+ )
+
+ assert result == "capacity_reached"
+ assert "central active-run capacity is 2/2" in capsys.readouterr().out
+
+
+@pytest.mark.parametrize("value", ["not-an-integer", "-2"])
+def test_active_opencode_dispatch_limit_rejects_invalid_values(monkeypatch, value):
+ monkeypatch.setenv("ACTIVE_OPENCODE_REVIEW_LIMIT", value)
+
+ with pytest.raises(RuntimeError, match="must be an integer greater than or equal to -1"):
+ sched.active_opencode_dispatch_limit()
+
+
def test_dispatch_strix_cancels_stale_central_run_and_keeps_current(monkeypatch, capsys):
calls = []
head_sha = "a" * 40
@@ -2657,15 +3067,15 @@ def test_print_summary_writes_github_step_summary(monkeypatch, tmp_path, capsys)
assert payload["decisions"][5]["guidance"]["head_repository"] == "fork/repo"
summary = summary_path.read_text(encoding="utf-8")
assert "## PR review merge scheduler" in summary
- assert "| #7 | block | merge conflict: DIRTY; base=main, head=feature\\|x; changed files to inspect first:" in summary
+ assert "| #7 | `block` | ``merge conflict: DIRTY; base=main, head=feature|x; changed files to inspect first:" in summary
assert "do not retry update-branch until the conflict is repaired" in summary
assert "### Outdated review threads" in summary
assert "Would resolve 1 outdated review thread(s)" in summary
assert (
- "| #8 | update_branch | current-head OpenCode review approved; "
- "branch update requested with workflow GITHUB_TOKEN inside GitHub Actions as github-actions[bot] |"
+ "| #8 | `update_branch` | `current-head OpenCode review approved; "
+ "branch update requested with workflow GITHUB_TOKEN inside GitHub Actions as github-actions[bot]` |"
) in summary
- assert "| #12 | merge | current head is approved; direct merge requested with workflow GITHUB_TOKEN" in summary
+ assert "| #12 | `merge` | `current head is approved; direct merge requested with workflow GITHUB_TOKEN" in summary
assert "fresh same-head OpenCode review" in summary
assert "### Conflict repair" in summary
assert "When GitHub shows `Conflicting`" in summary
@@ -2678,7 +3088,7 @@ def test_print_summary_writes_github_step_summary(monkeypatch, tmp_path, capsys)
assert "Changed files to inspect first:" in summary
assert "- `scripts/ci/pr_review_merge_scheduler.py`" in summary
assert "- `tests/test_pr_review_merge_scheduler.py`" in summary
- assert "- `docs/has\\`tick.md`" in summary
+ assert "- ``docs/has`tick.md``" in summary
assert "git push --force-with-lease" in summary
assert "### Branch update requests" in summary
assert "Requested `update-branch` for PR #8 with `workflow GITHUB_TOKEN`" in summary
@@ -2707,6 +3117,34 @@ def test_write_actions_summary_is_noop_without_summary_path(monkeypatch):
)
+def test_summary_markdown_renderers_neutralize_untrusted_markup(monkeypatch, tmp_path):
+ """Workflow-controlled values cannot create headings, links, HTML, or table cells."""
+ summary_path = tmp_path / "summary.md"
+ monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary_path))
+ malicious = "safe|cell\r\n## injected [click](https://evil.test)
`tick`"
+
+ assert sched.markdown_code_span("docs/has`tick.md") == "``docs/has`tick.md``"
+ assert sched.markdown_code_span("`edge`") == "`` `edge` ``"
+ assert sched.markdown_code_span("line1\rline2") == "`line1 line2`"
+ rendered_cell = sched.markdown_cell(malicious)
+ assert "\n" not in rendered_cell
+ assert "safe|cell" in rendered_cell
+ assert "\\|" not in rendered_cell
+ assert rendered_cell.startswith("``") and rendered_cell.endswith("``")
+
+ sched.write_actions_summary(
+ [sched.Decision(1, malicious, malicious)],
+ counts={"wait": 1},
+ dry_run=True,
+ base_branch=malicious,
+ project_flow=malicious,
+ )
+ summary = summary_path.read_text(encoding="utf-8")
+ assert "\n## injected" not in summary
+ assert "\n
N
assert "github.event.workflow_run.pull_requests[0].number" in workflow
+def test_targeted_central_scheduler_binds_live_sibling_pr_metadata() -> None:
+ """A bounded central dispatch must validate and operate on the requested live PR."""
+ workflow = workflow_text("pr-review-merge-scheduler.yml")
+
+ assert "Bind targeted central dispatch to live pull request metadata" in workflow
+ assert (
+ "github.event.client_payload.target_repository != '' && "
+ "github.event.client_payload.pr_number > 0 && "
+ "format('target-{0}-pr-{1}'"
+ ) in workflow
+ assert "^ContextualWisdomLab/[A-Za-z0-9._-]+$" in workflow
+ assert 'pull_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}")"' in workflow
+ assert 'base_repository" != "$TARGET_REPOSITORY"' in workflow
+ assert '[[ "$base_branch" == "HEAD" ]]' in workflow
+ assert '[[ "$base_branch" == /* ]]' in workflow
+ assert '[[ "$base_branch" == *"//"* ]]' in workflow
+ assert "Live PR metadata did not provide exact 40-character base and head SHAs" in workflow
+ assert "SCHEDULER_ACTIONS_REPOSITORY:" in workflow
+ assert '--repo "$SCHEDULER_TARGET_REPOSITORY"' in workflow
+ assert '--base-branch "$SCHEDULER_TARGET_BASE_BRANCH"' in workflow
+ assert 'args+=(--pr-number "$SCHEDULER_TARGET_PR_NUMBER")' in workflow
+ assert 'args+=(--expected-head-sha "$SCHEDULER_TARGET_HEAD_SHA")' in workflow
+ assert 'case "$SCHEDULER_TARGET_BASE_BRANCH" in' in workflow
+
+
def test_org_queue_sweep_covers_target_repositories_on_a_heartbeat() -> None:
"""Guard the org-wide approved-PR fallback sweep contract.