From d53a1b040b3a590c42c63381780b4c117a540bf2 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Tue, 31 Mar 2026 23:26:56 +0100 Subject: [PATCH] fix: Surface server errors in CLI events and unregister commands CLI --json mode now outputs the full error dict and exits non-zero when the server returns {"error": ...}. Text mode prints to stderr. Covers both cmd_events (e.g., resume with nonexistent session) and cmd_unregister (e.g., session not found). Fixes #116 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/agent_event_bus/cli.py | 13 ++++++++++ tests/test_cli.py | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/agent_event_bus/cli.py b/src/agent_event_bus/cli.py index 334dcb4..412b1fb 100644 --- a/src/agent_event_bus/cli.py +++ b/src/agent_event_bus/cli.py @@ -159,6 +159,11 @@ def cmd_unregister(args): sys.exit(1) result = call_tool("unregister_session", arguments, url=args.url, debug=args.debug) + + if "error" in result: + print(f"Error: {result['error']}", file=sys.stderr) + sys.exit(1) + print(json.dumps(result, indent=2)) @@ -253,6 +258,14 @@ def cmd_events(args): "get_events", arguments, url=args.url, timeout_ms=args.timeout, debug=args.debug ) + # Check for server-side errors (e.g., session not found) + if "error" in result: + if args.json: + print(json.dumps(result)) + else: + print(f"Error: {result['error']}", file=sys.stderr) + sys.exit(1) + # Result is now a dict with "events" and "next_cursor" events = result.get("events", []) next_cursor = result.get("next_cursor") diff --git a/tests/test_cli.py b/tests/test_cli.py index 70eac20..5bb9845 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ """Tests for CLI wrapper.""" +import json from argparse import Namespace from unittest.mock import MagicMock, patch @@ -517,6 +518,58 @@ def test_events_resume_with_session_id_works(self, mock_call, capsys): assert call_args["session_id"] == "test-session" +class TestCmdEventsErrorSurfacing: + """Tests for CLI surfacing server-side errors in events command.""" + + @patch("agent_event_bus.cli.call_tool") + def test_events_error_json_mode(self, mock_call, capsys): + """Test that --json mode outputs server error as JSON and exits non-zero.""" + mock_call.return_value = {"error": "Session not found", "session_id": "bad-id"} + + args = make_events_args(json=True, session_id="bad-id", resume=True) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_events(args) + + assert exc_info.value.code == 1 + captured = capsys.readouterr() + output = json.loads(captured.out) + assert output["error"] == "Session not found" + assert output["session_id"] == "bad-id" + + @patch("agent_event_bus.cli.call_tool") + def test_events_error_text_mode(self, mock_call, capsys): + """Test that text mode prints error to stderr and exits non-zero.""" + mock_call.return_value = {"error": "Session not found", "session_id": "bad-id"} + + args = make_events_args(session_id="bad-id", resume=True) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_events(args) + + assert exc_info.value.code == 1 + captured = capsys.readouterr() + assert "Session not found" in captured.err + + +class TestCmdUnregisterErrorSurfacing: + """Tests for CLI surfacing server-side errors in unregister command.""" + + @patch("agent_event_bus.cli.call_tool") + def test_unregister_error_surfaces(self, mock_call, capsys): + """Test that unregister surfaces server errors to stderr.""" + mock_call.return_value = {"error": "Session not found", "session_id": "bad-id"} + + args = Namespace(session_id="bad-id", client_id=None, url=None, debug=False) + + with pytest.raises(SystemExit) as exc_info: + cli.cmd_unregister(args) + + assert exc_info.value.code == 1 + captured = capsys.readouterr() + assert "Session not found" in captured.err + + class TestCmdNotify: """Tests for notify command."""