feat(037): Phase 3 — US2 Superset-Native Execution (T011-T015)
- T011: 4 NO-SQL tests (reject SQL, scalar execution, error taxonomy, temporal filter) - T012: _chart_data.py — SupersetChartDataMixin with execute_chart_data() - T013: query_executor.py — execute_dashboard_query (no-SQL guard, kind mapping) - T014: Agent tools — inspect_dashboard_query_model + execute_dashboard_result - T015: Verified superset_execute_sql excluded from _SCENARIO_TOOL_ALLOWLIST 4/4 executor tests pass. ChartDataMixin registered in SupersetClient. Dashboard testing tools added to both allowlist + dashboard context affinity.
This commit is contained in:
140
backend/tests/services/dashboard_testing/test_query_executor.py
Normal file
140
backend/tests/services/dashboard_testing/test_query_executor.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#region Test.DashboardTesting.QueryExecutor [C:3] [TYPE Module] [SEMANTICS testing,baseline,execution,no-sql]
|
||||
# @defgroup Tests for BaselineEngine.QueryExecutor — Superset-native chart-data execution without SQL.
|
||||
# @LAYER Test
|
||||
# @RELATION VERIFIES -> [BaselineEngine.QueryExecutor.ExecuteQuery]
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.schemas.dashboard_testing import (
|
||||
ExecuteQueryRequest, NormalizedFilterContext, NormalizedFilter,
|
||||
FilterValue, NormalizedValue, ValueKind,
|
||||
)
|
||||
from src.services.dashboard_testing.query_executor import execute_dashboard_query
|
||||
|
||||
# @region Test.DashboardTesting.QueryExecutor.NoSQLRejection [C:3] [TYPE Function] [SEMANTICS testing,baseline,no-sql,security]
|
||||
@pytest.mark.asyncio
|
||||
async def test_reject_sql_field_in_request():
|
||||
"""T011: Request with 'sql' field raises ValueError."""
|
||||
# The schema uses extra="forbid", so this should be caught by Pydantic
|
||||
with pytest.raises(Exception): # Pydantic ValidationError or ValueError
|
||||
ExecuteQueryRequest(
|
||||
environment_id="dev",
|
||||
dashboard_id=42,
|
||||
chart_id=128,
|
||||
result_key="sum__revenue",
|
||||
normalized_filters=NormalizedFilterContext(
|
||||
filters=[], filters_hash="sha256:empty"),
|
||||
sql="SELECT * FROM finance", # type: ignore # extra field
|
||||
)
|
||||
# @endregion Test.DashboardTesting.QueryExecutor.NoSQLRejection
|
||||
|
||||
# @region Test.DashboardTesting.QueryExecutor.BasicExecution [C:3] [TYPE Function] [SEMANTICS testing,baseline,execution]
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_scalar_metric():
|
||||
"""T011: Basic chart-data execution returns NormalizedValue."""
|
||||
client = AsyncMock()
|
||||
client.execute_chart_data = AsyncMock(return_value={
|
||||
"result": [{"data": {"sum__revenue": 50000.0}}],
|
||||
"query_id": "q-123",
|
||||
})
|
||||
|
||||
request = ExecuteQueryRequest(
|
||||
environment_id="ss-preprod",
|
||||
dashboard_id=42,
|
||||
chart_id=128,
|
||||
result_key="sum__revenue",
|
||||
normalized_filters=NormalizedFilterContext(
|
||||
filters=[
|
||||
NormalizedFilter(
|
||||
filter_id="NATIVE_FILTER-date",
|
||||
dataset_id=77,
|
||||
column="business_date",
|
||||
operator="TEMPORAL_RANGE",
|
||||
value=FilterValue(from_="2026-05-29", to="2026-05-29"),
|
||||
target_chart_ids=[128],
|
||||
)
|
||||
],
|
||||
filters_hash="sha256:test",
|
||||
),
|
||||
)
|
||||
|
||||
result = await execute_dashboard_query(client, request)
|
||||
|
||||
assert isinstance(result, NormalizedValue)
|
||||
assert result.raw_value is not None
|
||||
assert result.source # must have provenance
|
||||
# @endregion Test.DashboardTesting.QueryExecutor.BasicExecution
|
||||
|
||||
# @region Test.DashboardTesting.QueryExecutor.SupersetErrorTaxonomy [C:3] [TYPE Function] [SEMANTICS testing,baseline,error-taxonomy]
|
||||
@pytest.mark.asyncio
|
||||
async def test_superset_error_preserved():
|
||||
"""T011: Superset 403/5xx errors are preserved in result warnings."""
|
||||
from src.core.utils.network import SupersetAPIError
|
||||
|
||||
client = AsyncMock()
|
||||
client.execute_chart_data = AsyncMock(
|
||||
side_effect=SupersetAPIError("Forbidden", status_code=403)
|
||||
)
|
||||
|
||||
request = ExecuteQueryRequest(
|
||||
environment_id="dev",
|
||||
dashboard_id=42,
|
||||
chart_id=128,
|
||||
result_key="sum__revenue",
|
||||
normalized_filters=NormalizedFilterContext(
|
||||
filters=[], filters_hash="sha256:empty",
|
||||
),
|
||||
)
|
||||
|
||||
result = await execute_dashboard_query(client, request)
|
||||
|
||||
assert result.kind == ValueKind.UNKNOWN
|
||||
assert len(result.warnings) > 0
|
||||
assert "SUPERSET" in result.warnings[0].code
|
||||
# @endregion Test.DashboardTesting.QueryExecutor.SupersetErrorTaxonomy
|
||||
|
||||
# @region Test.DashboardTesting.QueryExecutor.TemporalFilterMapping [C:3] [TYPE Function] [SEMANTICS testing,baseline,temporal,filter]
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_filter_mapped_correctly():
|
||||
"""T011: TEMPORAL_RANGE filter is correctly mapped to chart-data format."""
|
||||
client = AsyncMock()
|
||||
execute_result: dict = {"result": [{"data": {"count": 150}}], "query_id": "q-456"}
|
||||
client.execute_chart_data = AsyncMock(return_value=execute_result)
|
||||
|
||||
request = ExecuteQueryRequest(
|
||||
environment_id="ss-preprod",
|
||||
dashboard_id=42,
|
||||
chart_id=128,
|
||||
result_key="count",
|
||||
normalized_filters=NormalizedFilterContext(
|
||||
filters=[
|
||||
NormalizedFilter(
|
||||
filter_id="NATIVE_FILTER-date",
|
||||
dataset_id=77,
|
||||
column="business_date",
|
||||
operator="TEMPORAL_RANGE",
|
||||
value=FilterValue(from_="2026-05-01", to="2026-05-31", inclusive=True),
|
||||
target_chart_ids=[128],
|
||||
)
|
||||
],
|
||||
filters_hash="sha256:test",
|
||||
),
|
||||
)
|
||||
|
||||
result = await execute_dashboard_query(client, request)
|
||||
assert result.raw_value == 150
|
||||
|
||||
# Verify the filter was passed correctly to execute_chart_data
|
||||
call_args = client.execute_chart_data.call_args
|
||||
filters = call_args.kwargs.get("filters", [])
|
||||
assert len(filters) == 1
|
||||
assert filters[0]["operator"] == "TEMPORAL_RANGE"
|
||||
assert filters[0]["from"] == "2026-05-01"
|
||||
assert filters[0]["to"] == "2026-05-31"
|
||||
# @endregion Test.DashboardTesting.QueryExecutor.TemporalFilterMapping
|
||||
|
||||
#endregion Test.DashboardTesting.QueryExecutor
|
||||
Reference in New Issue
Block a user