# #region Test.AgentChat.GradioApp [C:3] [TYPE Module] [SEMANTICS test,agent,gradio,chat] # @BRIEF Tests for agent/app.py — agent_handler, _handle_resume, _extract_user_id, _save_conversation, create_chat_interface, health. # @RELATION BINDS_TO -> [AgentChat.GradioApp] from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) import json from unittest.mock import AsyncMock, MagicMock, patch import pytest def _make_async_iter(items): """Create an async iterator from a list.""" class AsyncIter: def __init__(self, items): self._iter = iter(items) def __aiter__(self): return self async def __anext__(self): try: return next(self._iter) except StopIteration: raise StopAsyncIteration return AsyncIter(items) def _make_agent_mock(stream_events=None, raise_on_call=False): """Create a properly mocked agent for async iteration.""" agent = MagicMock() if raise_on_call: agent.astream_events = MagicMock(side_effect=stream_events if isinstance(stream_events, list) else stream_events) elif stream_events is not None: agent.astream_events = MagicMock(return_value=_make_async_iter(stream_events)) else: agent.astream_events = MagicMock(return_value=_make_async_iter([])) return agent @pytest.fixture(autouse=True) def clear_locks(): """Clear _user_locks before each test.""" from src.agent.app import _user_locks _user_locks.clear() @pytest.fixture def mock_request(): req = MagicMock() req.headers = {"authorization": ""} return req # #region test_extract_user_id [C:2] [TYPE Function] # @BRIEF Test _extract_user_id for various JWT payloads. class TestExtractUserId: def test_extracts_sub(self): from src.agent.app import _extract_user_id with patch("src.agent.app.jwt.decode", return_value={"sub": "user-1"}): assert _extract_user_id("fake-jwt") == "user-1" def test_extracts_user_id_fallback(self): from src.agent.app import _extract_user_id with patch("src.agent.app.jwt.decode", return_value={"user_id": "user-2"}): assert _extract_user_id("fake-jwt") == "user-2" def test_returns_unknown_on_exception(self): from src.agent.app import _extract_user_id with patch("src.agent.app.jwt.decode", side_effect=Exception("bad token")): assert _extract_user_id("bad") == "unknown" def test_returns_unknown_on_empty(self): from src.agent.app import _extract_user_id assert _extract_user_id("") == "unknown" # #endregion test_extract_user_id # #region test_agent_handler [C:2] [TYPE Function] # @BRIEF Test agent_handler for various scenarios. class TestAgentHandler: @pytest.mark.asyncio async def test_handles_concurrent_send(self, mock_request): from src.agent.app import agent_handler, _user_locks _user_locks["anon_default"] = True results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["code"] == "CONCURRENT_SEND" @pytest.mark.asyncio async def test_handles_file_too_large(self, mock_request): from src.agent.app import agent_handler, MAX_FILE_SIZE_BYTES message = {"text": "analyze", "files": ["fake_path"]} with patch("os.path.exists", return_value=True), \ patch("os.path.getsize", return_value=MAX_FILE_SIZE_BYTES + 1): results = [r async for r in agent_handler(message, [], mock_request, None, None)] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["code"] == "FILE_TOO_LARGE" @pytest.mark.asyncio async def test_handles_hitl_confirm(self, mock_request): from src.agent.app import agent_handler with patch("src.agent.app._handle_resume") as mock_resume: mock_resume.return_value = _make_async_iter([ json.dumps({"content": "confirmed", "metadata": {"type": "confirm_resolved"}}) ]) with patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("confirm", [], mock_request, "conv-1", "confirm")] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["type"] == "confirm_resolved" @pytest.mark.asyncio async def test_handles_hitl_deny(self, mock_request): from src.agent.app import agent_handler with patch("src.agent.app._handle_resume") as mock_resume: mock_resume.return_value = _make_async_iter([ json.dumps({"content": "denied", "metadata": {"type": "confirm_resolved", "result": "denied"}}) ]) with patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("deny", [], mock_request, "conv-1", "deny")] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["result"] == "denied" @pytest.mark.asyncio async def test_normal_send_yields_tokens(self, mock_request): from src.agent.app import agent_handler mock_chunk = MagicMock() mock_chunk.content = "Hello" mock_event_stream = [ {"event": "on_chat_model_stream", "data": {"chunk": mock_chunk}}, {"event": "on_chain_end", "interrupt": {}}, ] agent = _make_agent_mock(mock_event_stream) with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) > 0 token_data = json.loads(results[0]) assert token_data["metadata"]["type"] == "stream_token" @pytest.mark.asyncio async def test_tool_events_yielded(self, mock_request): from src.agent.app import agent_handler mock_event_stream = [ {"event": "on_tool_start", "name": "test_tool", "data": {"input": {}}}, {"event": "on_tool_end", "name": "test_tool", "data": {"output": "done"}}, ] agent = _make_agent_mock(mock_event_stream) with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()), \ patch("src.agent.app.log_tool_event", AsyncMock()): results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) == 2 assert json.loads(results[0])["metadata"]["type"] == "tool_start" assert json.loads(results[1])["metadata"]["type"] == "tool_end" @pytest.mark.asyncio async def test_tool_error_event(self, mock_request): from src.agent.app import agent_handler mock_event_stream = [ {"event": "on_tool_error", "name": "test_tool", "data": {"error": "something failed"}}, ] agent = _make_agent_mock(mock_event_stream) with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()), \ patch("src.agent.app.log_tool_event", AsyncMock()): results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) == 1 assert json.loads(results[0])["metadata"]["type"] == "tool_error" @pytest.mark.asyncio async def test_output_parser_exception_retry(self, mock_request): from src.agent.app import agent_handler from langchain_core.exceptions import OutputParserException mock_stream_after = _make_async_iter([ {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="retried")}} ]) call_count = [0] def mock_astream(*args, **kwargs): call_count[0] += 1 if call_count[0] == 1: raise OutputParserException("bad output") return mock_stream_after agent = MagicMock() agent.astream_events = mock_astream with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) > 0 @pytest.mark.asyncio async def test_output_parser_exception_final_failure(self, mock_request): from src.agent.app import agent_handler from langchain_core.exceptions import OutputParserException call_count = [0] def mock_astream(*args, **kwargs): call_count[0] += 1 raise OutputParserException(f"bad {call_count[0]}") agent = MagicMock() agent.astream_events = mock_astream with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("hello", [], mock_request, None, None)] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["code"] == "LLM_MALFORMED_OUTPUT" @pytest.mark.asyncio async def test_non_llm_error_saves_conversation(self, mock_request): from src.agent.app import agent_handler def mock_astream(*args, **kwargs): raise RuntimeError("API connection error") agent = MagicMock() agent.astream_events = mock_astream save_mock = AsyncMock() with patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", save_mock): with pytest.raises(RuntimeError, match="API connection error"): await agent_handler("hello", [], mock_request, None, None).__anext__() save_mock.assert_called_once() @pytest.mark.asyncio async def test_invalid_jwt_passes_gracefully(self): from src.agent.app import agent_handler import jwt as pyjwt req = MagicMock() req.headers = {"authorization": "Bearer invalid.jwt"} mock_event_stream = [ {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="ok")}} ] agent = _make_agent_mock(mock_event_stream) with patch("src.agent.app.jwt.decode", side_effect=pyjwt.InvalidTokenError("invalid")), \ patch("src.agent.app.create_agent", return_value=agent), \ patch("src.agent.app.get_all_tools", return_value=[]), \ patch("src.agent.app._save_conversation", AsyncMock()): results = [r async for r in agent_handler("hi", [], req, None, None)] assert len(results) == 1 # #endregion test_agent_handler # #region test_handle_resume [C:2] [TYPE Function] # @BRIEF Test _handle_resume confirm and deny paths. class TestHandleResume: @pytest.mark.asyncio async def test_confirm(self): from src.agent.app import _handle_resume mock_agent = MagicMock() with patch("src.agent.app.create_agent", return_value=mock_agent), \ patch("src.agent.app.get_all_tools", return_value=[]): results = [r async for r in _handle_resume("conv-1", "confirm")] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["result"] == "confirmed" @pytest.mark.asyncio async def test_deny(self): from src.agent.app import _handle_resume mock_agent = MagicMock() with patch("src.agent.app.create_agent", return_value=mock_agent), \ patch("src.agent.app.get_all_tools", return_value=[]): results = [r async for r in _handle_resume("conv-1", "deny")] assert len(results) == 1 data = json.loads(results[0]) assert data["metadata"]["result"] == "denied" # #endregion test_handle_resume # #region test_save_conversation [C:2] [TYPE Function] # @BRIEF Test _save_conversation with various scenarios. class TestSaveConversation: @pytest.mark.asyncio async def test_save_success(self): from src.agent.app import _save_conversation with patch("src.agent.app.httpx.AsyncClient") as mock_client: mock_client.return_value.__aenter__.return_value.post = AsyncMock() await _save_conversation("conv-1", "test message", "user-1") mock_client.return_value.__aenter__.return_value.post.assert_called_once() @pytest.mark.asyncio async def test_save_with_service_jwt(self): from src.agent.app import _save_conversation with patch("src.agent.app.httpx.AsyncClient") as mock_client, \ patch("src.agent.app.os.getenv", return_value="service-token"): mock_client.return_value.__aenter__.return_value.post = AsyncMock() await _save_conversation("conv-1", "hello", "admin") mock_client.return_value.__aenter__.return_value.post.assert_called_once() @pytest.mark.asyncio async def test_save_failure_logged(self): from src.agent.app import _save_conversation with patch("src.agent.app.httpx.AsyncClient") as mock_client: mock_client.return_value.__aenter__.return_value.post.side_effect = Exception("network err") # Should not raise await _save_conversation("conv-1", "msg", "u1") @pytest.mark.asyncio async def test_save_empty_title(self): from src.agent.app import _save_conversation with patch("src.agent.app.httpx.AsyncClient") as mock_client: client_instance = AsyncMock() mock_client.return_value.__aenter__.return_value = client_instance await _save_conversation("conv-1", " ", "user-1") call_kwargs = client_instance.post.call_args[1] assert call_kwargs["json"]["title"] == "Agent conversation" # #endregion test_save_conversation # #region test_create_chat_interface [C:2] [TYPE Function] # @BRIEF Test create_chat_interface returns a gr.ChatInterface. class TestCreateChatInterface: def test_returns_chat_interface(self): from src.agent.app import create_chat_interface with patch("src.agent.app.gr.ChatInterface") as mock_ci: result = create_chat_interface() assert result is mock_ci.return_value # #endregion test_create_chat_interface # #region test_health [C:2] [TYPE Function] # @BRIEF Test health endpoint returns status ok. class TestHealth: @pytest.mark.asyncio async def test_health_returns_ok(self): from src.agent.app import health result = await health() assert result["status"] == "ok" # #endregion test_health # #region test_file_upload_parsing [C:2] [TYPE Function] # @BRIEF Test file upload branch — parse_upload called for valid small files. class TestFileUploadParsing: @pytest.mark.asyncio async def test_handler_parses_uploaded_file(self, mock_request, tmp_path): """Valid small file triggers parse_upload and continues to stream.""" from src.agent.app import agent_handler test_file = tmp_path / "test.txt" test_file.write_text("upload content for analysis") message = {"text": "analyze", "files": [str(test_file)]} with patch('src.agent.app.create_agent') as mock_create, \ patch('src.agent.app.get_all_tools', return_value=[]), \ patch('src.agent.app._save_conversation', AsyncMock()), \ patch('src.agent.app.log_tool_event', AsyncMock()): agent = _make_agent_mock([ {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="result")}} ]) mock_create.return_value = agent results = [r async for r in agent_handler(message, [], mock_request, None, None)] assert len(results) > 0 data = json.loads(results[0]) assert data["metadata"]["type"] == "stream_token" # #endregion test_file_upload_parsing # #region test_app_main_block [C:2] [TYPE Function] # @BRIEF Test if __name__ == '__main__' block in app.py. class TestAppMainBlock: def test_app_main_block(self): """if __name__ == '__main__' creates ChatInterface and launches.""" import importlib.util from pathlib import Path app_path = Path(__file__).parent.parent.parent / "src" / "agent" / "app.py" spec = importlib.util.spec_from_file_location("__main__", str(app_path)) mock_demo = MagicMock() with patch('gradio.ChatInterface') as mock_ci: mock_ci.return_value = mock_demo module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) mock_demo.launch.assert_called_once() # #endregion test_app_main_block # #endregion Test.AgentChat.GradioApp