diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt
index 9e705850b5..eb83beda17 100644
--- a/requirements-strix-ci-hashes.txt
+++ b/requirements-strix-ci-hashes.txt
@@ -140,9 +140,9 @@ annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
-anyio==4.14.0 \
- --hash=sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89 \
- --hash=sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9
+anyio==4.14.2 \
+ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
+ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via
# google-genai
# gql
diff --git a/scripts/ci/actions_queue_health.py b/scripts/ci/actions_queue_health.py
index bb73698551..e01145b8e0 100644
--- a/scripts/ci/actions_queue_health.py
+++ b/scripts/ci/actions_queue_health.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Queue-health CLI with stable identity and audit-provenance guarantees.
-The shared collector implementation lives in ``actions_queue_health_core.py``.
-This entrypoint owns the consistency boundary that binds active-run evidence to
+Shared parsing and reporting primitives live in ``actions_queue_health_core.py``.
+This entrypoint owns collection and the consistency boundary that binds active-run evidence to
a stable pull-request view, carries stable workflow identity, and exports the
exact timestamp used for queue-age calculations.
"""
@@ -163,16 +163,7 @@ def collect_snapshot(
),
)
for workflow_run in workflow_runs:
- workflow_run_id = workflow_run.get("id")
- if (
- isinstance(workflow_run_id, bool)
- or not isinstance(workflow_run_id, int)
- or workflow_run_id <= 0
- ):
- raise QueueHealthError(
- "workflow run id must be a positive integer"
- )
- active_snapshot[workflow_run_id] = workflow_run
+ active_snapshot[workflow_run["id"]] = workflow_run
active_snapshots.append(active_snapshot)
first_snapshot, second_snapshot = active_snapshots
@@ -242,16 +233,7 @@ def collect_snapshot(
TERMINAL_DIAGNOSTIC_STATUSES
):
continue
- workflow_run_id = workflow_run.get("id")
- if (
- isinstance(workflow_run_id, bool)
- or not isinstance(workflow_run_id, int)
- or workflow_run_id <= 0
- ):
- raise QueueHealthError(
- "workflow run id must be a positive integer"
- )
- terminal_diagnostic_snapshot[workflow_run_id] = workflow_run
+ terminal_diagnostic_snapshot[workflow_run["id"]] = workflow_run
for terminal_status in TARGET_TERMINAL_DIAGNOSTIC_STATUSES:
target_workflow_runs = _list_payload(
diff --git a/scripts/ci/actions_queue_health_core.py b/scripts/ci/actions_queue_health_core.py
index db3e5570ba..50cb3e0793 100644
--- a/scripts/ci/actions_queue_health_core.py
+++ b/scripts/ci/actions_queue_health_core.py
@@ -112,6 +112,13 @@ def _list_payload(
declared_total_counts.append(payload["total_count"])
if not isinstance(values, list) or not all(isinstance(value, dict) for value in values):
raise QueueHealthError(f"GitHub response field {key!r} must be an array of objects")
+ if key == "workflow_runs" and any(
+ isinstance(value.get("id"), bool)
+ or not isinstance(value.get("id"), int)
+ or value["id"] <= 0
+ for value in values
+ ):
+ raise QueueHealthError("workflow run id must be a positive integer")
if isinstance(payload, dict) and PAGINATED_PAGES_KEY in payload:
record_identities: list[tuple[str, int]] = []
for value in values:
@@ -360,133 +367,6 @@ def _normalise_run(repository: str, run: dict[str, Any], jobs: list[dict[str, An
}
-def collect_snapshot(
- repositories: Sequence[str],
- *,
- runner: Runner = subprocess.run,
- generated_at: str | None = None,
-) -> dict[str, Any]:
- """Collect bounded queued/in-progress run and job data using read-only API calls."""
- validated = sorted({_repository_name(repository) for repository in repositories})
- if len(validated) != len(repositories):
- raise QueueHealthError("collection repository list contains duplicates")
- collected_repositories: list[dict[str, Any]] = []
- collection_errors: list[dict[str, str]] = []
- for repository in validated:
- try:
- metadata = github_json(f"repos/{repository}", runner=runner)
- if not isinstance(metadata, dict):
- raise QueueHealthError(f"repository metadata for {repository} is not an object")
- pulls_endpoint = f"repos/{repository}/pulls?state=open&per_page={MAX_API_PAGE_SIZE}"
- pull_requests = _list_payload(
- github_json(pulls_endpoint, paginate=True, runner=runner),
- "pulls",
- max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES,
- )
- normalized_pull_requests = sorted(
- (_normalise_pull_request(item) for item in pull_requests),
- key=lambda item: item["number"],
- )
- except IncompletePullRequestIdentity:
- time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS)
- try:
- retry_pull_requests = _list_payload(
- github_json(pulls_endpoint, paginate=True, runner=runner),
- "pulls",
- max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES,
- )
- normalized_pull_requests = sorted(
- (_normalise_pull_request(item) for item in retry_pull_requests),
- key=lambda item: item["number"],
- )
- except QueueHealthError as retry_exc:
- collection_errors.append(
- {
- "repository": repository,
- "error": f"pull-request identity validation failed: {retry_exc}",
- }
- )
- continue
- except QueueHealthError as exc:
- collection_errors.append({"repository": repository, "error": str(exc)})
- continue
- pull_requests_by_number = {item["number"]: item for item in normalized_pull_requests}
- runs_by_id: dict[int, dict[str, Any]] = {}
- try:
- active_statuses = ("in_progress", "pending", "queued", "requested", "waiting")
- snapshots: list[dict[int, dict[str, Any]]] = []
- for status_order in (active_statuses, tuple(reversed(active_statuses))):
- snapshot: dict[int, dict[str, Any]] = {}
- for status in status_order:
- runs = _list_payload(
- github_json(
- f"repos/{repository}/actions/runs?status={status}"
- f"&per_page={WORKFLOW_RUN_PAGE_SIZE}",
- paginate=True,
- max_pages=ACTIVE_RUN_MAX_API_PAGES,
- runner=runner,
- ),
- "workflow_runs",
- max_items=WORKFLOW_RUN_PAGE_SIZE * ACTIVE_RUN_MAX_API_PAGES,
- )
- for run in runs:
- run_id = run.get("id")
- if isinstance(run_id, bool) or not isinstance(run_id, int) or run_id <= 0:
- raise QueueHealthError("workflow run id must be a positive integer")
- snapshot[run_id] = run
- snapshots.append(snapshot)
- first_snapshot, second_snapshot = snapshots
- first_states = {
- run_id: str(run.get("status") or "").upper()
- for run_id, run in first_snapshot.items()
- }
- second_states = {
- run_id: str(run.get("status") or "").upper()
- for run_id, run in second_snapshot.items()
- }
- if first_states != second_states:
- raise QueueHealthError("active workflow run snapshot changed during collection")
- for run_id, run in second_snapshot.items():
- run_id = run.get("id")
- candidate = _normalise_run(repository, run, [])
- identity, _ = _run_identity(candidate, pull_requests_by_number)
- if identity != "current_head" or candidate["status"] not in {
- "IN_PROGRESS",
- "WAITING",
- }:
- runs_by_id[run_id] = candidate
- continue
- jobs_payload = github_json(
- f"repos/{repository}/actions/runs/{run_id}/jobs?per_page={MAX_API_PAGE_SIZE}",
- paginate=True,
- runner=runner,
- )
- jobs = _list_payload(
- jobs_payload,
- "jobs",
- max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES,
- )
- runs_by_id[run_id] = _normalise_run(repository, run, jobs)
- except QueueHealthError as exc:
- collection_errors.append({"repository": repository, "error": str(exc)})
- continue
- collected_repositories.append(
- {
- "full_name": repository,
- "default_branch": str(metadata.get("default_branch") or ""),
- "pull_requests": normalized_pull_requests,
- "runs": sorted(runs_by_id.values(), key=lambda item: item["id"]),
- }
- )
- timestamp = generated_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
- parse_timestamp(timestamp)
- return {
- "generated_at": timestamp,
- "repositories": collected_repositories,
- "collection_errors": collection_errors,
- }
-
-
def load_snapshot(path: Path) -> dict[str, Any]:
"""Load a JSON snapshot for offline, deterministic report generation."""
try:
@@ -822,34 +702,3 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.add_argument("--queue-age-slo-seconds", type=int, default=DEFAULT_QUEUE_AGE_SLO_SECONDS)
parser.add_argument("--now", help="Explicit timezone-aware evaluation time for deterministic reports")
return parser.parse_args(argv)
-
-
-def main(argv: Sequence[str] | None = None, *, stderr: TextIO = sys.stderr) -> int:
- """Collect or load a snapshot, write reports, and return a stable CLI status."""
- args = parse_args(argv)
- try:
- snapshot = load_snapshot(args.snapshot) if args.snapshot else collect_snapshot(load_allowlist(args.allowlist))
- now = parse_timestamp(args.now) if args.now else datetime.now(timezone.utc)
- report = build_report(
- snapshot,
- now=now,
- queue_age_slo_seconds=args.queue_age_slo_seconds,
- )
- write_reports(report, args.output_json, args.output_html)
- except (OSError, QueueHealthError, ValueError) as exc:
- print(f"ERROR: queue-health report failed: {exc}", file=stderr)
- return 2
- breaches = report["summary"]["unassigned_slo_breached_count"]
- if breaches:
- print(f"::warning::Actions queue-health found {breaches} unassigned current-head SLO breach(es).")
- print(
- "QUEUE_HEALTH_RESULT="
- f"observed={report['summary']['observed_job_count']} "
- f"pending={report['summary']['pending_job_count']} "
- f"slo_breaches={breaches}"
- )
- return 0
-
-
-if __name__ == "__main__": # pragma: no cover - exercised through the CLI tests.
- raise SystemExit(main())
diff --git a/tests/test_actions_queue_health_cancelled_before_runner.py b/tests/test_actions_queue_health_cancelled_before_runner.py
index 1827275f68..850f3bd866 100644
--- a/tests/test_actions_queue_health_cancelled_before_runner.py
+++ b/tests/test_actions_queue_health_cancelled_before_runner.py
@@ -17,7 +17,7 @@
SPEC.loader.exec_module(queue_health)
-def test_collect_snapshot_classifies_cancelled_job_before_runner_assignment() -> None:
+def test_collect_snapshot_classifies_cancelled_job_before_runner_assignment(monkeypatch) -> None:
"""A cancelled current-head job with no runner or steps stays explicit evidence."""
repository_name = "owner/repo"
pull_request = {
@@ -144,6 +144,9 @@ def runner(args: list[str], **_: object) -> CompletedProcess[str]:
"cancelled_before_runner_assignment"
)
assert report["summary"]["cancelled_before_runner_assignment_count"] == 1
+ actions = list(report["summary"]["external_actions"])
+ monkeypatch.setattr(queue_health, "_CORE_BUILD_REPORT", lambda *_args, **_kwargs: report)
+ assert queue_health.build_report(snapshot)["summary"]["external_actions"] == actions
def test_collect_snapshot_retains_cancelled_pull_request_target_current_head() -> None:
diff --git a/tests/test_actions_queue_health_post_evidence_retry.py b/tests/test_actions_queue_health_post_evidence_retry.py
index bc266fa96f..ae1c619cd0 100644
--- a/tests/test_actions_queue_health_post_evidence_retry.py
+++ b/tests/test_actions_queue_health_post_evidence_retry.py
@@ -5,6 +5,8 @@
from pathlib import Path
from subprocess import CompletedProcess
+import pytest
+
ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py"
@@ -82,3 +84,88 @@ def test_post_evidence_identity_read_fails_closed_after_retry_remains_incomplete
assert len(snapshot["collection_errors"]) == 1
assert snapshot["collection_errors"][0]["repository"] == "owner/repo"
assert "pull-request identity validation failed" in snapshot["collection_errors"][0]["error"]
+
+def _run(run_id: int, workflow_id: int) -> dict:
+ """Return one linked run for terminal-filter boundary tests."""
+ return {
+ "id": run_id,
+ "workflow_id": workflow_id,
+ "name": "required-check",
+ "event": "pull_request",
+ "status": "queued",
+ "head_sha": "head",
+ "pull_requests": [{"number": 1, "head": {"sha": "head"}}],
+ }
+
+
+@pytest.mark.parametrize(
+ ("active_runs", "completed_runs", "target_runs", "expected_error"),
+ [
+ ([{"id": 0, "status": "queued"}], [], [], "workflow run id"),
+ ([], [{**_run(8, 501), "status": "completed", "conclusion": "failure", "id": 0}], [], "workflow run id"),
+ ([], [{**_run(8, 501), "status": "completed", "conclusion": "success"}], [], None),
+ ([], [], [{**_run(8, 501), "status": "completed", "conclusion": "cancelled", "pull_requests": []}], None),
+ ],
+)
+def test_collector_rejects_invalid_run_ids_and_ignores_unrelated_terminal_runs(
+ active_runs: list[dict], completed_runs: list[dict], target_runs: list[dict], expected_error: str | None,
+) -> None:
+ """Bad identities fail closed; unrelated terminal runs do not become current-head evidence."""
+ def runner(args: list[str], **_kwargs: object) -> CompletedProcess[str]:
+ path = args[-1]
+ if path == "repos/owner/repo":
+ payload: object = {"default_branch": "main"}
+ elif path == "repos/owner/repo/pulls?state=open&per_page=100":
+ payload = [_pull()]
+ elif "status=completed&head_sha=" in path:
+ payload = completed_runs
+ elif "status=cancelled&event=pull_request_target" in path:
+ payload = target_runs
+ elif "status=queued" in path:
+ payload = active_runs
+ elif "/actions/runs?status=" in path:
+ payload = []
+ else:
+ raise AssertionError(f"unexpected endpoint: {path}")
+ return CompletedProcess(args, 0, json.dumps(payload), "")
+
+ snapshot = queue_health.collect_snapshot(
+ ["owner/repo"], runner=runner, generated_at="2026-09-02T00:00:00Z"
+ )
+ if expected_error:
+ assert snapshot["repositories"] == []
+ assert expected_error in snapshot["collection_errors"][0]["error"]
+ else:
+ assert snapshot["collection_errors"] == []
+ assert snapshot["repositories"][0]["runs"] == []
+
+
+def test_final_pull_identity_retry_failure_is_bounded(monkeypatch: pytest.MonkeyPatch) -> None:
+ """A repeatedly incomplete final PR view cannot certify current-head evidence."""
+ monkeypatch.setattr(queue_health.time, "sleep", lambda _seconds: None)
+ pull_reads = 0
+
+ def runner(args: list[str], **_kwargs: object) -> CompletedProcess[str]:
+ nonlocal pull_reads
+ path = args[-1]
+ if path == "repos/owner/repo":
+ payload: object = {"default_branch": "main"}
+ elif path == "repos/owner/repo/pulls?state=open&per_page=100":
+ pull_reads += 1
+ payload = [_pull()] if pull_reads == 1 else [{**_pull(), "head": {"sha": ""}}]
+ elif "/actions/runs?status=" in path:
+ payload = []
+ else:
+ raise AssertionError(f"unexpected endpoint: {path}")
+ return CompletedProcess(args, 0, json.dumps(payload), "")
+
+ snapshot = queue_health.collect_snapshot(["owner/repo"], runner=runner)
+ assert pull_reads == 3
+ assert snapshot["repositories"] == []
+ assert "pull-request identity validation failed" in snapshot["collection_errors"][0]["error"]
+
+
+def test_core_run_normalization_rejects_non_object() -> None:
+ """A malformed workflow-run payload cannot be classified as a real run."""
+ with pytest.raises(queue_health.QueueHealthError, match="workflow run entry must be an object"):
+ queue_health._CORE_NORMALISE_RUN("owner/repo", None, [])
diff --git a/tests/test_actions_queue_health_snapshot_consistency.py b/tests/test_actions_queue_health_snapshot_consistency.py
index b9711a09dd..8e939e2fc9 100644
--- a/tests/test_actions_queue_health_snapshot_consistency.py
+++ b/tests/test_actions_queue_health_snapshot_consistency.py
@@ -192,4 +192,5 @@ def test_invalid_present_workflow_id_fails_closed() -> None:
def test_queue_health_workflow_does_not_grant_unused_pull_request_permission() -> None:
"""The scheduler token keeps only permissions used outside the cross-repository token."""
workflow = (ROOT / ".github/workflows/actions-queue-health.yml").read_text(encoding="utf-8")
- assert "pull-requests: read" not in workflow
+ assert "\n pull-requests: read\n" not in workflow
+ assert "\n pull-requests: read\n" not in workflow
diff --git a/tests/test_actions_queue_health_terminal_preexecution.py b/tests/test_actions_queue_health_terminal_preexecution.py
index 05237ba340..c86c435e30 100644
--- a/tests/test_actions_queue_health_terminal_preexecution.py
+++ b/tests/test_actions_queue_health_terminal_preexecution.py
@@ -57,7 +57,7 @@ def _terminal_failure_job() -> dict:
}
-def test_terminal_preexecution_failure_survives_collection_and_is_not_product_failure() -> None:
+def test_terminal_preexecution_failure_survives_collection_and_is_not_product_failure(monkeypatch) -> None:
"""Keep failed zero-step jobs as explicit non-passing admission evidence."""
failed_run = _terminal_failure_run()
failed_job = _terminal_failure_job()
@@ -106,3 +106,6 @@ def runner(args: list[str], **_: object) -> CompletedProcess[str]:
assert row["recommended_action"] == "inspect_actions_control_plane_without_leaf_bypass"
assert report["summary"]["terminal_pre_execution_failure_count"] == 1
assert report["summary"]["terminal_job_count"] == 1
+ actions = list(report["summary"]["external_actions"])
+ monkeypatch.setattr(queue_health, "_CORE_BUILD_REPORT", lambda *_args, **_kwargs: report)
+ assert queue_health.build_report(snapshot)["summary"]["external_actions"] == actions
diff --git a/tests/test_noema_document_review_context.py b/tests/test_noema_document_review_context.py
index e6ec2e6d70..ffed1c8961 100644
--- a/tests/test_noema_document_review_context.py
+++ b/tests/test_noema_document_review_context.py
@@ -176,6 +176,13 @@ def test_forbidden_docx_entities_are_explicitly_rejected():
document.extract_review_document("docs/entity.docx", _docx_entity_bytes())
+def test_invalid_github_base64_content_fails_closed(monkeypatch):
+ """Malformed GitHub file data must not reach the document reader."""
+ monkeypatch.setattr(noema, "run", lambda _args, stdin=None: "not/base64!")
+ with pytest.raises(RuntimeError, match="malformed base64"):
+ noema.fetch_file_content_at_ref("owner/repo", "docs/review.docx", "head")
+
+
def test_hwp_reader_contract_is_local_and_fail_closed(monkeypatch):
"""HWP/HWPX use the configured local adapter and reject failed readers."""
monkeypatch.setenv(document.HWP_READER_ENV, "/trusted/hwp-mcp-source")
diff --git a/tests/test_noema_review_document_boundaries.py b/tests/test_noema_review_document_boundaries.py
new file mode 100644
index 0000000000..5680252b3f
--- /dev/null
+++ b/tests/test_noema_review_document_boundaries.py
@@ -0,0 +1,128 @@
+"""Exercise document-reader failure boundaries used by protected review."""
+
+from __future__ import annotations
+
+import io
+import runpy
+import sys
+import zipfile
+from pathlib import Path
+from subprocess import CompletedProcess
+
+import pytest
+
+from scripts.ci import noema_review_document as document
+
+
+def _docx(xml: str | None, *, extra_entries: int = 0) -> bytes:
+ """Build a small DOCX archive with optional missing document XML."""
+ output = io.BytesIO()
+ with zipfile.ZipFile(output, "w") as archive:
+ if xml is not None:
+ archive.writestr("word/document.xml", xml)
+ for index in range(extra_entries):
+ archive.writestr(f"extra-{index}", "x")
+ return output.getvalue()
+
+
+def _body(content: str) -> str:
+ """Wrap Word body content in the namespace expected by the reader."""
+ return (
+ f''
+ f"{content}"
+ )
+
+
+def test_document_input_limits_and_unsupported_formats(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Reject oversized, unsupported, and unconfigured reader inputs."""
+ monkeypatch.setattr(document, "MAX_DOCUMENT_BYTES", 2)
+ with pytest.raises(document.DocumentReadError, match="8 MiB"):
+ document.extract_review_document("a.docx", b"long")
+ with pytest.raises(document.DocumentReadError, match="unsupported"):
+ document.extract_review_document("a.pdf", b"ok")
+ monkeypatch.delenv(document.HWP_READER_ENV, raising=False)
+ with pytest.raises(document.DocumentReadError, match="not configured"):
+ document.extract_review_document("a.hwp", b"ok")
+
+
+def test_docx_archive_and_xml_boundaries(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Reject partial archives and XML without visible document content."""
+ valid = _docx(_body("ok"))
+ monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_ENTRIES", 0)
+ with pytest.raises(document.DocumentReadError, match="too many entries"):
+ document.extract_review_document("a.docx", valid)
+ monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_ENTRIES", 2048)
+ monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES", 1)
+ with pytest.raises(document.DocumentReadError, match="unpacked size"):
+ document.extract_review_document("a.docx", valid)
+ monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES", 64 * 1024 * 1024)
+ cases = (
+ (_docx(None, extra_entries=1), "no word/document.xml"),
+ (_docx("'), "no document body"),
+ (_docx(_body("")), "no readable text"),
+ )
+ for payload, message in cases:
+ with pytest.raises(document.DocumentReadError, match=message):
+ document.extract_review_document("a.docx", payload)
+
+
+def test_docx_visible_controls_and_uneven_table() -> None:
+ """Keep tabs, line breaks, and uneven table cells in reviewer text."""
+ xml = _body(
+ "ABC"
+ "X|Y"
+ "Z"
+ ""
+ ""
+ )
+ text = document.extract_review_document("a.docx", _docx(xml))
+ assert "A\tB\nC" in text
+ assert "X\\|Y" in text
+ assert "| Z | |" in text
+ assert "### Table 1 (2 rows x 2 columns)" in text
+ assert "Table 2" not in text
+
+
+def test_hwp_reader_rejects_process_and_output_failures(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Keep parser failures and untrusted output out of the review prompt."""
+ monkeypatch.setenv(document.HWP_READER_ENV, "/reviewed/source")
+
+ def unavailable(*_args: object, **_kwargs: object) -> None:
+ raise OSError("private process detail")
+
+ monkeypatch.setattr(document.subprocess, "run", unavailable)
+ with pytest.raises(document.DocumentReadError, match="could not start"):
+ document.extract_review_document("a.hwpx", b"data")
+
+ for stdout, message in ((b"abcd", "bounded output"), (b"\xff", "non-UTF-8"), (b" ", "empty text")):
+ monkeypatch.setattr(document, "MAX_DOCUMENT_TEXT_BYTES", 3)
+ completed = CompletedProcess(["node"], 0, stdout, b"")
+ monkeypatch.setattr(
+ document.subprocess,
+ "run",
+ lambda *_args, **_kwargs: completed,
+ )
+ with pytest.raises(document.DocumentReadError, match=message):
+ document.extract_review_document("a.hwpx", b"data")
+
+
+def test_document_text_truncation_and_cli(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ """Bound UTF-8 output and preserve a useful local CLI failure exit."""
+ monkeypatch.setattr(document, "MAX_DOCUMENT_TEXT_BYTES", 4)
+ assert document._bounded_text("ééé").startswith("éé\n[document text truncated;")
+ assert document._bounded_text("ok") == "ok"
+
+ path = tmp_path / "review.docx"
+ path.write_bytes(_docx(_body("ok")))
+ monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path)])
+ assert document._main() == 0
+ assert "ok" in capsys.readouterr().out
+ monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path.with_name("missing.docx"))])
+ assert document._main() == 1
+ assert capsys.readouterr().err
+
+ monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path)])
+ with pytest.raises(SystemExit) as exit_status:
+ runpy.run_path(str(Path(document.__file__)), run_name="__main__")
+ assert exit_status.value.code == 0
diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py
index 4b8715d361..b8afdfcd92 100644
--- a/tests/test_pr_review_merge_scheduler.py
+++ b/tests/test_pr_review_merge_scheduler.py
@@ -2363,19 +2363,19 @@ def fake_active_workflow_runs(repo, statuses, *, event=None, created=None, head_
assert created == ">=2026-09-17T11:50:00Z"
return [
{
- "path": ".github/workflows/opencode-review-coalesce-tick.yml",
+ "path": ".github/workflows/other.yml",
"conclusion": "success",
- "updated_at": "2026-09-17T11:55:00Z",
+ "updated_at": "2026-09-17T11:59:00Z",
},
{
"path": ".github/workflows/opencode-review-coalesce-tick.yml",
"conclusion": "success",
- "updated_at": "2026-09-17T11:40:00Z",
+ "updated_at": "2026-09-17T11:55:00Z",
},
{
- "path": ".github/workflows/other.yml",
+ "path": ".github/workflows/opencode-review-coalesce-tick.yml",
"conclusion": "success",
- "updated_at": "2026-09-17T11:59:00Z",
+ "updated_at": "2026-09-17T11:40:00Z",
},
]