Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

197 lines
7.9 KiB
Python

"""Tests for the Sentry issue-fix tool: gating, context, lifecycle, error_kind."""
from __future__ import annotations
import os
from unittest.mock import MagicMock, patch
import httpx
from integrations.coding_agent import CodingResult
from integrations.sentry import SentryConfig
from tools.cross_vendor.fix_sentry_issue import FixSentryIssueTool, fix_sentry_issue
from tools.cross_vendor.fix_sentry_issue.context import gather_issue_context
_CONFIG = "tools.cross_vendor.fix_sentry_issue.context.sentry_config_from_env"
_GET_ISSUE = "tools.cross_vendor.fix_sentry_issue.context.get_sentry_issue"
_VERIFY = "tools.cross_vendor.fix_sentry_issue.runner.verify_coding_agent"
_RUN = "tools.cross_vendor.fix_sentry_issue.runner.run_coding_task"
_ISSUE = {
"title": "TypeError: 'NoneType' object is not subscriptable",
"culprit": "app.handlers.process_event",
"level": "error",
"count": "42",
"metadata": {
"type": "TypeError",
"value": "'NoneType' object is not subscriptable",
"filename": "app/handlers.py",
"function": "process_event",
},
}
_URL = "https://acme.sentry.io/issues/12345/"
# --------------------------------------------------------------------------- #
# metadata + availability
# --------------------------------------------------------------------------- #
def test_metadata_is_mutating_on_investigation_surface() -> None:
t = fix_sentry_issue
assert t.name == "fix_sentry_issue"
assert t.source == "sentry"
assert t.side_effect_level == "mutating"
assert t.requires_approval is True
assert t.surfaces == ("investigation",)
assert t.input_schema["required"] == ["sentry_url"]
assert "error_kind" in t.outputs
assert t.metadata().name == "fix_sentry_issue"
def test_is_available_off_by_default_then_opt_in() -> None:
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("PI_ISSUE_FIX_ENABLED", None)
assert fix_sentry_issue.is_available({}) is False
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
assert fix_sentry_issue.is_available({}) is True
# --------------------------------------------------------------------------- #
# context building
# --------------------------------------------------------------------------- #
@patch(_GET_ISSUE, return_value=_ISSUE)
@patch(_CONFIG, return_value=MagicMock())
def test_gather_issue_context_builds_masked_task(
_mock_cfg: MagicMock, _mock_issue: MagicMock
) -> None:
ctx = gather_issue_context(_URL)
assert ctx.issue_id == "12345"
assert "Issue:" in ctx.task
assert "TypeError" in ctx.task
assert "app/handlers.py" in ctx.task
@patch(_GET_ISSUE, return_value=_ISSUE)
@patch(_CONFIG, return_value=SentryConfig(organization_slug="env-org", auth_token="tok"))
def test_gather_uses_org_from_url_over_config(_mock_cfg: MagicMock, mock_issue: MagicMock) -> None:
# URL org is "acme"; config org is "env-org" — the URL must win.
gather_issue_context("https://acme.sentry.io/issues/12345/")
used_config = mock_issue.call_args.kwargs["config"]
assert used_config.organization_slug == "acme"
# --------------------------------------------------------------------------- #
# run() lifecycle + error_kind
# --------------------------------------------------------------------------- #
def test_run_disabled() -> None:
with patch.dict(os.environ, {}, clear=False):
os.environ.pop("PI_ISSUE_FIX_ENABLED", None)
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["success"] is False
assert out["error_kind"] == "disabled"
def test_run_invalid_url() -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url="https://github.com/foo/bar/issues/1")
assert out["error_kind"] == "invalid_input"
@patch(_CONFIG, return_value=None)
def test_run_sentry_unavailable(_mock_cfg: MagicMock) -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "sentry_unavailable"
@patch(_GET_ISSUE, return_value={})
@patch(_CONFIG, return_value=MagicMock())
def test_run_issue_not_found_empty_response(_mock_cfg: MagicMock, _mock_issue: MagicMock) -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "issue_not_found"
def _http_status_error(status: int) -> httpx.HTTPStatusError:
req = httpx.Request("GET", "https://acme.sentry.io/api/0/issues/12345/")
return httpx.HTTPStatusError("err", request=req, response=httpx.Response(status, request=req))
@patch(_GET_ISSUE, side_effect=_http_status_error(404))
@patch(_CONFIG, return_value=MagicMock())
def test_run_issue_not_found_on_http_404(_mock_cfg: MagicMock, _mock_issue: MagicMock) -> None:
# The common real failure: valid URL, issue id doesn't exist -> Sentry 404.
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "issue_not_found"
@patch(_GET_ISSUE, side_effect=_http_status_error(403))
@patch(_CONFIG, return_value=MagicMock())
def test_run_sentry_auth_error_on_http_403(_mock_cfg: MagicMock, _mock_issue: MagicMock) -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "sentry_unavailable"
@patch(_GET_ISSUE, side_effect=httpx.ConnectError("boom"))
@patch(_CONFIG, return_value=MagicMock())
def test_run_sentry_network_error(_mock_cfg: MagicMock, _mock_issue: MagicMock) -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "sentry_unavailable"
@patch(_VERIFY, return_value=(False, "pi not installed"))
@patch(_GET_ISSUE, return_value=_ISSUE)
@patch(_CONFIG, return_value=MagicMock())
def test_run_cli_unavailable(
_mock_cfg: MagicMock, _mock_issue: MagicMock, _mock_verify: MagicMock
) -> None:
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["error_kind"] == "cli_unavailable"
@patch(_RUN)
@patch(_VERIFY, return_value=(True, "ok"))
@patch(_GET_ISSUE, return_value=_ISSUE)
@patch(_CONFIG, return_value=MagicMock())
def test_run_success(
_mock_cfg: MagicMock, _mock_issue: MagicMock, _mock_verify: MagicMock, mock_run: MagicMock
) -> None:
mock_run.return_value = CodingResult(
success=True,
summary="Guarded the None case in process_event.",
changed_files=["app/handlers.py"],
diff="diff --git a/app/handlers.py b/app/handlers.py\n",
returncode=0,
)
with patch.dict(os.environ, {"PI_ISSUE_FIX_ENABLED": "1"}, clear=False):
out = fix_sentry_issue.run(sentry_url=_URL)
assert out["success"] is True
assert out["error_kind"] is None
assert out["issue_id"] == "12345"
assert out["changed_files"] == ["app/handlers.py"]
assert "diff --git" in out["diff"]
# the synthesized Sentry task was passed to the Pi client
assert "Sentry issue" in mock_run.call_args.args[0]
# --------------------------------------------------------------------------- #
# registry discovery
# --------------------------------------------------------------------------- #
def test_registry_discovers_fix_sentry_issue_on_investigation_surface() -> None:
from tools.registry import get_registered_tool_map
investigation = get_registered_tool_map("investigation")
chat = get_registered_tool_map("chat")
assert "fix_sentry_issue" in investigation
assert "fix_sentry_issue" not in chat
rt = investigation["fix_sentry_issue"]
assert rt.requires_approval is True
assert rt.side_effect_level == "mutating"
def test_tool_subclass_constructs() -> None:
assert isinstance(fix_sentry_issue, FixSentryIssueTool)