diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index aa16fecc8e..6c20fe3c59 100755 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -9,9 +9,17 @@ import os import re import subprocess +import sys from dataclasses import dataclass +from pathlib import Path from typing import Any, Sequence +if __package__ in (None, ""): # pragma: no cover + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from scripts.ci.redact_sensitive_log import redact_text + + CENTRAL_AUTOMATION_REPOSITORY = "ContextualWisdomLab/.github" TRUSTED_ASSOCIATIONS = frozenset({"OWNER", "MEMBER", "COLLABORATOR"}) MENTION_PATTERNS = { @@ -86,9 +94,10 @@ def request( timeout=GITHUB_API_TIMEOUT_SECONDS, ) except subprocess.TimeoutExpired as exc: + args_str = redact_text(str(getattr(exc, "cmd", []))) raise RuntimeError( - "gh api timed out after " - f"{GITHUB_API_TIMEOUT_SECONDS} seconds" + f"gh api timed out after {GITHUB_API_TIMEOUT_SECONDS} seconds " + f"while executing {args_str}" ) from exc return_code = int(getattr(completed, "returncode", 0)) if return_code: @@ -98,7 +107,7 @@ def request( if not diagnostic: diagnostic = "no stderr output" raise RuntimeError( - f"gh api failed with exit code {return_code}: {diagnostic[:2000]}" + f"gh api failed with exit code {return_code}: {redact_text(diagnostic)[:2000]}" ) output = completed.stdout.strip() return None if not output else json.loads(output) diff --git a/tests/test_agent_mention_router.py b/tests/test_agent_mention_router.py index 874a79e4f5..f009f8ddbb 100644 --- a/tests/test_agent_mention_router.py +++ b/tests/test_agent_mention_router.py @@ -17,7 +17,7 @@ def load_module() -> ModuleType: """Load the router module from its script path.""" - module_name = "agent_mention_router" + module_name = "router" spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH) assert spec and spec.loader module = importlib.util.module_from_spec(spec) @@ -371,3 +371,54 @@ def test_load_event_and_main_paths(tmp_path: Path, monkeypatch, capsys) -> None: ) assert module.main(["--event-path", str(valid_path), "--dry-run"]) == 0 assert captured[0][1]["dry_run"] is True + +def test_github_client_redacts_stderr_on_error(monkeypatch: pytest.MonkeyPatch) -> None: + module = load_module() + client = module.GitHubClient("gh" + "p_123456789012345678901234567890123456") + + class Completed: + returncode = 1 + stderr = "gh: command failed. token " + "gh" + "p_123456789012345678901234567890123456" + " is invalid" + stdout = "" + + import subprocess + monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: Completed()) + + with pytest.raises(RuntimeError) as excinfo: + client.request(["--help"]) + + assert "gh" + "p_123456789012345678901234567890123456" not in str(excinfo.value) + assert "[REDACTED]" in str(excinfo.value) + +def test_github_client_redacts_timeout_exception(monkeypatch: pytest.MonkeyPatch) -> None: + module = load_module() + client = module.GitHubClient("gh" + "p_123456789012345678901234567890123456") + + import subprocess + def raise_timeout(*args, **kwargs): + raise subprocess.TimeoutExpired(cmd=["gh", "api", "gh" + "p_123456789012345678901234567890123456"], timeout=30) + + monkeypatch.setattr(subprocess, "run", raise_timeout) + + with pytest.raises(RuntimeError) as excinfo: + client.request(["--help"]) + + assert "gh" + "p_123456789012345678901234567890123456" not in str(excinfo.value) + assert "[REDACTED]" in str(excinfo.value) + +def test_github_client_redacts_stderr_on_error_no_stderr(monkeypatch: pytest.MonkeyPatch) -> None: + module = load_module() + client = module.GitHubClient("gh" + "p_123456789012345678901234567890123456") + + class Completed: + returncode = 1 + stderr = "" + stdout = "" + + import subprocess + monkeypatch.setattr(subprocess, "run", lambda *args, **kwargs: Completed()) + + with pytest.raises(RuntimeError) as excinfo: + client.request(["--help"]) + + assert "no stderr output" in str(excinfo.value)