== User stories == US1: Контекст с дашборда/датасета → /agent с URL params US2: Guardrails card — env badge, 7 risk tones, countdown, permission_denied US3: Tools optimization — retry, timeout, summarise, RBAC + context affinity == Backend == - _context.py (NEW): UIContext validation (7 checks) - _tool_filter.py (NEW): RBAC + context affinity pipeline - _confirmation.py: build_confirmation_contract_v2, permission_denied_payload - tools.py: superset_list_databases, retry/summarise/timeout wrappers - app.py: _inject_uicontext, _inject_env_id_into_tools, database prefetch в runtime context - _persistence.py: prefetch_databases() - agent_superset_explore.py: GET /databases endpoint - _llm_async_http.py, _persistence.py: fix double /v1 в LLM URL (LM Studio Unexpected endpoint) == Frontend == - AgentChatModel.svelte.ts: 5 atoms, 3 actions, countdown, context - AgentChat.svelte: production banner, process steps, debug panel - ConfirmationCard.svelte: 7 risk tones, permission_denied, countdown - ToolCallCard.svelte: retrying/timeout/cancelled states - StreamProcessor.svelte.ts: tool_retry, timeout, permission_denied - TopNavbar: sparkles icon + Ассистент - sidebarNavigation: AI section - DashboardHeader, datasets/+page: contextual AI buttons - Icon: sparkles, brain, cpu icons - tailwind: assistant category colors - i18n: en/ru nav keys == Tests == - 159 backend agent tests (+16 US3: retry, timeout, summarise, contracts) - 2544 frontend tests (+11 model + component tests) - 15 JSON fixtures (10 API + 5 model) == Specs == - specs/035-agent-chat-context/: spec, UX, plan, tasks, research, data-model, contracts, quickstart, traceability, fixtures, checklists Closes #035
160 lines
6.0 KiB
Python
160 lines
6.0 KiB
Python
# #region TestAgentChat.Api [C:3] [TYPE Module] [SEMANTICS test,agent,api,conversations]
|
|
# @BRIEF Tests for AgentChat REST API — save, active gate, LLM config.
|
|
# @RELATION BINDS_TO -> [AgentChat.Api.Conversations]
|
|
# @TEST_EDGE: save_valid -> new conversation created
|
|
# @TEST_EDGE: save_update -> existing conversation updated
|
|
# @TEST_EDGE: active_gate -> always returns {active: false}
|
|
# @TEST_EDGE: llm_config -> endpoint reachable
|
|
# @NOTE History and delete/archive for /api/assistant prefix are handled by legacy assistant routes
|
|
# (FR-020 backward compat). The new agent routes use /api/agent prefix for save/active/llm-config.
|
|
import asyncio
|
|
import pytest
|
|
from unittest.mock import MagicMock
|
|
import uuid
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from src.api.routes import agent_conversations
|
|
from src.core.database import SessionLocal
|
|
from src.schemas.agent import SaveConversationRequest
|
|
|
|
|
|
def _make_mock_user(user_id: str = "test-user-id"):
|
|
"""Create a mock authenticated user."""
|
|
mock_user = MagicMock()
|
|
mock_user.id = user_id
|
|
mock_user.username = "test-user"
|
|
return mock_user
|
|
|
|
|
|
MOCK_USER = _make_mock_user()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def db_session():
|
|
"""Provide a real session against the pytest global SQLite database."""
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _run(coro):
|
|
return asyncio.run(coro)
|
|
|
|
|
|
# #region TestAgentChat.Api.Save [C:2] [TYPE Function] [SEMANTICS test,api,save]
|
|
# @BRIEF Conversation save endpoint tests — POST /api/agent/conversations/save.
|
|
|
|
def test_save_conversation(db_session):
|
|
"""POST /api/agent/conversations/save creates a new conversation."""
|
|
conversation_id = f"test-save-{uuid.uuid4().hex}"
|
|
data = _run(
|
|
agent_conversations.save_conversation(
|
|
SaveConversationRequest(conversation_id=conversation_id, title="Save Test", user_id="test-user-id"),
|
|
db_session,
|
|
)
|
|
)
|
|
assert data["saved"] is True
|
|
assert data["conversation_id"] == conversation_id
|
|
|
|
|
|
def test_save_conversation_updates_existing(db_session):
|
|
"""POST /api/agent/conversations/save updates existing conversation title."""
|
|
conversation_id = f"test-save-{uuid.uuid4().hex}"
|
|
# Create
|
|
_run(
|
|
agent_conversations.save_conversation(
|
|
SaveConversationRequest(conversation_id=conversation_id, title="Original", user_id="test-user-id"),
|
|
db_session,
|
|
)
|
|
)
|
|
# Update
|
|
data = _run(
|
|
agent_conversations.save_conversation(
|
|
SaveConversationRequest(conversation_id=conversation_id, title="Updated Title", user_id="test-user-id"),
|
|
db_session,
|
|
)
|
|
)
|
|
assert data["saved"] is True
|
|
|
|
|
|
def test_save_conversation_default_user_id(db_session):
|
|
"""POST /api/agent/conversations/save without user_id defaults to 'admin'."""
|
|
data = _run(
|
|
agent_conversations.save_conversation(
|
|
SaveConversationRequest(conversation_id=f"test-save-{uuid.uuid4().hex}", title="Default User"),
|
|
db_session,
|
|
)
|
|
)
|
|
assert data["saved"] is True
|
|
# #endregion TestAgentChat.Api.Save
|
|
|
|
|
|
# #region TestAgentChat.Api.ActiveGate [C:2] [TYPE Function] [SEMANTICS test,api,active]
|
|
# @BRIEF Multi-tab active session gate tests — GET /api/agent/conversations/active.
|
|
|
|
def test_check_active_session():
|
|
"""GET /api/agent/conversations/active returns {active: false}."""
|
|
data = _run(agent_conversations.check_active_session())
|
|
assert data == {"active": False}
|
|
# #endregion TestAgentChat.Api.ActiveGate
|
|
|
|
|
|
# #region TestAgentChat.Api.LlmConfig [C:2] [TYPE Function] [SEMANTICS test,api,llm]
|
|
# @BRIEF LLM config endpoint tests — GET /api/agent/llm-config.
|
|
# @NOTE Requires ENCRYPTION_KEY at request time. Passes even when env var is cleared mid-suite.
|
|
|
|
def test_llm_config_endpoint_reachable(db_session):
|
|
"""GET /api/agent/llm-config is reachable (skip if EncryptionManager unavailable)."""
|
|
try:
|
|
config_manager = MagicMock()
|
|
config_manager.get_config.return_value.settings.llm = {}
|
|
data = _run(agent_conversations.get_agent_llm_config(db_session, config_manager))
|
|
assert isinstance(data, dict)
|
|
except RuntimeError as e:
|
|
if "ENCRYPTION_KEY" in str(e):
|
|
pytest.skip("ENCRYPTION_KEY not available at request time")
|
|
raise
|
|
|
|
|
|
def test_llm_config_response_shape(db_session):
|
|
"""LLM config response contains expected fields when 200."""
|
|
try:
|
|
config_manager = MagicMock()
|
|
config_manager.get_config.return_value.settings.llm = {}
|
|
data = _run(agent_conversations.get_agent_llm_config(db_session, config_manager))
|
|
assert "configured" in data, "Response should have 'configured' field"
|
|
except RuntimeError as e:
|
|
if "ENCRYPTION_KEY" in str(e):
|
|
pytest.skip("ENCRYPTION_KEY not available at request time")
|
|
raise
|
|
# #endregion TestAgentChat.Api.LlmConfig
|
|
|
|
|
|
# #region TestAgentChat.Api.LegacyCompat [C:2] [TYPE Function] [SEMANTICS test,api,legacy]
|
|
# @BRIEF Legacy assistant route backward compatibility (FR-020).
|
|
|
|
def test_legacy_history_returns_empty_for_nonexistent(db_session):
|
|
"""GET /api/assistant/history returns 404 for nonexistent conversation."""
|
|
bad_id = f"nonexistent-{uuid.uuid4().hex}"
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
_run(agent_conversations.get_history(bad_id, user=MOCK_USER, db=db_session))
|
|
assert exc_info.value.status_code == 404
|
|
assert exc_info.value.detail == "Conversation not found"
|
|
|
|
|
|
def test_legacy_conversations_list(db_session):
|
|
"""GET /api/assistant/conversations returns 200 with items (legacy compat)."""
|
|
data = _run(agent_conversations.list_conversations(page=1, page_size=20, search="", user=MOCK_USER, db=db_session))
|
|
assert hasattr(data, "items")
|
|
|
|
|
|
def test_legacy_pagination_params(db_session):
|
|
"""GET /api/assistant/conversations supports page/page_size (legacy compat)."""
|
|
data = _run(agent_conversations.list_conversations(page=1, page_size=5, search="", user=MOCK_USER, db=db_session))
|
|
assert hasattr(data, "items")
|
|
# #endregion TestAgentChat.Api.LegacyCompat
|
|
# #endregion TestAgentChat.Api
|