test: 8 parallel agents — fix 150+ failures, add 30+ test files. schemas/models 99-100%, core/client_registry ~98%, maintenance 95-98%, git edges 97%, translate 98-100%, dashboard routes 95-96%, dataset_review 100%
This commit is contained in:
243
backend/tests/core/superset_client/test_client_datasets_edge.py
Normal file
243
backend/tests/core/superset_client/test_client_datasets_edge.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# #region Test.SupersetClient.Datasets.Edge [C:3] [TYPE Module] [SEMANTICS test, superset, datasets, mixin, edge]
|
||||
# @BRIEF Edge case tests for SupersetDatasetsMixin — get_datasets_summary, get_dataset_linked_dashboard_count, get_dataset_detail edge cases.
|
||||
# @RELATION BINDS_TO -> [SupersetDatasetsMixin]
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGetDatasetsSummary:
|
||||
@pytest.mark.asyncio
|
||||
async def test_summary(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_datasets = AsyncMock(return_value=(2, [
|
||||
{"id": 1, "table_name": "orders", "schema": "public", "database": {"database_name": "Main"}},
|
||||
{"id": 2, "table_name": "users", "schema": "public"}, # no database key → fallback to Unknown
|
||||
]))
|
||||
result = await mixin.get_datasets_summary()
|
||||
assert len(result) == 2
|
||||
assert result[0]["database"] == "Main"
|
||||
assert result[1]["database"] == "Unknown"
|
||||
|
||||
|
||||
class TestGetDatasetLinkedDashboardCount:
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_dashboards_in_response(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.return_value = {"dashboards": [{"id": 1}, {"id": 2}]}
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_result_key(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.return_value = {"result": {"dashboards": [{"id": 1}]}}
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_dict_response(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.return_value = "not_a_dict"
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exception_returns_zero(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.side_effect = Exception("API error")
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_dashboards(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.return_value = {"dashboards": []}
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_structure(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.client = AsyncMock()
|
||||
mixin.client.request.return_value = {"other": "data"}
|
||||
count = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert count == 0
|
||||
|
||||
|
||||
class TestGetDatasetDetail:
|
||||
@pytest.mark.asyncio
|
||||
async def test_column_with_none_id_skipped(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [
|
||||
{"id": None, "column_name": "skip_me"},
|
||||
{"id": 2, "column_name": "keep_me"},
|
||||
],
|
||||
"metrics": [],
|
||||
"sql": "SELECT 1",
|
||||
"database": {"id": 1, "backend": "postgresql"},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={"dashboards": [{"id": 1, "dashboard_title": "Dash"}]})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert len(detail["columns"]) == 1
|
||||
assert detail["columns"][0]["name"] == "keep_me"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_metric_with_none_id_skipped(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [],
|
||||
"metrics": [
|
||||
{"id": None, "metric_name": "skip"},
|
||||
{"id": 5, "metric_name": "keep"},
|
||||
],
|
||||
"sql": "",
|
||||
"database": {"id": 1},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={"dashboards": []})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert len(detail["metrics"]) == 1
|
||||
assert detail["metrics"][0]["metric_name"] == "keep"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_with_none_id_skipped(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [], "metrics": [], "sql": "",
|
||||
"database": {"id": 1, "backend": "postgresql"},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={
|
||||
"dashboards": [{"id": None, "dashboard_title": "Skip"}]
|
||||
})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert len(detail["linked_dashboards"]) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboard_int_id(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [], "metrics": [], "sql": "",
|
||||
"database": {"id": 1, "backend": "postgresql"},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={
|
||||
"dashboards": [
|
||||
{"id": 10, "dashboard_title": "Good"},
|
||||
{"id": None, "dashboard_title": "Skip"},
|
||||
]
|
||||
})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert len(detail["linked_dashboards"]) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboards_from_result(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {"columns": [], "metrics": [], "sql": "", "database": {"id": 1}}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
# Use the result key path
|
||||
mixin.client.request = AsyncMock(return_value={
|
||||
"result": {"dashboards": {"result": [{"id": 1}]}}
|
||||
})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert len(detail["linked_dashboards"]) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dashboards_api_exception(self):
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {"columns": [], "metrics": [], "sql": "", "database": {"id": 1}}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request.side_effect = Exception("API error")
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert detail["linked_dashboards"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_result_not_in_response_structure(self):
|
||||
"""get_dataset returns data without 'result' key."""
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"id": 1, "columns": [{"id": 1, "column_name": "a"}],
|
||||
"metrics": [], "sql": "", "database": {"id": 1},
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={"dashboards": []})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert detail["id"] == 1
|
||||
assert len(detail["columns"]) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_as_bool_edge_cases(self):
|
||||
"""as_bool handles various input types."""
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [
|
||||
{"id": 1, "column_name": "c1", "is_dttm": "true", "is_active": "yes"},
|
||||
{"id": 2, "column_name": "c2", "is_dttm": "false", "is_active": "no"},
|
||||
],
|
||||
"metrics": [], "sql": "", "database": {"id": 1},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={"dashboards": []})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
cols = {c["name"]: c for c in detail["columns"]}
|
||||
assert cols["c1"]["is_dttm"] is True
|
||||
assert cols["c2"]["is_dttm"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_database_backend_fallback(self):
|
||||
"""Uses engine as fallback for database_backend."""
|
||||
from src.core.superset_client._datasets import SupersetDatasetsMixin
|
||||
mixin = SupersetDatasetsMixin()
|
||||
mixin.get_dataset = AsyncMock(return_value={
|
||||
"result": {
|
||||
"columns": [], "metrics": [], "sql": "",
|
||||
"database": {"id": 1, "engine": "postgresql"},
|
||||
}
|
||||
})
|
||||
mixin.client = MagicMock()
|
||||
mixin.client.request = AsyncMock(return_value={"dashboards": []})
|
||||
detail = await mixin.get_dataset_detail(1)
|
||||
assert detail["database_backend"] == "postgresql"
|
||||
# #endregion Test.SupersetClient.Datasets.Edge
|
||||
203
backend/tests/core/task_manager/test_graph.py
Normal file
203
backend/tests/core/task_manager/test_graph.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# #region Test.TaskManager.Graph [C:3] [TYPE Module] [SEMANTICS test, task, graph, registry, crud]
|
||||
# @BRIEF Tests for TaskGraph — in-memory task registry with persistence.
|
||||
# @RELATION BINDS_TO -> [TaskGraphModule]
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.task_manager.graph import TaskGraph
|
||||
from src.core.task_manager.models import Task, TaskStatus
|
||||
from src.core.task_manager.persistence import TaskPersistenceService
|
||||
|
||||
|
||||
def _make_task(**kwargs):
|
||||
"""Helper to create a Task mock with required attributes."""
|
||||
t = MagicMock(spec=Task)
|
||||
t.started_at = kwargs.pop('started_at', None)
|
||||
for k, v in kwargs.items():
|
||||
setattr(t, k, v)
|
||||
return t
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def persistence():
|
||||
return MagicMock(spec=TaskPersistenceService)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def graph(persistence):
|
||||
return TaskGraph(persistence)
|
||||
|
||||
|
||||
class TestInit:
|
||||
def test_empty(self, graph):
|
||||
assert graph.tasks == {}
|
||||
assert graph.task_futures == {}
|
||||
assert graph.persistence_service is not None
|
||||
|
||||
|
||||
class TestLoadPersistedTasks:
|
||||
def test_loads_new_tasks(self, graph, persistence):
|
||||
task1 = _make_task(id="t1")
|
||||
task2 = _make_task(id="t2")
|
||||
persistence.load_tasks.return_value = [task1, task2]
|
||||
graph.load_persisted_tasks(limit=50)
|
||||
assert len(graph.tasks) == 2
|
||||
assert graph.tasks["t1"] is task1
|
||||
assert graph.tasks["t2"] is task2
|
||||
persistence.load_tasks.assert_called_once_with(limit=50)
|
||||
|
||||
def test_skips_existing_tasks(self, graph, persistence):
|
||||
existing = _make_task(id="t1")
|
||||
graph.tasks["t1"] = existing
|
||||
new_task = _make_task(id="t2")
|
||||
loaded = _make_task(id="t1") # same ID as existing
|
||||
persistence.load_tasks.return_value = [loaded, new_task]
|
||||
graph.load_persisted_tasks()
|
||||
assert graph.tasks["t1"] is existing # not overwritten
|
||||
assert graph.tasks["t2"] is new_task
|
||||
|
||||
|
||||
class TestGetTask:
|
||||
def test_found(self, graph):
|
||||
task = _make_task(id="t1")
|
||||
graph.tasks["t1"] = task
|
||||
assert graph.get_task("t1") is task
|
||||
|
||||
def test_not_found(self, graph):
|
||||
assert graph.get_task("nonexistent") is None
|
||||
|
||||
|
||||
class TestGetAllTasks:
|
||||
def test_all(self, graph):
|
||||
t1 = _make_task(id="a")
|
||||
t2 = _make_task(id="b")
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
all_tasks = graph.get_all_tasks()
|
||||
assert len(all_tasks) == 2
|
||||
|
||||
|
||||
class TestGetTasks:
|
||||
def test_no_filters(self, graph):
|
||||
t1 = _make_task(id="a", started_at=None)
|
||||
t2 = _make_task(id="b", started_at=None)
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0)
|
||||
assert len(result) <= 2
|
||||
|
||||
def test_filter_by_status(self, graph):
|
||||
t1 = _make_task(id="a", status=TaskStatus.SUCCESS, started_at=None)
|
||||
t2 = _make_task(id="b", status=TaskStatus.FAILED, started_at=None)
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0, status=TaskStatus.SUCCESS)
|
||||
assert len(result) == 1
|
||||
assert result[0] is t1
|
||||
|
||||
def test_filter_by_plugin_ids(self, graph):
|
||||
t1 = _make_task(id="a", plugin_id="plugin_a", started_at=None)
|
||||
t2 = _make_task(id="b", plugin_id="plugin_b", started_at=None)
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0, plugin_ids=["plugin_a"])
|
||||
assert len(result) == 1
|
||||
assert result[0] is t1
|
||||
|
||||
def test_filter_completed_only(self, graph):
|
||||
t1 = _make_task(id="a", status=TaskStatus.SUCCESS, started_at=None)
|
||||
t2 = _make_task(id="b", status=TaskStatus.RUNNING, started_at=None)
|
||||
t3 = _make_task(id="c", status=TaskStatus.FAILED, started_at=None)
|
||||
graph.tasks = {"a": t1, "b": t2, "c": t3}
|
||||
result = graph.get_tasks(limit=10, offset=0, completed_only=True)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_sort_key_none_started_at(self, graph):
|
||||
"""Tasks with None started_at get sorted to the end."""
|
||||
t1 = _make_task(id="a", started_at=None)
|
||||
t2 = _make_task(id="b", started_at=datetime.now(UTC))
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0)
|
||||
assert result[0] is t2 # newest first
|
||||
|
||||
def test_sort_key_non_datetime(self, graph):
|
||||
"""started_at is not a datetime → treated as -inf."""
|
||||
t1 = _make_task(id="a", started_at="not_a_datetime")
|
||||
t2 = _make_task(id="b", started_at=datetime.now(UTC))
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0)
|
||||
assert result[0] is t2
|
||||
|
||||
def test_sort_key_naive_datetime(self, graph):
|
||||
"""Naive datetime gets UTC timezone."""
|
||||
t1 = _make_task(id="a", started_at=datetime(2024, 1, 1, 12, 0, 0)) # no tz
|
||||
t2 = _make_task(id="b", started_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=UTC))
|
||||
graph.tasks = {"a": t1, "b": t2}
|
||||
result = graph.get_tasks(limit=10, offset=0)
|
||||
assert result[0] is t2
|
||||
|
||||
|
||||
class TestAddTask:
|
||||
def test_adds(self, graph):
|
||||
task = _make_task(id="t1")
|
||||
graph.add_task(task)
|
||||
assert graph.tasks["t1"] is task
|
||||
|
||||
|
||||
class TestRemoveTasks:
|
||||
def test_removes_with_future(self, graph):
|
||||
task = _make_task(id="t1")
|
||||
future = MagicMock()
|
||||
graph.tasks["t1"] = task
|
||||
graph.task_futures["t1"] = future
|
||||
count = graph.remove_tasks(["t1"])
|
||||
assert count == 1
|
||||
assert "t1" not in graph.tasks
|
||||
assert "t1" not in graph.task_futures
|
||||
future.cancel.assert_called_once()
|
||||
|
||||
def test_removes_without_future(self, graph, persistence):
|
||||
task = _make_task(id="t1")
|
||||
graph.tasks["t1"] = task
|
||||
count = graph.remove_tasks(["t1"])
|
||||
assert count == 1
|
||||
persistence.delete_tasks.assert_called_once_with(["t1"])
|
||||
|
||||
def test_removes_empty_list(self, graph, persistence):
|
||||
count = graph.remove_tasks([])
|
||||
assert count == 0
|
||||
persistence.delete_tasks.assert_not_called()
|
||||
|
||||
|
||||
class TestCreateFuture:
|
||||
def test_creates(self, graph):
|
||||
future = MagicMock()
|
||||
graph.create_future("t1", future)
|
||||
assert graph.task_futures["t1"] is future
|
||||
|
||||
|
||||
class TestResolveFuture:
|
||||
def test_resolves(self, graph):
|
||||
future = MagicMock()
|
||||
graph.task_futures["t1"] = future
|
||||
graph.resolve_future("t1", result=True)
|
||||
future.set_result.assert_called_once_with(True)
|
||||
assert "t1" not in graph.task_futures
|
||||
|
||||
def test_resolve_nonexistent(self, graph):
|
||||
graph.resolve_future("nonexistent") # should not raise
|
||||
|
||||
|
||||
class TestRemoveFuture:
|
||||
def test_removes(self, graph):
|
||||
future = MagicMock()
|
||||
graph.task_futures["t1"] = future
|
||||
graph.remove_future("t1")
|
||||
assert "t1" not in graph.task_futures
|
||||
|
||||
def test_remove_nonexistent(self, graph):
|
||||
graph.remove_future("nonexistent") # should not raise
|
||||
# #endregion Test.TaskManager.Graph
|
||||
Reference in New Issue
Block a user