455 lines
18 KiB
Python
455 lines
18 KiB
Python
# #region Test.Assistant.ToolMaintenance [C:2] [TYPE Module] [SEMANTICS test,assistant,maintenance,events]
|
|
# @BRIEF Tests for _tool_maintenance.py — maintenance handlers.
|
|
# @RELATION BINDS_TO -> [AssistantToolMaintenance]
|
|
# @TEST_EDGE: list_maintenance_events -> Lists active and completed events
|
|
# @TEST_EDGE: list_maintenance_no_events -> Empty maintenance response
|
|
# @TEST_EDGE: start_maintenance -> Start with tables and start_time
|
|
# @TEST_EDGE: start_maintenance_missing_params -> 422 on missing params
|
|
# @TEST_EDGE: start_maintenance_invalid_time -> 422 on bad datetime
|
|
# @TEST_EDGE: start_maintenance_too_many_tables -> 422 on >100 tables
|
|
# @TEST_EDGE: end_maintenance_by_id -> End specific event
|
|
# @TEST_EDGE: end_maintenance_not_found -> 404 on missing event
|
|
# @TEST_EDGE: end_maintenance_all_active -> End all active events
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def _make_maintenance_event(event_id="evt-1", status="ACTIVE", tables=None, environment_id="env-1"):
|
|
ev = MagicMock()
|
|
ev.id = event_id
|
|
ev.status = status
|
|
ev.tables = tables or ["users"]
|
|
ev.environment_id = environment_id
|
|
ev.created_at = datetime.now(UTC)
|
|
ev.end_time = None
|
|
return ev
|
|
|
|
|
|
class TestHandleListMaintenanceEvents:
|
|
"""handle_list_maintenance_events — listing events."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _patch_permissions(self):
|
|
with patch("src.api.routes.assistant._tool_maintenance._check_any_permission", return_value=None):
|
|
yield
|
|
|
|
@pytest.fixture
|
|
def intent(self):
|
|
return {"entities": {}}
|
|
|
|
@pytest.fixture
|
|
def current_user(self):
|
|
from src.schemas.auth import User
|
|
return User(id="user-1", username="test", email="test@x.com", is_active=True,
|
|
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lists_active_and_completed(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_list_maintenance_events
|
|
from src.models.maintenance import MaintenanceEventStatus
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-99"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
active_events = [_make_maintenance_event("evt-1", MaintenanceEventStatus.ACTIVE)]
|
|
completed_events = [_make_maintenance_event("evt-2", MaintenanceEventStatus.COMPLETED)]
|
|
|
|
# Mock the queries
|
|
db.query.return_value.filter.return_value = MagicMock()
|
|
db.query.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
|
|
# Separate mocks for each query
|
|
def query_side_effect(model):
|
|
q = MagicMock()
|
|
if model.__name__ == "MaintenanceEvent":
|
|
# Active events
|
|
q.filter.return_value = MagicMock()
|
|
q.filter.return_value.filter.return_value.all.side_effect = [
|
|
[], # expired
|
|
active_events, # active
|
|
]
|
|
q.filter.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = completed_events
|
|
q.filter.return_value.filter.return_value.order_by.return_value.all.return_value = active_events
|
|
q.filter.return_value.in_.return_value = q.filter.return_value
|
|
q.filter.return_value.isnot.return_value = q.filter.return_value
|
|
return q
|
|
|
|
db.query.side_effect = query_side_effect
|
|
|
|
# Mock dashboard state count
|
|
db.query.return_value.filter.return_value.filter.return_value = MagicMock()
|
|
db.query.return_value.filter.return_value.filter.return_value.count.return_value = 3
|
|
|
|
text, task_id, actions = await handle_list_maintenance_events(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Активные" in text or "Завершённые" in text
|
|
assert actions is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_events(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_list_maintenance_events
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-99"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
def query_side_effect(model):
|
|
q = MagicMock()
|
|
if model.__name__ == "MaintenanceEvent":
|
|
# Expired: query().filter(a, b, c).all()
|
|
q.filter.return_value.all.return_value = []
|
|
# Active: query().filter(...).order_by().all()
|
|
q.filter.return_value.order_by.return_value.all.return_value = []
|
|
# Completed: query().filter(...).order_by().limit().all()
|
|
q.filter.return_value.order_by.return_value.limit.return_value.all.return_value = []
|
|
else:
|
|
# MaintenanceDashboardState: query().filter(...).count() — won't be reached
|
|
q.filter.return_value.count.return_value = 0
|
|
return q
|
|
|
|
db.query.side_effect = query_side_effect
|
|
|
|
text, task_id, actions = await handle_list_maintenance_events(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Нет событий" in text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_with_expired_events(self, intent, current_user):
|
|
"""Expired events trigger end-task creation."""
|
|
from src.api.routes.assistant._tool_maintenance import handle_list_maintenance_events
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-exp-1"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
expired_events = [
|
|
_make_maintenance_event("evt-exp-1", "ACTIVE"),
|
|
_make_maintenance_event("evt-exp-2", "PARTIAL"),
|
|
]
|
|
active_events = [_make_maintenance_event("evt-act-1", "ACTIVE")]
|
|
completed_events = [_make_maintenance_event("evt-cmp-1", "COMPLETED")]
|
|
|
|
class CountMock:
|
|
"""Mock that returns a specific count value."""
|
|
def __init__(self, val):
|
|
self._val = val
|
|
def count(self):
|
|
return self._val
|
|
|
|
def query_side_effect(model):
|
|
q = MagicMock()
|
|
if model.__name__ == "MaintenanceEvent":
|
|
# First .filter(...).all() → expired events
|
|
# Then .filter(...).order_by().all() → active
|
|
# Then .filter(...).order_by().limit().all() → completed
|
|
f1 = MagicMock()
|
|
f1.all.side_effect = [expired_events, active_events, completed_events]
|
|
q.filter.return_value = f1
|
|
# For the order_by path used in active/completed:
|
|
f1.order_by.return_value.all.return_value = active_events
|
|
f1.order_by.return_value.limit.return_value.all.return_value = completed_events
|
|
elif model.__name__ == "MaintenanceDashboardState":
|
|
q.filter.return_value.filter.return_value.count.return_value = 2
|
|
return q
|
|
|
|
db.query.side_effect = query_side_effect
|
|
|
|
text, task_id, actions = await handle_list_maintenance_events(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Активные" in text
|
|
assert "Завершённые" in text
|
|
# Should have created tasks for expired events
|
|
assert task_manager.create_task.call_count >= 2
|
|
|
|
|
|
class TestHandleStartMaintenance:
|
|
"""handle_start_maintenance — starting maintenance events."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _patch_permissions(self):
|
|
with patch("src.api.routes.assistant._tool_maintenance._check_any_permission", return_value=None):
|
|
yield
|
|
|
|
@pytest.fixture
|
|
def intent(self):
|
|
return {"entities": {}}
|
|
|
|
@pytest.fixture
|
|
def current_user(self):
|
|
from src.schemas.auth import User
|
|
return User(id="user-1", username="test", email="test@x.com", is_active=True,
|
|
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_success(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-42"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": ["users", "orders"],
|
|
"start_time": "2025-06-15T10:00",
|
|
"end_time": "2025-06-15T12:00",
|
|
"message": "Scheduled maintenance",
|
|
}
|
|
|
|
text, task_id, actions = await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Обслуживание запущено" in text
|
|
assert task_id == "task-42"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_without_end_time(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-43"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": ["users"],
|
|
"start_time": "2025-06-15T10:00",
|
|
}
|
|
|
|
text, task_id, actions = await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Обслуживание запущено" in text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_missing_params_raises_422(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_missing_tables_raises_422(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {"start_time": "2025-06-15T10:00"}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_invalid_time_raises_422(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": ["users"],
|
|
"start_time": "not-a-date",
|
|
}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 422
|
|
assert "start_time" in exc.value.detail.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_invalid_end_time_raises_422(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": ["users"],
|
|
"start_time": "2025-06-15T10:00",
|
|
"end_time": "not-a-date",
|
|
}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 422
|
|
assert "end_time" in exc.value.detail.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_too_many_tables_raises_422(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": [f"table_{i}" for i in range(101)],
|
|
"start_time": "2025-06-15T10:00",
|
|
}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 422
|
|
assert "100" in exc.value.detail
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_with_tables_as_string(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_start_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-44"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
intent["entities"] = {
|
|
"tables": "users,orders,products",
|
|
"start_time": "2025-06-15T10:00",
|
|
}
|
|
|
|
text, task_id, actions = await handle_start_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Обслуживание запущено" in text
|
|
# Should have split the string
|
|
task_params = task_manager.create_task.call_args[1]["params"]
|
|
assert task_params["tables"] == ["users", "orders", "products"]
|
|
|
|
|
|
class TestHandleEndMaintenance:
|
|
"""handle_end_maintenance — ending maintenance events."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _patch_permissions(self):
|
|
with patch("src.api.routes.assistant._tool_maintenance._check_any_permission", return_value=None):
|
|
yield
|
|
|
|
@pytest.fixture
|
|
def intent(self):
|
|
return {"entities": {}}
|
|
|
|
@pytest.fixture
|
|
def current_user(self):
|
|
from src.schemas.auth import User
|
|
return User(id="user-1", username="test", email="test@x.com", is_active=True,
|
|
auth_source="internal", created_at=datetime.now(UTC), last_login=datetime.now(UTC), roles=[])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_end_by_id(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-45"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
event = _make_maintenance_event("evt-1")
|
|
db.query.return_value.filter.return_value.first.return_value = event
|
|
|
|
intent["entities"] = {"event_id": "evt-1"}
|
|
|
|
text, task_id, actions = await handle_end_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Завершение" in text
|
|
assert task_id == "task-45"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_end_not_found_raises_404(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
intent["entities"] = {"event_id": "evt-nonexistent"}
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await handle_end_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert exc.value.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_end_all_active_success(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
|
|
from src.models.maintenance import MaintenanceEventStatus
|
|
|
|
task_manager = MagicMock()
|
|
task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-46"))
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
active_events = [
|
|
_make_maintenance_event("evt-1", MaintenanceEventStatus.ACTIVE),
|
|
_make_maintenance_event("evt-2", MaintenanceEventStatus.PARTIAL),
|
|
]
|
|
# Code: db.query(MaintenanceEvent).filter(...).all()
|
|
db.query.return_value.filter.return_value.all.return_value = active_events
|
|
|
|
intent["entities"] = {} # No event_id -> end all
|
|
|
|
text, task_id, actions = await handle_end_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Завершение" in text
|
|
assert "2" in text # Should mention 2 events
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_end_all_no_active(self, intent, current_user):
|
|
from src.api.routes.assistant._tool_maintenance import handle_end_maintenance
|
|
|
|
task_manager = MagicMock()
|
|
config_manager = MagicMock()
|
|
db = MagicMock()
|
|
|
|
# Code: db.query(MaintenanceEvent).filter(...).all()
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
intent["entities"] = {} # No event_id
|
|
|
|
text, task_id, actions = await handle_end_maintenance(
|
|
intent, current_user, task_manager, config_manager, db
|
|
)
|
|
assert "Нет активных" in text
|
|
assert task_id is None
|
|
# #endregion Test.Assistant.ToolMaintenance
|