diff --git a/gateway/run.py b/gateway/run.py index 617a38418e..51c9c529fe 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -7645,13 +7645,14 @@ class GatewayRunner: from hermes_cli.debug import ( _capture_dump, collect_debug_report, upload_to_pastebin, _schedule_auto_delete, - _GATEWAY_PRIVACY_NOTICE, + _GATEWAY_PRIVACY_NOTICE, _best_effort_sweep_expired_pastes, ) loop = asyncio.get_running_loop() # Run blocking I/O (dump capture, log reads, uploads) in a thread. def _collect_and_upload(): + _best_effort_sweep_expired_pastes() dump_text = _capture_dump() report = collect_debug_report(log_lines=200, dump_text=dump_text) diff --git a/hermes_cli/debug.py b/hermes_cli/debug.py index 348de59a9d..2e622b6331 100644 --- a/hermes_cli/debug.py +++ b/hermes_cli/debug.py @@ -148,6 +148,14 @@ def _sweep_expired_pastes(now: Optional[float] = None) -> tuple[int, int]: return (deleted, len(remaining)) +def _best_effort_sweep_expired_pastes() -> None: + """Attempt pending-paste cleanup without letting /debug fail offline.""" + try: + _sweep_expired_pastes() + except Exception: + pass + + # --------------------------------------------------------------------------- # Privacy / delete helpers # --------------------------------------------------------------------------- @@ -518,6 +526,8 @@ def collect_debug_report( def run_debug_share(args): """Collect debug report + full logs, upload each, print URLs.""" + _best_effort_sweep_expired_pastes() + log_lines = getattr(args, "lines", 200) expiry = getattr(args, "expire", 7) local_only = getattr(args, "local", False) diff --git a/tests/gateway/test_debug_command.py b/tests/gateway/test_debug_command.py new file mode 100644 index 0000000000..48cda30140 --- /dev/null +++ b/tests/gateway/test_debug_command.py @@ -0,0 +1,60 @@ +"""Tests for the gateway /debug command.""" + +from unittest.mock import patch + +import pytest + +from gateway.config import GatewayConfig, Platform +from gateway.platforms.base import MessageEvent +from gateway.session import SessionSource + + +def _make_event(text="/debug", platform=Platform.TELEGRAM, + user_id="12345", chat_id="67890"): + source = SessionSource( + platform=platform, + user_id=user_id, + chat_id=chat_id, + user_name="testuser", + ) + return MessageEvent(text=text, source=source) + + +def _make_runner(): + from gateway.run import GatewayRunner + + runner = object.__new__(GatewayRunner) + runner.config = GatewayConfig() + runner.adapters = {} + return runner + + +class TestHandleDebugCommand: + @pytest.mark.asyncio + async def test_debug_sweeps_expired_pastes_before_upload(self): + runner = _make_runner() + event = _make_event() + + with patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)) as mock_sweep, \ + patch("hermes_cli.debug._capture_dump", return_value="dump"), \ + patch("hermes_cli.debug.collect_debug_report", return_value="report"), \ + patch("hermes_cli.debug.upload_to_pastebin", return_value="https://paste.rs/report"), \ + patch("hermes_cli.debug._schedule_auto_delete"): + result = await runner._handle_debug_command(event) + + mock_sweep.assert_called_once() + assert "https://paste.rs/report" in result + + @pytest.mark.asyncio + async def test_debug_survives_sweep_failure(self): + runner = _make_runner() + event = _make_event() + + with patch("hermes_cli.debug._sweep_expired_pastes", side_effect=RuntimeError("offline")), \ + patch("hermes_cli.debug._capture_dump", return_value="dump"), \ + patch("hermes_cli.debug.collect_debug_report", return_value="report"), \ + patch("hermes_cli.debug.upload_to_pastebin", return_value="https://paste.rs/report"), \ + patch("hermes_cli.debug._schedule_auto_delete"): + result = await runner._handle_debug_command(event) + + assert "https://paste.rs/report" in result diff --git a/tests/hermes_cli/test_debug.py b/tests/hermes_cli/test_debug.py index a72b2057cd..e89b9e5acd 100644 --- a/tests/hermes_cli/test_debug.py +++ b/tests/hermes_cli/test_debug.py @@ -283,6 +283,44 @@ class TestCollectDebugReport: class TestRunDebugShare: """Test the run_debug_share CLI handler.""" + def test_share_sweeps_expired_pastes(self, hermes_home, capsys): + """Slash-command path should sweep old pending deletes before uploading.""" + from hermes_cli.debug import run_debug_share + + args = MagicMock() + args.lines = 50 + args.expire = 7 + args.local = False + + with patch("hermes_cli.dump.run_dump"), \ + patch("hermes_cli.debug._sweep_expired_pastes", return_value=(0, 0)) as mock_sweep, \ + patch("hermes_cli.debug.upload_to_pastebin", + return_value="https://paste.rs/test"): + run_debug_share(args) + + mock_sweep.assert_called_once() + assert "Debug report uploaded" in capsys.readouterr().out + + def test_share_survives_sweep_failure(self, hermes_home, capsys): + """Expired-paste cleanup is best-effort and must not block sharing.""" + from hermes_cli.debug import run_debug_share + + args = MagicMock() + args.lines = 50 + args.expire = 7 + args.local = False + + with patch("hermes_cli.dump.run_dump"), \ + patch( + "hermes_cli.debug._sweep_expired_pastes", + side_effect=RuntimeError("offline"), + ), \ + patch("hermes_cli.debug.upload_to_pastebin", + return_value="https://paste.rs/test"): + run_debug_share(args) + + assert "https://paste.rs/test" in capsys.readouterr().out + def test_local_flag_prints_full_logs(self, hermes_home, capsys): """--local prints the report plus full log contents.""" from hermes_cli.debug import run_debug_share