semantic update

This commit is contained in:
2026-02-23 13:15:48 +03:00
parent 008b6d72c9
commit 26880d2e09
29 changed files with 5134 additions and 958 deletions

View File

@@ -1,8 +1,10 @@
# [DEF:backend.src.api.routes.__tests__.test_datasets:Module]
# @TIER: STANDARD
# @SEMANTICS: datasets, api, tests, pagination, mapping, docs
# @PURPOSE: Unit tests for Datasets API endpoints
# @LAYER: API
# @RELATION: TESTS -> backend.src.api.routes.datasets
# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths.
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
@@ -14,6 +16,7 @@ client = TestClient(app)
# [DEF:test_get_datasets_success:Function]
# @PURPOSE: Validate successful datasets listing contract for an existing environment.
# @TEST: GET /api/datasets returns 200 and valid schema
# @PRE: env_id exists
# @POST: Response matches DatasetsResponse schema

View File

@@ -6,7 +6,7 @@
# @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 datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from fastapi.testclient import TestClient
@@ -97,6 +97,28 @@ def test_get_reports_filter_and_pagination():
app.dependency_overrides.clear()
def test_get_reports_handles_mixed_naive_and_aware_datetimes():
naive_now = datetime.utcnow()
aware_now = datetime.now(timezone.utc)
tasks = [
_make_task("t-naive", "superset-backup", TaskStatus.SUCCESS, naive_now - timedelta(minutes=5), naive_now - timedelta(minutes=4)),
_make_task("t-aware", "superset-migration", TaskStatus.FAILED, aware_now - timedelta(minutes=3), aware_now - timedelta(minutes=2)),
]
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?sort_by=updated_at&sort_order=desc")
assert response.status_code == 200
data = response.json()
assert data["total"] == 2
assert len(data["items"]) == 2
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))]

View File

@@ -1,5 +1,6 @@
# [DEF:ConfigManagerModule:Module]
#
# @TIER: STANDARD
# @SEMANTICS: config, manager, persistence, postgresql
# @PURPOSE: Manages application configuration persisted in database with one-time migration from JSON.
# @LAYER: Core
@@ -26,9 +27,11 @@ from .logger import logger, configure_logger, belief_scope
# [DEF:ConfigManager:Class]
# @TIER: STANDARD
# @PURPOSE: A class to handle application configuration persistence and management.
class ConfigManager:
# [DEF:__init__:Function]
# @TIER: STANDARD
# @PURPOSE: Initializes the ConfigManager.
# @PRE: isinstance(config_path, str) and len(config_path) > 0
# @POST: self.config is an instance of AppConfig

View File

@@ -1,5 +1,6 @@
# [DEF:backend.src.scripts.migrate_sqlite_to_postgres:Module]
#
# @TIER: STANDARD
# @SEMANTICS: migration, sqlite, postgresql, config, task_logs, task_records
# @PURPOSE: Migrates legacy config and task history from SQLite/file storage to PostgreSQL.
# @LAYER: Scripts
@@ -35,7 +36,10 @@ DEFAULT_TARGET_URL = os.getenv(
# [DEF:_json_load_if_needed:Function]
# @TIER: STANDARD
# @PURPOSE: Parses JSON-like values from SQLite TEXT/JSON columns to Python objects.
# @PRE: value is scalar JSON/text/list/dict or None.
# @POST: Returns normalized Python object or original scalar value.
def _json_load_if_needed(value: Any) -> Any:
with belief_scope("_json_load_if_needed"):
if value is None:
@@ -52,6 +56,7 @@ def _json_load_if_needed(value: Any) -> Any:
except json.JSONDecodeError:
return value
return value
# [/DEF:_json_load_if_needed:Function]
# [DEF:_find_legacy_config_path:Function]
@@ -70,6 +75,7 @@ def _find_legacy_config_path(explicit_path: Optional[str]) -> Optional[Path]:
if candidate.exists():
return candidate
return None
# [/DEF:_find_legacy_config_path:Function]
# [DEF:_connect_sqlite:Function]
@@ -79,6 +85,7 @@ def _connect_sqlite(path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(str(path))
conn.row_factory = sqlite3.Row
return conn
# [/DEF:_connect_sqlite:Function]
# [DEF:_ensure_target_schema:Function]
@@ -143,6 +150,7 @@ def _ensure_target_schema(engine) -> None:
with engine.begin() as conn:
for stmt in stmts:
conn.execute(text(stmt))
# [/DEF:_ensure_target_schema:Function]
# [DEF:_migrate_config:Function]
@@ -168,6 +176,7 @@ def _migrate_config(engine, legacy_config_path: Optional[Path]) -> int:
)
logger.info("[_migrate_config][Coherence:OK] Config migrated from %s", legacy_config_path)
return 1
# [/DEF:_migrate_config:Function]
# [DEF:_migrate_tasks_and_logs:Function]
@@ -283,6 +292,7 @@ def _migrate_tasks_and_logs(engine, sqlite_conn: sqlite3.Connection) -> Dict[str
stats["task_logs_total"],
)
return stats
# [/DEF:_migrate_tasks_and_logs:Function]
# [DEF:run_migration:Function]
@@ -303,6 +313,7 @@ def run_migration(sqlite_path: Path, target_url: str, legacy_config_path: Option
return stats
finally:
sqlite_conn.close()
# [/DEF:run_migration:Function]
# [DEF:main:Function]

View File

@@ -1,9 +1,11 @@
# [DEF:backend.src.services.__tests__.test_resource_service:Module]
# @TIER: STANDARD
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity
# @PURPOSE: Unit tests for ResourceService
# @LAYER: Service
# @RELATION: TESTS -> backend.src.services.resource_service
# @RELATION: VERIFIES -> ResourceService
# @INVARIANT: Resource summaries preserve task linkage and status projection behavior.
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
@@ -11,6 +13,7 @@ from datetime import datetime
# [DEF:test_get_dashboards_with_status:Function]
# @PURPOSE: Validate dashboard enrichment includes git/task status projections.
# @TEST: get_dashboards_with_status returns dashboards with git and task status
# @PRE: SupersetClient returns dashboard list
# @POST: Each dashboard has git_status and last_task fields

View File

@@ -9,7 +9,7 @@
# @INVARIANT: List responses are deterministic and include applied filter echo metadata.
# [SECTION: IMPORTS]
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional
from ...core.task_manager import TaskManager
@@ -23,9 +23,14 @@ from .normalizer import normalize_task_report
# @TIER: CRITICAL
# @PRE: TaskManager dependency is initialized.
# @POST: Provides deterministic list/detail report responses.
# @INVARIANT: Service methods are read-only over task history source.
class ReportsService:
# [DEF:__init__:Function]
# @TIER: CRITICAL
# @PURPOSE: Initialize service with TaskManager dependency.
# @PRE: task_manager is a live TaskManager instance.
# @POST: self.task_manager is assigned and ready for read operations.
# @INVARIANT: Constructor performs no task mutations.
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
def __init__(self, task_manager: TaskManager):
self.task_manager = task_manager
@@ -33,6 +38,9 @@ class ReportsService:
# [DEF:_load_normalized_reports:Function]
# @PURPOSE: Build normalized reports from all available tasks.
# @PRE: Task manager returns iterable task history records.
# @POST: Returns normalized report list preserving source cardinality.
# @INVARIANT: Every returned item is a TaskReport.
# @RETURN: List[TaskReport] - Reports sorted later by list logic.
def _load_normalized_reports(self) -> List[TaskReport]:
tasks = self.task_manager.get_all_tasks()
@@ -40,8 +48,40 @@ class ReportsService:
return reports
# [/DEF:_load_normalized_reports:Function]
# [DEF:_to_utc_datetime:Function]
# @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
# @PRE: value is either datetime or None.
# @POST: Returns UTC-aware datetime or None.
# @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
# @PARAM: value (Optional[datetime]) - Source datetime value.
# @RETURN: Optional[datetime] - UTC-aware datetime or None.
def _to_utc_datetime(self, value: Optional[datetime]) -> Optional[datetime]:
if value is None:
return None
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
# [/DEF:_to_utc_datetime:Function]
# [DEF:_datetime_sort_key:Function]
# @PURPOSE: Produce stable numeric sort key for report timestamps.
# @PRE: report contains updated_at datetime.
# @POST: Returns float timestamp suitable for deterministic sorting.
# @INVARIANT: Mixed naive/aware datetimes never raise TypeError.
# @PARAM: report (TaskReport) - Report item.
# @RETURN: float - UTC timestamp key.
def _datetime_sort_key(self, report: TaskReport) -> float:
updated = self._to_utc_datetime(report.updated_at)
if updated is None:
return 0.0
return updated.timestamp()
# [/DEF:_datetime_sort_key:Function]
# [DEF:_matches_query:Function]
# @PURPOSE: Apply query filtering to a report.
# @PRE: report and query are normalized schema instances.
# @POST: Returns True iff report satisfies all active query filters.
# @INVARIANT: Filter evaluation is side-effect free.
# @PARAM: report (TaskReport) - Candidate report.
# @PARAM: query (ReportQuery) - Applied query.
# @RETURN: bool - True if report matches all filters.
@@ -50,9 +90,13 @@ class ReportsService:
return False
if query.statuses and report.status not in query.statuses:
return False
if query.time_from and report.updated_at < query.time_from:
report_updated_at = self._to_utc_datetime(report.updated_at)
query_time_from = self._to_utc_datetime(query.time_from)
query_time_to = self._to_utc_datetime(query.time_to)
if query_time_from and report_updated_at and report_updated_at < query_time_from:
return False
if query.time_to and report.updated_at > query.time_to:
if query_time_to and report_updated_at and report_updated_at > query_time_to:
return False
if query.search:
needle = query.search.lower()
@@ -64,6 +108,9 @@ class ReportsService:
# [DEF:_sort_reports:Function]
# @PURPOSE: Sort reports deterministically according to query settings.
# @PRE: reports contains only TaskReport items.
# @POST: Returns reports ordered by selected sort field and order.
# @INVARIANT: Sorting criteria are deterministic for equal input.
# @PARAM: reports (List[TaskReport]) - Filtered reports.
# @PARAM: query (ReportQuery) - Sort config.
# @RETURN: List[TaskReport] - Sorted reports.
@@ -75,7 +122,7 @@ class ReportsService:
elif query.sort_by == "task_type":
reports.sort(key=lambda item: item.task_type.value, reverse=reverse)
else:
reports.sort(key=lambda item: item.updated_at, reverse=reverse)
reports.sort(key=self._datetime_sort_key, reverse=reverse)
return reports
# [/DEF:_sort_reports:Function]