chore: remaining pre-existing changes (storage, stream processor, tests, run.sh)

This commit is contained in:
2026-06-30 15:21:15 +03:00
parent 3b4ac807a5
commit 131c7cdfa4
9 changed files with 319 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
import json
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -77,6 +78,120 @@ class TestExtractUserId:
# #endregion test_extract_user_id
# #region test_confirmation_metadata [C:2] [TYPE Function]
# @BRIEF Test backend HITL confirmation contract exposed to the frontend.
class TestConfirmationMetadata:
def test_fast_confirmation_tool_detects_no_arg_read_intent(self):
from src.agent.app import _fast_confirmation_tool
assert _fast_confirmation_tool("Какие есть окружения?") == "list_environments"
assert _fast_confirmation_tool("Покажи maintenance") == "list_maintenance_events"
assert _fast_confirmation_tool("Запусти миграцию") is None
def test_extracts_tool_call_and_read_contract(self):
from src.agent.app import _confirmation_metadata
msg = MagicMock()
msg.tool_calls = [{"name": "list_environments", "args": {"env_id": "prod"}}]
state = MagicMock()
state.values = {"messages": [msg]}
state.next = ("tools",)
meta = _confirmation_metadata("conv-1", state, "Какие есть окружения?")
assert meta["type"] == "confirm_required"
assert meta["thread_id"] == "conv-1"
assert meta["prompt"] == "Разрешить чтение данных?"
assert meta["tool_name"] == "list_environments"
assert meta["tool_args"] == {"env_id": "prod"}
assert meta["risk"] == "read"
assert meta["risk_level"] == "safe"
assert meta["requires_confirmation"] is True
assert meta["intent"]["operation"] == "list_environments"
def test_infers_tool_from_text_when_state_only_has_graph_node(self):
from src.agent.app import _confirmation_metadata
state = MagicMock()
state.values = {"messages": []}
state.next = ("tools",)
meta = _confirmation_metadata("conv-2", state, "Покажи окружения")
assert meta["tool_name"] == "list_environments"
assert meta["risk"] == "read"
assert meta["risk_level"] == "safe"
assert meta["prompt"] == "Разрешить чтение данных?"
def test_write_contract_for_guarded_tool(self):
from src.agent.app import _confirmation_metadata
msg = MagicMock()
msg.tool_calls = [{
"name": "start_maintenance",
"args": {"environment_id": "prod", "tables": ["sales"]},
}]
state = MagicMock()
state.values = {"messages": [msg]}
state.next = ("tools",)
meta = _confirmation_metadata("conv-3", state, "Запусти maintenance")
assert meta["tool_name"] == "start_maintenance"
assert meta["risk"] == "write"
assert meta["risk_level"] == "guarded"
assert meta["prompt"] == "Подтвердить изменение данных?"
def test_unknown_contract_does_not_expose_graph_node_as_tool(self):
from src.agent.app import _confirmation_metadata
state = MagicMock()
state.values = {"messages": []}
state.next = ("tools",)
meta = _confirmation_metadata("conv-4", state, "Непонятное действие")
assert meta["tool_name"] == "unknown_action"
assert meta["risk"] == "unknown"
assert meta["risk_level"] == "unknown"
assert meta["prompt"] == "Подтвердите действие"
def test_fast_resume_deny_closes_without_langgraph(self):
from src.agent.app import _handle_resume, _pending_confirmations
_pending_confirmations["conv-fast-deny"] = {
"tool_name": "list_environments",
"tool_args": {},
}
async def collect():
return [chunk async for chunk in _handle_resume("conv-fast-deny", "deny")]
chunks = asyncio.run(collect())
data = json.loads(chunks[0])
assert data["metadata"] == {"type": "confirm_resolved", "result": "denied"}
def test_fast_resume_confirm_executes_tool_directly(self):
from src.agent.app import _handle_resume, _pending_confirmations
tool = MagicMock()
tool.ainvoke = AsyncMock(return_value='[{"id":"ss-dev"}]')
_pending_confirmations["conv-fast-confirm"] = {
"tool_name": "list_environments",
"tool_args": {},
}
async def collect():
with patch("src.agent.app._find_tool", return_value=tool):
return [chunk async for chunk in _handle_resume("conv-fast-confirm", "confirm")]
chunks = asyncio.run(collect())
metadata_types = [json.loads(chunk)["metadata"]["type"] for chunk in chunks]
assert metadata_types == ["confirm_resolved", "tool_start", "tool_end", "stream_token"]
assert json.loads(chunks[-1])["content"] == '[{"id":"ss-dev"}]'
# #endregion test_confirmation_metadata
# #region test_agent_handler [C:2] [TYPE Function]
# @BRIEF Test agent_handler for various scenarios.
class TestAgentHandler:

View File

@@ -267,6 +267,32 @@ async def test_list_environments_calls_correct_url():
args, kwargs = call_args
url = args[0] if args else kwargs.get("url", "")
assert "api/settings/environments" in url
@pytest.mark.anyio
async def test_list_environments_redacts_sensitive_fields():
"""list_environments must not expose backend secrets to chat output."""
from src.agent.context import set_service_jwt, set_user_jwt
from src.agent.tools import list_environments
set_user_jwt("jwt")
set_service_jwt("svc-jwt")
with patch("httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_client.return_value.__aenter__.return_value = mock_instance
mock_instance.get.return_value.status_code = 200
mock_instance.get.return_value.text = (
'[{"id":"prod","password":"secret-pass","api_key":"secret-key",'
'"nested":{"token":"secret-token"},"name":"ss-prod"}]'
)
result = await list_environments.ainvoke({})
assert "secret-pass" not in result
assert "secret-key" not in result
assert "secret-token" not in result
assert result.count("[redacted]") == 3
# #endregion TestAgentChat.Tools.ListEnvironments