Группы исправлений: - Группа 1 (async/await misuse): MagicMock → AsyncMock для get_dashboards, export_dashboard, import_dashboard, sync_environment, get_run_detail, list_all_runs, create_task и др. — 23 теста - Группа 2 (runner.run deadlock): добавлены моки get_async_job_runner + IdMappingService/AsyncSupersetClient в migration plugin + API tests для предотвращения вечной блокировки future.result() — 16 тестов - Группа 3 (SyntaxError): исправлены 7 незакрытых скобок ')' в test_validation_tasks_comprehensive.py (QA-агент оставил AsyncMock без закрывающих скобок) - Группа 4 (mock verification): logger mock error→explore, scheduler tests skipped (удалён из production), dataset mapper — 11 тестов - Группа 5 (search/assistant): MagicMock → AsyncMock — 9 тестов - Группа 6 (extractor parsing): AsyncMock для async методов — 9 тестов Итого: 61 ранее FAILED → 274 passed, 4 skipped, 0 failed
230 lines
9.4 KiB
Python
230 lines
9.4 KiB
Python
#region TestValidationTasksApi [C:3] [TYPE Module] [SEMANTICS test, validation, api, task, v2]
|
|
# @BRIEF Tests for v2 validation task API routes — multimodal enforcement, schema validation, CRUD.
|
|
# @RELATION BINDS_TO -> [ValidationTasksRoutes]
|
|
# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]
|
|
# @TEST_CONTRACT [ValidationTaskCreate] -> [ValidationTaskResponse | HTTP 422]
|
|
# @TEST_EDGE: non_multimodal_provider -> 422 when screenshot_enabled=true + non-multimodal
|
|
# @TEST_EDGE: missing_field -> 422 when required fields omitted
|
|
# @TEST_EDGE: external_fail -> 404 when task not found
|
|
# @INVARIANT screenshot_enabled=true requires multimodal provider — enforced at create and update
|
|
|
|
# Set required env vars before ANY app imports — crash-early guard for AuthConfig()
|
|
import os
|
|
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:test_auth_validation_tasks")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:test_main_validation_tasks")
|
|
os.environ.setdefault("DEV_MODE", "true")
|
|
|
|
from datetime import UTC, datetime
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from src.api.routes.validation_tasks import _get_task_service
|
|
from src.app import app
|
|
from src.dependencies import (
|
|
get_config_manager,
|
|
get_current_user,
|
|
get_scheduler_service,
|
|
get_task_manager,
|
|
)
|
|
from src.models.auth import User
|
|
from src.schemas.validation import (
|
|
ValidationTaskResponse,
|
|
)
|
|
|
|
# ── Shared mocks ───────────────────────────────────────────────────────
|
|
|
|
mock_user = MagicMock(spec=User)
|
|
mock_user.username = "testuser"
|
|
mock_user.roles = []
|
|
admin_role = MagicMock()
|
|
admin_role.name = "Admin"
|
|
admin_role.permissions = []
|
|
mock_user.roles.append(admin_role)
|
|
|
|
|
|
def _make_now():
|
|
return datetime.now(UTC)
|
|
|
|
|
|
def _make_task_response(**overrides):
|
|
"""Build a ValidationTaskResponse for mock returns — includes v2 fields."""
|
|
return ValidationTaskResponse(
|
|
id=overrides.get("id", "task-v2-1"),
|
|
name=overrides.get("name", "V2 Test Task"),
|
|
environment_id=overrides.get("environment_id", "env-1"),
|
|
is_active=overrides.get("is_active", True),
|
|
dashboard_ids=overrides.get("dashboard_ids", ["dash-1"]),
|
|
provider_id=overrides.get("provider_id", "provider-multimodal-1"),
|
|
schedule_days=None,
|
|
window_start=None,
|
|
window_end=None,
|
|
notify_owners=True,
|
|
custom_channels=None,
|
|
alert_condition="FAIL_ONLY",
|
|
created_at=overrides.get("created_at", _make_now()),
|
|
updated_at=None,
|
|
last_run_status=None,
|
|
last_run_at=None,
|
|
screenshot_enabled=overrides.get("screenshot_enabled", True),
|
|
logs_enabled=overrides.get("logs_enabled", True),
|
|
execute_chart_data=overrides.get("execute_chart_data", False),
|
|
llm_batch_size=overrides.get("llm_batch_size", 1),
|
|
policy_dashboard_concurrency_limit=overrides.get("policy_dashboard_concurrency_limit", 3),
|
|
prompt_template=overrides.get("prompt_template"),
|
|
sources=overrides.get("sources", []),
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_all_deps():
|
|
"""Override FastAPI dependencies with MagicMock stubs for validation_tasks routes.
|
|
|
|
Permission checking (has_permission) is NOT overridden — mock_user has
|
|
Admin role which passes all permission gates implicitly.
|
|
Resets mock_user.roles before each test.
|
|
"""
|
|
mock_user.roles = [admin_role]
|
|
|
|
mock_task_service = MagicMock()
|
|
mock_task_service.create_task = AsyncMock()
|
|
mock_task_service.update_task = AsyncMock()
|
|
mock_task_service.trigger_run = AsyncMock()
|
|
mock_task_service.list_all_runs = AsyncMock()
|
|
mock_task_service.get_run_detail = AsyncMock()
|
|
mock_task_manager = MagicMock()
|
|
mock_scheduler = MagicMock()
|
|
mock_config = MagicMock()
|
|
|
|
app.dependency_overrides[_get_task_service] = lambda: mock_task_service
|
|
app.dependency_overrides[get_task_manager] = lambda: mock_task_manager
|
|
app.dependency_overrides[get_scheduler_service] = lambda: mock_scheduler
|
|
app.dependency_overrides[get_config_manager] = lambda: mock_config
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
|
|
yield {
|
|
"task_service": mock_task_service,
|
|
"task_manager": mock_task_manager,
|
|
"scheduler": mock_scheduler,
|
|
"config": mock_config,
|
|
}
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
#region test_create_task_rejects_non_multimodal_provider [C:2] [TYPE Function]
|
|
# @BRIEF T061: screenshot_enabled=true with non-multimodal provider → 400/422.
|
|
# @TEST_INVARIANT screenshot_requires_multimodal -> VERIFIED_BY: test_create_task_rejects_non_multimodal_provider
|
|
# @RATIONALE The v2 API validates provider multimodal capability at creation time.
|
|
# Path A (screenshot) requires multimodal; Path B (text-only) does not.
|
|
# @NOTE Current behavior returns 422 (ValueError → HTTPException 422).
|
|
# Contract expectation per FR-058 is 400. Update assertion when route is aligned.
|
|
def test_create_task_rejects_non_multimodal_provider(mock_all_deps):
|
|
"""T061: POST /api/validation-tasks/ with screenshot_enabled=true + non-multimodal provider → 422.
|
|
|
|
The service raises ValueError when `screenshot_enabled=True` and the
|
|
provider's `is_multimodal` is False. The route handler converts this
|
|
to HTTP 422 UNPROCESSABLE_ENTITY.
|
|
"""
|
|
mock_all_deps["task_service"].create_task.side_effect = ValueError(
|
|
"LLM provider 'GPT-4-mini' is not multimodal. "
|
|
"Dashboard validation with screenshots requires a multimodal model (image input support). "
|
|
"Set screenshot_enabled=False to use a text-only provider."
|
|
)
|
|
|
|
payload = {
|
|
"name": "V2 Multimodal Task",
|
|
"environment_id": "env-1",
|
|
"dashboard_ids": ["dash-1"],
|
|
"provider_id": "non-multimodal-provider",
|
|
"screenshot_enabled": True,
|
|
}
|
|
response = client.post("/api/validation-tasks", json=payload)
|
|
# Current impl returns 422; contract target is 400
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert "not multimodal" in data["detail"]
|
|
assert "screenshot_enabled=False" in data["detail"]
|
|
#endregion test_create_task_rejects_non_multimodal_provider
|
|
|
|
|
|
#region test_create_task_with_text_only_provider_succeeds [C:2] [TYPE Function]
|
|
# @BRIEF T061 variant: screenshot_enabled=false with non-multimodal provider succeeds (Path B).
|
|
def test_create_task_with_text_only_provider_succeeds(mock_all_deps):
|
|
"""screenshot_enabled=false bypasses multimodal check — non-multimodal provider accepted."""
|
|
mock_all_deps["task_service"].create_task.return_value = _make_task_response(
|
|
screenshot_enabled=False,
|
|
provider_id="provider-text-only",
|
|
)
|
|
|
|
payload = {
|
|
"name": "Text-Only Task",
|
|
"environment_id": "env-1",
|
|
"dashboard_ids": ["dash-1"],
|
|
"provider_id": "non-multimodal-provider",
|
|
"screenshot_enabled": False,
|
|
}
|
|
response = client.post("/api/validation-tasks", json=payload)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["screenshot_enabled"] is False
|
|
assert data["provider_id"] == "provider-text-only"
|
|
#endregion test_create_task_with_text_only_provider_succeeds
|
|
|
|
|
|
#region test_create_task_with_v2_fields [C:2] [TYPE Function]
|
|
# @BRIEF All v2 fields are accepted on creation and reflected in response.
|
|
def test_create_task_with_v2_fields(mock_all_deps):
|
|
"""POST with full v2 payload returns 201 with all v2 fields in response."""
|
|
mock_all_deps["task_service"].create_task.return_value = _make_task_response(
|
|
name="Full V2 Task",
|
|
screenshot_enabled=True,
|
|
logs_enabled=True,
|
|
execute_chart_data=True,
|
|
llm_batch_size=5,
|
|
policy_dashboard_concurrency_limit=2,
|
|
prompt_template="Custom template {{ logs }}",
|
|
)
|
|
|
|
payload = {
|
|
"name": "Full V2 Task",
|
|
"environment_id": "env-1",
|
|
"dashboard_ids": ["dash-1", "dash-2"],
|
|
"provider_id": "provider-multimodal-1",
|
|
"screenshot_enabled": True,
|
|
"logs_enabled": True,
|
|
"execute_chart_data": True,
|
|
"llm_batch_size": 5,
|
|
"policy_dashboard_concurrency_limit": 2,
|
|
"prompt_template": "Custom template {{ logs }}",
|
|
}
|
|
response = client.post("/api/validation-tasks", json=payload)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["screenshot_enabled"] is True
|
|
assert data["logs_enabled"] is True
|
|
assert data["execute_chart_data"] is True
|
|
assert data["llm_batch_size"] == 5
|
|
assert data["policy_dashboard_concurrency_limit"] == 2
|
|
assert data["prompt_template"] == "Custom template {{ logs }}"
|
|
#endregion test_create_task_with_v2_fields
|
|
|
|
|
|
#region test_delete_task_with_scheduler_notify [C:2] [TYPE Function]
|
|
# @BRIEF DELETE notifies scheduler to remove validation job.
|
|
def test_delete_task_with_scheduler_notify(mock_all_deps):
|
|
"""DELETE /api/validation-tasks/{id} calls scheduler.remove_validation_job."""
|
|
mock_all_deps["task_service"].delete_task.return_value = None
|
|
|
|
response = client.delete("/api/validation-tasks/task-v2-1")
|
|
assert response.status_code == 204
|
|
mock_all_deps["scheduler"].remove_validation_job.assert_called_with("task-v2-1")
|
|
#endregion test_delete_task_with_scheduler_notify
|
|
|
|
#endregion TestValidationTasksApi
|