test(backend): add 55+ test files to push coverage to 98%
Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
This commit is contained in:
354
backend/tests/test_agent/test_app.py
Normal file
354
backend/tests/test_agent/test_app.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# #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
|
||||
# #endregion Test.AgentChat.GradioApp
|
||||
110
backend/tests/test_agent/test_langgraph_setup.py
Normal file
110
backend/tests/test_agent/test_langgraph_setup.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# #region Test.AgentChat.LangGraph.Setup [C:3] [TYPE Module] [SEMANTICS test,agent,langgraph,setup]
|
||||
# @BRIEF Tests for agent/langgraph_setup.py — configure_from_api, create_agent.
|
||||
# @RELATION BINDS_TO -> [AgentChat.LangGraph.Setup]
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
|
||||
# #region test_configure_from_api [C:2] [TYPE Function]
|
||||
# @BRIEF Test configure_from_api updates global config.
|
||||
class TestConfigureFromApi:
|
||||
def test_sets_llm_config(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None # Reset
|
||||
ls.configure_from_api({"configured": True, "api_key": "sk-test", "default_model": "gpt-4o"})
|
||||
assert ls._llm_config is not None
|
||||
assert ls._llm_config["configured"] is True
|
||||
# Reset for other tests
|
||||
ls.configure_from_api(None)
|
||||
ls._llm_config = None
|
||||
|
||||
def test_overwrites_previous_config(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None # Reset
|
||||
ls.configure_from_api({"configured": True, "api_key": "sk-1"})
|
||||
ls.configure_from_api({"configured": False})
|
||||
assert ls._llm_config["configured"] is False
|
||||
ls._llm_config = None
|
||||
# #endregion test_configure_from_api
|
||||
|
||||
|
||||
# #region test_create_agent [C:2] [TYPE Function]
|
||||
# @BRIEF Test create_agent with various LLM config states.
|
||||
class TestCreateAgent:
|
||||
def test_creates_agent_with_api_config(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None # Reset
|
||||
ls.configure_from_api({
|
||||
"configured": True,
|
||||
"api_key": "sk-api-config",
|
||||
"base_url": "https://custom.api.com/v1",
|
||||
"default_model": "gpt-4o-mini",
|
||||
})
|
||||
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
|
||||
patch("src.agent.langgraph_setup.create_react_agent") as mock_create:
|
||||
mock_create.return_value = MagicMock()
|
||||
result = ls.create_agent([MagicMock()])
|
||||
assert result is mock_create.return_value
|
||||
call_kwargs = mock_llm.call_args[1]
|
||||
assert call_kwargs["api_key"] == "sk-api-config"
|
||||
assert call_kwargs["base_url"] == "https://custom.api.com/v1"
|
||||
assert call_kwargs["model"] == "gpt-4o-mini"
|
||||
ls._llm_config = None
|
||||
|
||||
def test_creates_agent_with_env_fallback(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None # Reset
|
||||
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
|
||||
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
|
||||
patch("src.agent.langgraph_setup.os.getenv") as mock_getenv:
|
||||
mock_getenv.side_effect = lambda key, default=None: {
|
||||
"LLM_API_KEY": "sk-env-key",
|
||||
"LLM_BASE_URL": "https://env.api.com",
|
||||
"LLM_MODEL": "gpt-4",
|
||||
}.get(key, default)
|
||||
mock_create.return_value = MagicMock()
|
||||
result = ls.create_agent([])
|
||||
assert result is mock_create.return_value
|
||||
call_kwargs = mock_llm.call_args[1]
|
||||
assert call_kwargs["api_key"] == "sk-env-key"
|
||||
assert call_kwargs["model"] == "gpt-4"
|
||||
ls._llm_config = None
|
||||
|
||||
def test_creates_agent_with_partial_api_config(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None
|
||||
ls.configure_from_api({
|
||||
"configured": True,
|
||||
"api_key": "sk-key-only",
|
||||
})
|
||||
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
|
||||
patch("src.agent.langgraph_setup.create_react_agent") as mock_create:
|
||||
mock_create.return_value = MagicMock()
|
||||
result = ls.create_agent([])
|
||||
assert result is mock_create.return_value
|
||||
call_kwargs = mock_llm.call_args[1]
|
||||
assert call_kwargs["api_key"] == "sk-key-only"
|
||||
assert call_kwargs["base_url"] == "https://api.openai.com/v1"
|
||||
assert call_kwargs["model"] == "gpt-4o-mini"
|
||||
ls._llm_config = None
|
||||
|
||||
def test_uses_inmemory_saver(self):
|
||||
import src.agent.langgraph_setup as ls
|
||||
ls._llm_config = None
|
||||
with patch("src.agent.langgraph_setup.ChatOpenAI") as mock_llm, \
|
||||
patch("src.agent.langgraph_setup.create_react_agent") as mock_create, \
|
||||
patch("src.agent.langgraph_setup.os.getenv", return_value=None):
|
||||
mock_create.return_value = MagicMock()
|
||||
ls.create_agent([])
|
||||
call_kwargs = mock_create.call_args[1]
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
assert isinstance(call_kwargs["checkpointer"], InMemorySaver)
|
||||
ls._llm_config = None
|
||||
# #endregion test_create_agent
|
||||
# #endregion Test.AgentChat.LangGraph.Setup
|
||||
88
backend/tests/test_agent/test_middleware.py
Normal file
88
backend/tests/test_agent/test_middleware.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# #region Test.AgentChat.Middleware [C:3] [TYPE Module] [SEMANTICS test,agent,middleware,audit]
|
||||
# @BRIEF Tests for agent/middleware.py — log_tool_event.
|
||||
# @RELATION BINDS_TO -> [AgentChat.Middleware]
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
|
||||
# #region test_log_tool_event [C:2] [TYPE Function]
|
||||
# @BRIEF Test log_tool_event for various event types.
|
||||
class TestLogToolEvent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_tool_start(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {
|
||||
"event": "on_tool_start",
|
||||
"name": "migrate",
|
||||
"data": {"input": {"dashboard_id": "42"}},
|
||||
}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value="user-token"):
|
||||
await log_tool_event(event, "conv-1")
|
||||
# No exception = success
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_tool_end(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {
|
||||
"event": "on_tool_end",
|
||||
"name": "migrate",
|
||||
"data": {"output": "success"},
|
||||
}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value="user-token"):
|
||||
await log_tool_event(event, "conv-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_tool_error(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {
|
||||
"event": "on_tool_error",
|
||||
"name": "migrate",
|
||||
"data": {"error": "Connection failed"},
|
||||
}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value="user-token"):
|
||||
await log_tool_event(event, "conv-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_logs_without_user_jwt(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {
|
||||
"event": "on_tool_start",
|
||||
"name": "test_tool",
|
||||
"data": {"input": {}},
|
||||
}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value=None):
|
||||
await log_tool_event(event, "conv-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_missing_data_key(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {"event": "on_tool_start", "name": "test_tool"}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value=None):
|
||||
await log_tool_event(event, "conv-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_unknown_event_kind(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
event = {"event": "on_custom_event", "name": "custom"}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value=None):
|
||||
await log_tool_event(event, "conv-1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncates_long_input(self):
|
||||
from src.agent.middleware import log_tool_event
|
||||
long_input = "x" * 1000
|
||||
event = {
|
||||
"event": "on_tool_start",
|
||||
"name": "big_tool",
|
||||
"data": {"input": long_input},
|
||||
}
|
||||
with patch("src.agent.middleware.get_user_jwt", return_value="token"):
|
||||
await log_tool_event(event, "conv-1")
|
||||
# #endregion test_log_tool_event
|
||||
# #endregion Test.AgentChat.Middleware
|
||||
128
backend/tests/test_agent/test_run.py
Normal file
128
backend/tests/test_agent/test_run.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# #region Test.AgentChat.Run [C:3] [TYPE Module] [SEMANTICS test,agent,run,entrypoint]
|
||||
# @BRIEF Tests for agent/run.py — _find_free_port and _fetch_llm_config.
|
||||
# @RELATION BINDS_TO -> [AgentChat.Run]
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||
|
||||
import socket
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
|
||||
# #region test_find_free_port [C:2] [TYPE Function]
|
||||
# @BRIEF Test _find_free_port for port scanning behavior.
|
||||
class TestFindFreePort:
|
||||
def test_returns_free_port(self):
|
||||
from src.agent.run import _find_free_port
|
||||
with patch("socket.socket") as mock_socket:
|
||||
mock_instance = MagicMock()
|
||||
mock_socket.return_value.__enter__.return_value = mock_instance
|
||||
result = _find_free_port(8000, 10)
|
||||
assert result == 8000
|
||||
mock_instance.bind.assert_called_once_with(("", 8000))
|
||||
|
||||
def test_skips_busy_ports(self):
|
||||
from src.agent.run import _find_free_port
|
||||
with patch("socket.socket") as mock_socket:
|
||||
mock_instance = MagicMock()
|
||||
mock_socket.return_value.__enter__.return_value = mock_instance
|
||||
# Ports 8000-8002 busy, 8003 free
|
||||
mock_instance.bind.side_effect = [
|
||||
OSError("Address in use"), # 8000
|
||||
OSError("Address in use"), # 8001
|
||||
OSError("Address in use"), # 8002
|
||||
None, # 8003 — success
|
||||
]
|
||||
result = _find_free_port(8000, 10)
|
||||
assert result == 8003
|
||||
assert mock_instance.bind.call_count == 4
|
||||
|
||||
def test_raises_when_all_busy(self):
|
||||
from src.agent.run import _find_free_port
|
||||
with patch("socket.socket") as mock_socket:
|
||||
mock_instance = MagicMock()
|
||||
mock_socket.return_value.__enter__.return_value = mock_instance
|
||||
mock_instance.bind.side_effect = OSError("Address in use")
|
||||
with pytest.raises(OSError, match="No free port found"):
|
||||
_find_free_port(8000, 3)
|
||||
assert mock_instance.bind.call_count == 3
|
||||
# #endregion test_find_free_port
|
||||
|
||||
|
||||
# #region test_fetch_llm_config [C:2] [TYPE Function]
|
||||
# @BRIEF Test _fetch_llm_config with retry and fallback behavior.
|
||||
class TestFetchLlmConfig:
|
||||
def test_returns_config_on_success(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
with patch("src.agent.run.httpx.get") as mock_get:
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"configured": True, "provider_type": "openai", "default_model": "gpt-4o"}
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is not None
|
||||
assert result["configured"] is True
|
||||
|
||||
def test_returns_none_when_not_configured(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
with patch("src.agent.run.httpx.get") as mock_get:
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"configured": False, "reason": "no provider"}
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
|
||||
def test_retries_on_failure(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
import time as time_module
|
||||
with patch("src.agent.run.httpx.get") as mock_get, \
|
||||
patch.object(time_module, "sleep") as mock_sleep:
|
||||
mock_get.side_effect = Exception("Connection refused")
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
assert mock_get.call_count == 6
|
||||
|
||||
def test_retries_then_returns_config(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
import time as time_module
|
||||
with patch("src.agent.run.httpx.get") as mock_get, \
|
||||
patch.object(time_module, "sleep") as mock_sleep:
|
||||
mock_get.side_effect = [
|
||||
Exception("Timeout"), # Attempt 1
|
||||
Exception("Timeout"), # Attempt 2
|
||||
MagicMock(json=lambda: {"configured": True, "provider_type": "openai"}), # Attempt 3
|
||||
]
|
||||
result = _fetch_llm_config()
|
||||
assert result is not None
|
||||
assert result["configured"] is True
|
||||
|
||||
def test_returns_none_after_max_retries_with_http_error(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
import time as time_module
|
||||
with patch("src.agent.run.httpx.get") as mock_get, \
|
||||
patch.object(time_module, "sleep") as mock_sleep:
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is None
|
||||
assert mock_get.call_count == 6
|
||||
|
||||
def test_uses_service_token_header(self):
|
||||
from src.agent.run import _fetch_llm_config
|
||||
with patch("src.agent.run.httpx.get") as mock_get, \
|
||||
patch("src.agent.run.os.getenv", return_value="test-token"):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"configured": True}
|
||||
mock_get.return_value = mock_response
|
||||
result = _fetch_llm_config()
|
||||
assert result is not None
|
||||
# Verify Authorization header was sent
|
||||
call_kwargs = mock_get.call_args[1]
|
||||
assert call_kwargs["headers"].get("Authorization") == "Bearer test-token"
|
||||
|
||||
|
||||
# #endregion test_fetch_llm_config
|
||||
# #endregion Test.AgentChat.Run
|
||||
Reference in New Issue
Block a user