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
189 lines
6.7 KiB
Python
189 lines
6.7 KiB
Python
"""Tests for the interactive-shell 'fix Sentry issue' action tool (intake routing).
|
|
|
|
Covers gating (only offered when opted in), dispatch to ``fix_sentry_issue`` with
|
|
the right args, result rendering, and the action-context wrapper. The action LLM's
|
|
intent selection is a live concern exercised via ReplDriver, not here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from rich.console import Console
|
|
|
|
import tools.interactive_shell.actions.sentry_fix as sentry_fix
|
|
from core.agent_harness.tools.tool_context import (
|
|
ACTION_TOOL_CONTEXT_RESOURCE_KEY,
|
|
ActionToolContext,
|
|
)
|
|
from core.types import AgentToolContext
|
|
|
|
_TOOL_RUN = "tools.interactive_shell.actions.sentry_fix.fix_sentry_issue.run"
|
|
_URL = "https://acme.sentry.io/issues/12345/"
|
|
|
|
|
|
def _ctx() -> tuple[ActionToolContext, io.StringIO]:
|
|
buf = io.StringIO()
|
|
console = Console(file=buf, force_terminal=False, highlight=False, width=200)
|
|
return ActionToolContext(session=MagicMock(), console=console), buf
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# execute_sentry_fix_tool
|
|
# --------------------------------------------------------------------------- #
|
|
def test_empty_url_is_not_handled() -> None:
|
|
ctx, _ = _ctx()
|
|
assert sentry_fix.execute_sentry_fix_tool({"sentry_url": " "}, ctx) is False
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_dispatches_with_open_pr_and_renders_pr(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": True,
|
|
"error_kind": None,
|
|
"issue_id": "12345",
|
|
"summary": "Guarded the None case.",
|
|
"changed_files": ["app/handlers.py"],
|
|
"pr_url": "https://github.com/acme/app/pull/9",
|
|
"branch_name": "opensre/sentry-fix-12345-abc",
|
|
}
|
|
ctx, buf = _ctx()
|
|
handled = sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL, "open_pr": True}, ctx)
|
|
|
|
assert handled is True
|
|
mock_run.assert_called_once_with(sentry_url=_URL, open_pr=True)
|
|
out = buf.getvalue()
|
|
assert "opening a pull request" in out
|
|
assert "https://github.com/acme/app/pull/9" in out
|
|
assert "app/handlers.py" in out
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_dispatches_diff_only_by_default(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": True,
|
|
"issue_id": "12345",
|
|
"summary": "s",
|
|
"changed_files": ["a.py"],
|
|
"pr_url": None,
|
|
"branch_name": None,
|
|
}
|
|
ctx, buf = _ctx()
|
|
sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL}, ctx)
|
|
|
|
mock_run.assert_called_once_with(sentry_url=_URL, open_pr=False)
|
|
assert "no PR requested" in buf.getvalue()
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_renders_error(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": False,
|
|
"error_kind": "push_failed",
|
|
"error": "remote rejected",
|
|
"changed_files": ["a.py"],
|
|
}
|
|
ctx, buf = _ctx()
|
|
sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL, "open_pr": True}, ctx)
|
|
|
|
out = buf.getvalue()
|
|
assert "Could not fix" in out
|
|
assert "push_failed" in out
|
|
assert "still in your working tree" in out
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_renders_push_failure_says_committed_needs_push(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": False,
|
|
"error_kind": "push_failed",
|
|
"error": "remote rejected",
|
|
"changed_files": ["a.py"],
|
|
"branch_name": "opensre/sentry-fix-12345-abc",
|
|
}
|
|
ctx, buf = _ctx()
|
|
sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL, "open_pr": True}, ctx)
|
|
|
|
out = buf.getvalue()
|
|
assert "opensre/sentry-fix-12345-abc" in out
|
|
assert "committed on branch" in out
|
|
assert "push it and open the PR manually" in out
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_renders_commit_failure_says_not_committed(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": False,
|
|
"error_kind": "commit_failed",
|
|
"error": "git commit failed",
|
|
"changed_files": ["a.py"],
|
|
"branch_name": "opensre/sentry-fix-12345-abc",
|
|
}
|
|
ctx, buf = _ctx()
|
|
sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL, "open_pr": True}, ctx)
|
|
|
|
out = buf.getvalue()
|
|
# Nothing was committed, so the user must commit first — not just push.
|
|
assert "not committed" in out
|
|
assert "commit them, push, and open the PR manually" in out
|
|
|
|
|
|
@patch(_TOOL_RUN)
|
|
def test_renders_pr_failed_recovery_says_already_pushed(mock_run: MagicMock) -> None:
|
|
mock_run.return_value = {
|
|
"success": False,
|
|
"error_kind": "pr_failed",
|
|
"error": "422 validation failed",
|
|
"changed_files": ["a.py"],
|
|
"branch_name": "opensre/sentry-fix-12345-abc",
|
|
}
|
|
ctx, buf = _ctx()
|
|
sentry_fix.execute_sentry_fix_tool({"sentry_url": _URL, "open_pr": True}, ctx)
|
|
|
|
out = buf.getvalue()
|
|
# The branch is already pushed, so guidance is "open the PR", not "push it".
|
|
assert "pushed to branch" in out
|
|
assert "open the PR manually" in out
|
|
assert "push and open" not in out
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# run_sentry_fix (action-context wrapper)
|
|
# --------------------------------------------------------------------------- #
|
|
@patch(_TOOL_RUN, return_value={"success": True, "issue_id": "1", "pr_url": None})
|
|
def test_run_sentry_fix_uses_action_context(mock_run: MagicMock) -> None:
|
|
action_ctx, _ = _ctx()
|
|
agent_ctx = AgentToolContext(
|
|
resolved_integrations={},
|
|
resources={ACTION_TOOL_CONTEXT_RESOURCE_KEY: action_ctx},
|
|
)
|
|
result = sentry_fix.run_sentry_fix(sentry_url=_URL, context=agent_ctx, open_pr=False)
|
|
assert result["ok"] is True
|
|
mock_run.assert_called_once_with(sentry_url=_URL, open_pr=False)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# gating + registry
|
|
# --------------------------------------------------------------------------- #
|
|
def test_is_available_follows_opt_in(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
tool = sentry_fix.fix_sentry_issue_start_tool
|
|
monkeypatch.delenv("PI_ISSUE_FIX_ENABLED", raising=False)
|
|
assert tool.is_available({}) is False
|
|
monkeypatch.setenv("PI_ISSUE_FIX_ENABLED", "1")
|
|
assert tool.is_available({}) is True
|
|
|
|
|
|
def test_registered_as_action_tool(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from tools.registry import get_registered_tool_map
|
|
|
|
monkeypatch.setenv("PI_ISSUE_FIX_ENABLED", "1")
|
|
action = get_registered_tool_map("action")
|
|
assert "fix_sentry_issue_start" in action
|
|
tool = action["fix_sentry_issue_start"]
|
|
assert tool.side_effect_level == "mutating"
|
|
assert tool.input_schema["required"] == ["sentry_url"]
|
|
# Not offered on the investigation/chat surfaces (that's the underlying tool).
|
|
assert "fix_sentry_issue_start" not in get_registered_tool_map("investigation")
|