таски готовы

This commit is contained in:
2026-02-23 10:18:56 +03:00
parent f0c85e4c03
commit 008b6d72c9
48 changed files with 3559 additions and 72 deletions

View File

@@ -0,0 +1,81 @@
{
"mixed_task_reports": {
"description": "Mixed reports across all supported task types",
"items": [
{
"report_id": "rep-001",
"task_id": "task-001",
"task_type": "llm_verification",
"status": "success",
"started_at": "2026-02-22T09:00:00Z",
"updated_at": "2026-02-22T09:00:30Z",
"summary": "LLM verification completed",
"details": {
"checks_performed": 12,
"issues_found": 1
}
},
{
"report_id": "rep-002",
"task_id": "task-002",
"task_type": "backup",
"status": "failed",
"started_at": "2026-02-22T09:10:00Z",
"updated_at": "2026-02-22T09:11:00Z",
"summary": "Backup failed due to storage limit",
"error_context": {
"message": "Not enough disk space",
"next_actions": ["Free storage", "Retry backup"]
}
},
{
"report_id": "rep-003",
"task_id": "task-003",
"task_type": "migration",
"status": "in_progress",
"started_at": "2026-02-22T09:20:00Z",
"updated_at": "2026-02-22T09:21:00Z",
"summary": "Migration running",
"details": {
"progress_percent": 42
}
},
{
"report_id": "rep-004",
"task_id": "task-004",
"task_type": "documentation",
"status": "partial",
"started_at": "2026-02-22T09:30:00Z",
"updated_at": "2026-02-22T09:31:00Z",
"summary": "Documentation generated with partial coverage",
"error_context": {
"message": "Missing metadata for 3 columns",
"next_actions": ["Review missing metadata"]
}
}
]
},
"unknown_type_partial_payload": {
"description": "Unknown type and partial payload fallback coverage",
"items": [
{
"report_id": "rep-unknown-001",
"task_id": "task-unknown-001",
"task_type": "unknown",
"status": "failed",
"updated_at": "2026-02-22T10:00:00Z",
"summary": "Unknown task type failed",
"details": null
},
{
"report_id": "rep-partial-001",
"task_id": "task-partial-001",
"task_type": "backup",
"status": "success",
"updated_at": "2026-02-22T10:05:00Z",
"summary": "Backup completed",
"details": {}
}
]
}
}

View File

@@ -0,0 +1,51 @@
# [DEF:backend.tests.test_report_normalizer:Module]
# @TIER: CRITICAL
# @SEMANTICS: tests, reports, normalizer, fallback
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
# @LAYER: Domain (Tests)
# @RELATION: TESTS -> backend.src.services.reports.normalizer
# @INVARIANT: Unknown plugin types are mapped to canonical unknown task type.
from datetime import datetime
from src.core.task_manager.models import Task, TaskStatus
from src.services.reports.normalizer import normalize_task_report
def test_unknown_type_maps_to_unknown_profile():
task = Task(
id="unknown-1",
plugin_id="custom-unmapped-plugin",
status=TaskStatus.FAILED,
started_at=datetime.utcnow(),
finished_at=datetime.utcnow(),
params={},
result={"error_message": "Unexpected plugin payload"},
)
report = normalize_task_report(task)
assert report.task_type.value == "unknown"
assert report.summary
assert report.error_context is not None
def test_partial_payload_keeps_report_visible_with_placeholders():
task = Task(
id="partial-1",
plugin_id="superset-backup",
status=TaskStatus.SUCCESS,
started_at=datetime.utcnow(),
finished_at=datetime.utcnow(),
params={},
result=None,
)
report = normalize_task_report(task)
assert report.task_type.value == "backup"
assert report.details is not None
assert "result" in report.details
# [/DEF:backend.tests.test_report_normalizer:Module]

View File

@@ -0,0 +1,117 @@
# [DEF:backend.tests.test_reports_api:Module]
# @TIER: CRITICAL
# @SEMANTICS: tests, reports, api, contract, pagination, filtering
# @PURPOSE: Contract tests for GET /api/reports defaults, pagination, and filtering behavior.
# @LAYER: Domain (Tests)
# @RELATION: TESTS -> backend.src.api.routes.reports
# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}.
from datetime import datetime, timedelta
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks
def get_all_tasks(self):
return self._tasks
def _admin_user():
admin_role = SimpleNamespace(name="Admin", permissions=[])
return SimpleNamespace(username="test-admin", roles=[admin_role])
def _make_task(task_id: str, plugin_id: str, status: TaskStatus, started_at: datetime, finished_at: datetime = None, result=None):
return Task(
id=task_id,
plugin_id=plugin_id,
status=status,
started_at=started_at,
finished_at=finished_at,
params={"environment_id": "env-1"},
result=result or {"summary": f"{plugin_id} {status.value.lower()}"},
)
def test_get_reports_default_pagination_contract():
now = datetime.utcnow()
tasks = [
_make_task("t-1", "superset-backup", TaskStatus.SUCCESS, now - timedelta(minutes=10), now - timedelta(minutes=9)),
_make_task("t-2", "superset-migration", TaskStatus.FAILED, now - timedelta(minutes=8), now - timedelta(minutes=7)),
_make_task("t-3", "llm_dashboard_validation", TaskStatus.RUNNING, now - timedelta(minutes=6), None),
]
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)
try:
client = TestClient(app)
response = client.get("/api/reports")
assert response.status_code == 200
data = response.json()
assert set(["items", "total", "page", "page_size", "has_next", "applied_filters"]).issubset(data.keys())
assert data["page"] == 1
assert data["page_size"] == 20
assert data["total"] == 3
assert isinstance(data["items"], list)
assert data["applied_filters"]["sort_by"] == "updated_at"
assert data["applied_filters"]["sort_order"] == "desc"
finally:
app.dependency_overrides.clear()
def test_get_reports_filter_and_pagination():
now = datetime.utcnow()
tasks = [
_make_task("t-1", "superset-backup", TaskStatus.SUCCESS, now - timedelta(minutes=30), now - timedelta(minutes=29)),
_make_task("t-2", "superset-backup", TaskStatus.FAILED, now - timedelta(minutes=20), now - timedelta(minutes=19)),
_make_task("t-3", "superset-migration", TaskStatus.FAILED, now - timedelta(minutes=10), now - timedelta(minutes=9)),
]
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)
try:
client = TestClient(app)
response = client.get("/api/reports?task_types=backup&statuses=failed&page=1&page_size=1")
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert data["page"] == 1
assert data["page_size"] == 1
assert data["has_next"] is False
assert len(data["items"]) == 1
assert data["items"][0]["task_type"] == "backup"
assert data["items"][0]["status"] == "failed"
finally:
app.dependency_overrides.clear()
def test_get_reports_invalid_filter_returns_400():
now = datetime.utcnow()
tasks = [_make_task("t-1", "superset-backup", TaskStatus.SUCCESS, now - timedelta(minutes=5), now - timedelta(minutes=4))]
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)
try:
client = TestClient(app)
response = client.get("/api/reports?task_types=bad_type")
assert response.status_code == 400
body = response.json()
assert "detail" in body
finally:
app.dependency_overrides.clear()
# [/DEF:backend.tests.test_reports_api:Module]

View File

@@ -0,0 +1,83 @@
# [DEF:backend.tests.test_reports_detail_api:Module]
# @TIER: CRITICAL
# @SEMANTICS: tests, reports, api, detail, diagnostics
# @PURPOSE: Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
# @LAYER: Domain (Tests)
# @RELATION: TESTS -> backend.src.api.routes.reports
from datetime import datetime, timedelta
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks
def get_all_tasks(self):
return self._tasks
def _admin_user():
role = SimpleNamespace(name="Admin", permissions=[])
return SimpleNamespace(username="test-admin", roles=[role])
def _make_task(task_id: str, plugin_id: str, status: TaskStatus, result=None):
now = datetime.utcnow()
return Task(
id=task_id,
plugin_id=plugin_id,
status=status,
started_at=now - timedelta(minutes=2),
finished_at=now - timedelta(minutes=1) if status != TaskStatus.RUNNING else None,
params={"environment_id": "env-1"},
result=result or {"summary": f"{plugin_id} result"},
)
def test_get_report_detail_success():
task = _make_task(
"detail-1",
"superset-migration",
TaskStatus.FAILED,
result={"error": {"message": "Step failed", "next_actions": ["Check mapping", "Retry"]}},
)
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])
try:
client = TestClient(app)
response = client.get("/api/reports/detail-1")
assert response.status_code == 200
data = response.json()
assert "report" in data
assert data["report"]["report_id"] == "detail-1"
assert "diagnostics" in data
assert "next_actions" in data
finally:
app.dependency_overrides.clear()
def test_get_report_detail_not_found():
task = _make_task("detail-2", "superset-backup", TaskStatus.SUCCESS)
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager([task])
try:
client = TestClient(app)
response = client.get("/api/reports/unknown-id")
assert response.status_code == 404
finally:
app.dependency_overrides.clear()
# [/DEF:backend.tests.test_reports_detail_api:Module]

View File

@@ -0,0 +1,81 @@
# [DEF:backend.tests.test_reports_openapi_conformance:Module]
# @TIER: CRITICAL
# @SEMANTICS: tests, reports, openapi, conformance
# @PURPOSE: Validate implemented reports payload shape against OpenAPI-required top-level contract fields.
# @LAYER: Domain (Tests)
# @RELATION: TESTS -> specs/020-task-reports-design/contracts/reports-api.openapi.yaml
# @INVARIANT: List and detail payloads include required contract keys.
from datetime import datetime
from types import SimpleNamespace
from fastapi.testclient import TestClient
from src.app import app
from src.core.task_manager.models import Task, TaskStatus
from src.dependencies import get_current_user, get_task_manager
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks
def get_all_tasks(self):
return self._tasks
def _admin_user():
role = SimpleNamespace(name="Admin", permissions=[])
return SimpleNamespace(username="test-admin", roles=[role])
def _task(task_id: str, plugin_id: str, status: TaskStatus):
now = datetime.utcnow()
return Task(
id=task_id,
plugin_id=plugin_id,
status=status,
started_at=now,
finished_at=now if status != TaskStatus.RUNNING else None,
params={"environment_id": "env-1"},
result={"summary": f"{plugin_id} {status.value.lower()}"},
)
def test_reports_list_openapi_required_keys():
tasks = [
_task("r-1", "superset-backup", TaskStatus.SUCCESS),
_task("r-2", "superset-migration", TaskStatus.FAILED),
]
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)
try:
client = TestClient(app)
response = client.get("/api/reports")
assert response.status_code == 200
body = response.json()
required = {"items", "total", "page", "page_size", "has_next", "applied_filters"}
assert required.issubset(body.keys())
finally:
app.dependency_overrides.clear()
def test_reports_detail_openapi_required_keys():
tasks = [_task("r-3", "llm_dashboard_validation", TaskStatus.SUCCESS)]
app.dependency_overrides[get_current_user] = lambda: _admin_user()
app.dependency_overrides[get_task_manager] = lambda: _FakeTaskManager(tasks)
try:
client = TestClient(app)
response = client.get("/api/reports/r-3")
assert response.status_code == 200
body = response.json()
assert "report" in body
finally:
app.dependency_overrides.clear()
# [/DEF:backend.tests.test_reports_openapi_conformance:Module]