Files
ss-tools/backend/tests/test_agent/test_conversation_api.py
busya ff60865183 feat(agent-chat): 035-agent-chat-context — контекст, guardrails, tools, database discovery
== 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
2026-07-04 22:47:17 +03:00

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