chore: remaining pre-existing changes (storage, stream processor, tests, run.sh)
This commit is contained in:
@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
|
||||
class FileCategory(str, Enum):
|
||||
BACKUP = "backups"
|
||||
REPOSITORY = "repositories"
|
||||
CHAT_UPLOADS = "chat_uploads"
|
||||
# #endregion FileCategory
|
||||
|
||||
# #region StorageConfig [C:1] [TYPE Class]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user