Spaces:
Running
Running
| """Tests for session event logging (tool calls, UI events persistence).""" | |
| import json | |
| import asyncio | |
| from unittest.mock import patch, MagicMock, AsyncMock | |
| import pytest | |
| import reachy_mini_conversation_app.stream_api as stream_api | |
| def mock_conversation_logger(): | |
| """Provide a mock ConversationLogger and wire it into stream_api.""" | |
| mock_logger = MagicMock() | |
| mock_logger.log_turn = MagicMock(return_value=1) | |
| mock_logger.get_session = MagicMock(return_value=[]) | |
| original_logger = stream_api._conversation_logger | |
| original_session_id = stream_api._current_session_id | |
| stream_api._conversation_logger = mock_logger | |
| stream_api._current_session_id = "test-session-abc" | |
| yield mock_logger | |
| stream_api._conversation_logger = original_logger | |
| stream_api._current_session_id = original_session_id | |
| def mock_broadcast(): | |
| """Mock the broadcast to avoid needing real WebSocket connections.""" | |
| with patch.object(stream_api.manager, "broadcast", new_callable=AsyncMock) as m: | |
| yield m | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tool event persistence | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def test_emit_tool_start_persists_to_conversation_log( | |
| mock_conversation_logger, mock_broadcast | |
| ): | |
| """emit_tool_start should log a 'tool' turn to the conversation log.""" | |
| await stream_api.emit_tool_start( | |
| name="log_entry", | |
| args={"type": "headache", "intensity": 7}, | |
| event_id="evt-001", | |
| ) | |
| mock_conversation_logger.log_turn.assert_called_once() | |
| call_kwargs = mock_conversation_logger.log_turn.call_args | |
| assert call_kwargs[1]["role"] == "tool" | |
| assert "log_entry" in call_kwargs[1]["content"] | |
| assert "headache" in call_kwargs[1]["content"] | |
| assert call_kwargs[1]["session_id"] == "test-session-abc" | |
| async def test_emit_tool_result_persists_to_conversation_log( | |
| mock_conversation_logger, mock_broadcast | |
| ): | |
| """emit_tool_result should log a 'tool_result' turn.""" | |
| await stream_api.emit_tool_result( | |
| name="log_entry", | |
| result={"status": "ok", "entry_id": 42}, | |
| event_id="evt-001", | |
| status="completed", | |
| ) | |
| mock_conversation_logger.log_turn.assert_called_once() | |
| call_kwargs = mock_conversation_logger.log_turn.call_args | |
| assert call_kwargs[1]["role"] == "tool_result" | |
| assert "log_entry" in call_kwargs[1]["content"] | |
| assert call_kwargs[1]["metadata"]["status"] == "completed" | |
| async def test_emit_tool_result_truncates_large_results( | |
| mock_conversation_logger, mock_broadcast | |
| ): | |
| """Large tool results should be truncated to avoid DB bloat.""" | |
| big_result = {"data": "x" * 2000} | |
| await stream_api.emit_tool_result( | |
| name="big_tool", | |
| result=big_result, | |
| event_id="evt-002", | |
| ) | |
| call_kwargs = mock_conversation_logger.log_turn.call_args | |
| content = call_kwargs[1]["content"] | |
| assert len(content) < 1200 # truncated from 2000+ | |
| assert "truncated" in content | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # UI event persistence | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def test_emit_ui_component_persists_to_conversation_log( | |
| mock_conversation_logger, mock_broadcast | |
| ): | |
| """emit_ui_component should log a 'ui' turn.""" | |
| await stream_api.emit_ui_component( | |
| component_name="HeadacheLog", | |
| props={"intensity": 7, "location": "frontal"}, | |
| message_id="msg-001", | |
| ) | |
| mock_conversation_logger.log_turn.assert_called_once() | |
| call_kwargs = mock_conversation_logger.log_turn.call_args | |
| assert call_kwargs[1]["role"] == "ui" | |
| assert "HeadacheLog" in call_kwargs[1]["content"] | |
| assert "frontal" in call_kwargs[1]["content"] | |
| async def test_emit_ui_component_truncates_large_props( | |
| mock_conversation_logger, mock_broadcast | |
| ): | |
| """Large UI props should be truncated.""" | |
| big_props = {"data": "y" * 1000} | |
| await stream_api.emit_ui_component( | |
| component_name="BigComponent", | |
| props=big_props, | |
| ) | |
| call_kwargs = mock_conversation_logger.log_turn.call_args | |
| content = call_kwargs[1]["content"] | |
| assert len(content) < 700 # truncated from 1000+ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # No-op when logger is not initialized | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def test_emit_tool_start_no_crash_without_logger(mock_broadcast): | |
| """Should not crash when conversation logger is not initialized.""" | |
| original = stream_api._conversation_logger | |
| stream_api._conversation_logger = None | |
| try: | |
| await stream_api.emit_tool_start( | |
| name="test_tool", | |
| args={"a": 1}, | |
| event_id="evt-999", | |
| ) | |
| # Should complete without error | |
| finally: | |
| stream_api._conversation_logger = original | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Session-log endpoint | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def test_session_log_endpoint_returns_turns(mock_conversation_logger): | |
| """GET /session-log should return turns from ConversationLogger.""" | |
| mock_conversation_logger.get_session.return_value = [ | |
| {"turn_index": 0, "role": "user", "content": "Hello"}, | |
| {"turn_index": 1, "role": "tool", "content": "log_entry({})"}, | |
| {"turn_index": 2, "role": "ui", "content": "HeadacheLog: {}"}, | |
| ] | |
| result = await stream_api.session_log(session_id="test-session-abc") | |
| assert result["session_id"] == "test-session-abc" | |
| assert result["turn_count"] == 3 | |
| assert result["turns"][1]["role"] == "tool" | |
| async def test_session_log_endpoint_no_logger(): | |
| """Should return error when logger not initialized.""" | |
| original = stream_api._conversation_logger | |
| stream_api._conversation_logger = None | |
| try: | |
| result = await stream_api.session_log() | |
| assert "error" in result | |
| finally: | |
| stream_api._conversation_logger = original | |