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:
2026-07-28 19:32:09 +03:00
parent 012f903a57
commit b8235ef2a1
7 changed files with 556 additions and 10 deletions

View 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