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
This commit is contained in:
87
backend/tests/test_agent_timeout.py
Normal file
87
backend/tests/test_agent_timeout.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# #region Test.AgentChat.ToolTimeout [C:3] [TYPE Module] [SEMANTICS test,agent,tools,timeout]
|
||||
# @BRIEF Contract tests for _execute_with_timeout — configurable timeout wrapper.
|
||||
# @RELATION BINDS_TO -> [AgentChat.Tools.Timeout]
|
||||
# @TEST_EDGE: complete_under_timeout -> Tool completes normally, returns result
|
||||
# @TEST_EDGE: read_tool_timeout -> Read tool exceeds timeout, raises TimeoutError
|
||||
# @TEST_EDGE: write_tool_timeout -> Write tool exceeds timeout, raises TimeoutError
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ── Tests ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
# #region test_completes_under_timeout [C:2] [TYPE Function]
|
||||
# @BRIEF GIVEN a tool that returns quickly WHEN _execute_with_timeout is called with a 30s timeout THEN the result is returned normally.
|
||||
@pytest.mark.asyncio
|
||||
async def test_completes_under_timeout():
|
||||
"""Tool returns its result before the timeout expires."""
|
||||
from src.agent.tools import _execute_with_timeout
|
||||
|
||||
expected = {"status": "ok", "data": [1, 2, 3]}
|
||||
fast_fn = AsyncMock(return_value=expected)
|
||||
|
||||
with patch("src.agent.tools.logger.explore") as mock_explore:
|
||||
result = await _execute_with_timeout("fast_tool", fast_fn, is_write=False, timeout_s=30)
|
||||
|
||||
assert result == expected
|
||||
fast_fn.assert_called_once()
|
||||
mock_explore.assert_not_called()
|
||||
# #endregion test_completes_under_timeout
|
||||
|
||||
|
||||
# #region test_read_tool_timeout [C:2] [TYPE Function]
|
||||
# @BRIEF GIVEN a read tool that exceeds the timeout WHEN _execute_with_timeout is called THEN TimeoutError is raised and logger.explore is invoked.
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_tool_timeout():
|
||||
"""Read tool that sleeps longer than timeout_s must raise TimeoutError."""
|
||||
from src.agent.tools import _execute_with_timeout
|
||||
|
||||
async def slow_read():
|
||||
await asyncio.sleep(0.3)
|
||||
return "never_reached"
|
||||
|
||||
with patch("src.agent.tools.logger.explore") as mock_explore:
|
||||
with pytest.raises(TimeoutError):
|
||||
await _execute_with_timeout("slow_read", slow_read, is_write=False, timeout_s=0.05)
|
||||
|
||||
mock_explore.assert_called_once()
|
||||
call_args = mock_explore.call_args
|
||||
assert call_args[0][0] == "Tool timeout"
|
||||
payload = call_args[1]["payload"]
|
||||
assert payload["tool"] == "slow_read"
|
||||
assert payload["timeout_s"] == 0.05
|
||||
assert payload["is_write"] is False
|
||||
assert call_args[1]["extra"]["src"] == "AgentChat.Tools.Timeout"
|
||||
# #endregion test_read_tool_timeout
|
||||
|
||||
|
||||
# #region test_write_tool_timeout [C:2] [TYPE Function]
|
||||
# @BRIEF GIVEN a write tool that exceeds the timeout WHEN _execute_with_timeout is called THEN TimeoutError is raised with is_write=True logged.
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_tool_timeout():
|
||||
"""Write tool that sleeps longer than timeout_s must raise TimeoutError."""
|
||||
from src.agent.tools import _execute_with_timeout
|
||||
|
||||
async def slow_write():
|
||||
await asyncio.sleep(0.3)
|
||||
return "never_reached"
|
||||
|
||||
with patch("src.agent.tools.logger.explore") as mock_explore:
|
||||
with pytest.raises(TimeoutError):
|
||||
await _execute_with_timeout("slow_write", slow_write, is_write=True, timeout_s=0.05)
|
||||
|
||||
mock_explore.assert_called_once()
|
||||
call_args = mock_explore.call_args
|
||||
assert call_args[0][0] == "Tool timeout"
|
||||
payload = call_args[1]["payload"]
|
||||
assert payload["tool"] == "slow_write"
|
||||
assert payload["timeout_s"] == 0.05
|
||||
assert payload["is_write"] is True
|
||||
assert call_args[1]["extra"]["src"] == "AgentChat.Tools.Timeout"
|
||||
# #endregion test_write_tool_timeout
|
||||
|
||||
# #endregion Test.AgentChat.ToolTimeout
|
||||
Reference in New Issue
Block a user