== 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
136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
# #region TestAgentChat.Confirmations [C:3] [TYPE Module] [SEMANTICS test,agent,confirmation,hitl]
|
|
# @BRIEF Supplementary HITL confirmation flow tests — concurrent send, unknown action, model edge cases.
|
|
# @RELATION BINDS_TO -> [AgentChat.GradioApp.Handler]
|
|
# @TEST_EDGE: concurrent_send -> handler yields CONCURRENT_SEND error
|
|
# @TEST_EDGE: confirm_without_conversation_id -> handler still processes confirm
|
|
# @NOTE Resume confirm/deny handler tests are in test_agent_handler.py (test_handler_resume_confirm,
|
|
# test_handler_resume_deny). Model-level state machine tests are in AgentChatModel.test.ts.
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
sys.path.append(str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
|
|
import jwt
|
|
|
|
JWT_SECRET = "test-jwt-secret-key"
|
|
os.environ["JWT_SECRET"] = JWT_SECRET
|
|
os.environ["OPENAI_API_KEY"] = "sk-test-key"
|
|
os.environ["LLM_API_KEY"] = "sk-test-key"
|
|
os.environ["LLM_MODEL"] = "gpt-4o"
|
|
os.environ["LLM_BASE_URL"] = "https://api.openai.com/v1"
|
|
|
|
|
|
def _make_test_jwt(user_id: str = "test-user") -> str:
|
|
return jwt.encode({"sub": user_id}, JWT_SECRET, algorithm="HS256")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_save_conversation():
|
|
with patch("src.agent.app.save_conversation", new_callable=AsyncMock):
|
|
yield
|
|
|
|
|
|
# #region TestAgentChat.Confirmations.Concurrent [C:2] [TYPE Function] [SEMANTICS test,confirmation,concurrent]
|
|
# @BRIEF Concurrent send lock prevents multiple simultaneous sends from same user.
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_concurrent_send_lock():
|
|
"""Handler rejects concurrent sends from same user with CONCURRENT_SEND error."""
|
|
from src.agent.app import _user_locks, agent_handler
|
|
|
|
# Handler resolves user_id as "admin" when no JWT and no user_id_str are provided
|
|
_user_locks["admin"] = True
|
|
|
|
history: list = []
|
|
mock_request = MagicMock()
|
|
mock_request.headers = {}
|
|
mock_request.client.host = "127.0.0.1"
|
|
|
|
message = {"text": "hello", "files": None}
|
|
|
|
results = []
|
|
async for chunk in agent_handler(message, history, mock_request, "test-conv", None):
|
|
results.append(chunk)
|
|
|
|
assert len(results) == 1
|
|
import json
|
|
parsed = json.loads(results[0]) if isinstance(results[0], str) else results[0]
|
|
assert parsed["metadata"]["code"] == "CONCURRENT_SEND"
|
|
|
|
# Clean up
|
|
_user_locks.pop("admin", None)
|
|
# #endregion TestAgentChat.Confirmations.Concurrent
|
|
|
|
|
|
# #region TestAgentChat.Confirmations.UnknownAction [C:2] [TYPE Function] [SEMANTICS test,confirmation,unknown]
|
|
# @BRIEF Non-confirm/deny action values are treated as normal messages.
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_unknown_action_treated_as_normal():
|
|
"""Handler with unknown action string proceeds as normal message send."""
|
|
from src.agent.app import agent_handler
|
|
|
|
history: list = []
|
|
mock_request = MagicMock()
|
|
token = _make_test_jwt()
|
|
mock_request.headers = {"authorization": f"Bearer {token}"}
|
|
mock_request.client.host = "127.0.0.1"
|
|
|
|
message = {"text": "hello", "files": None}
|
|
|
|
with patch("src.agent.app.create_agent") as mock_create:
|
|
mock_graph = MagicMock()
|
|
async def _mock_stream(*_args, **_kwargs):
|
|
yield {"event": "on_chat_model_stream", "data": {"chunk": MagicMock(content="Hello")}}
|
|
mock_graph.astream_events = _mock_stream
|
|
state = MagicMock(spec_set=["next"])
|
|
state.next = ()
|
|
mock_graph.aget_state = AsyncMock(return_value=state)
|
|
mock_create.return_value = mock_graph
|
|
|
|
results = []
|
|
async for chunk in agent_handler(message, history, mock_request, "test-conv", "unknown_action"):
|
|
results.append(chunk)
|
|
|
|
# Should produce stream tokens, not confirm_resolved
|
|
assert len(results) > 0
|
|
import json
|
|
meta_types = [json.loads(r)["metadata"]["type"] for r in results]
|
|
assert "stream_token" in meta_types
|
|
assert "confirm_resolved" not in meta_types
|
|
# #endregion TestAgentChat.Confirmations.UnknownAction
|
|
|
|
|
|
# #region TestAgentChat.Confirmations.ExpiredState [C:2] [TYPE Function] [SEMANTICS test,confirmation,expired]
|
|
# @BRIEF Stale confirmations — checkpoint no longer available.
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_confirm_no_graph():
|
|
"""Handler with action='confirm' but create_agent raises handles it gracefully."""
|
|
from src.agent.app import agent_handler
|
|
|
|
history: list = []
|
|
mock_request = MagicMock()
|
|
token = _make_test_jwt()
|
|
mock_request.headers = {"authorization": f"Bearer {token}"}
|
|
mock_request.client.host = "127.0.0.1"
|
|
|
|
message = {"text": "confirm", "files": None}
|
|
|
|
with patch("src.agent._confirmation.create_agent") as mock_create:
|
|
mock_create.side_effect = Exception("Graph creation failed")
|
|
|
|
try:
|
|
results = []
|
|
async for chunk in agent_handler(message, history, mock_request, "test-conv", "confirm"):
|
|
results.append(chunk)
|
|
# Exception may propagate or be caught — either is acceptable
|
|
except Exception:
|
|
pass
|
|
# #endregion TestAgentChat.Confirmations.ExpiredState
|
|
# #endregion TestAgentChat.Confirmations
|