mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
fix(mcp): fix crashes in list tools, dataset info, chart preview, and add owner/favorite filters (#38277)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,21 @@ def _mock_chart(id: int = 1, slice_name: str = "Test Chart") -> Mock:
|
||||
chart.id = id
|
||||
chart.slice_name = slice_name
|
||||
chart.uuid = f"chart-uuid-{id}"
|
||||
chart.tags = []
|
||||
chart.owners = []
|
||||
chart.viz_type = "table"
|
||||
chart.datasource_name = None
|
||||
chart.datasource_type = None
|
||||
chart.description = None
|
||||
chart.cache_timeout = None
|
||||
chart.changed_by = None
|
||||
chart.changed_by_name = None
|
||||
chart.changed_on = None
|
||||
chart.changed_on_humanized = None
|
||||
chart.created_by = None
|
||||
chart.created_by_name = None
|
||||
chart.created_on = None
|
||||
chart.created_on_humanized = None
|
||||
return chart
|
||||
|
||||
|
||||
@@ -390,7 +405,7 @@ class TestAddChartToExistingDashboard:
|
||||
):
|
||||
"""Test adding a chart to an existing dashboard."""
|
||||
mock_dashboard = _mock_dashboard(id=1, title="Existing Dashboard")
|
||||
mock_dashboard.slices = [Mock(id=10), Mock(id=20)]
|
||||
mock_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=20)]
|
||||
mock_dashboard.position_json = json.dumps(
|
||||
{
|
||||
"ROOT_ID": {
|
||||
@@ -430,7 +445,11 @@ class TestAddChartToExistingDashboard:
|
||||
mock_db_session.get.return_value = mock_chart
|
||||
|
||||
updated_dashboard = _mock_dashboard(id=1, title="Existing Dashboard")
|
||||
updated_dashboard.slices = [Mock(id=10), Mock(id=20), Mock(id=30)]
|
||||
updated_dashboard.slices = [
|
||||
_mock_chart(id=10),
|
||||
_mock_chart(id=20),
|
||||
_mock_chart(id=30),
|
||||
]
|
||||
mock_update_command.return_value.run.return_value = updated_dashboard
|
||||
|
||||
request = {"dashboard_id": 1, "chart_id": 30}
|
||||
@@ -505,7 +524,7 @@ class TestAddChartToExistingDashboard:
|
||||
):
|
||||
"""Test error when chart is already in dashboard."""
|
||||
mock_dashboard = _mock_dashboard()
|
||||
mock_dashboard.slices = [Mock(id=5)]
|
||||
mock_dashboard.slices = [_mock_chart(id=5)]
|
||||
mock_find_dashboard.return_value = mock_dashboard
|
||||
mock_db_session.get.return_value = _mock_chart(id=5)
|
||||
request = {"dashboard_id": 1, "chart_id": 5}
|
||||
@@ -537,7 +556,7 @@ class TestAddChartToExistingDashboard:
|
||||
mock_db_session.get.return_value = mock_chart
|
||||
|
||||
updated_dashboard = _mock_dashboard(id=2)
|
||||
updated_dashboard.slices = [Mock(id=15)]
|
||||
updated_dashboard.slices = [_mock_chart(id=15)]
|
||||
mock_update_command.return_value.run.return_value = updated_dashboard
|
||||
|
||||
request = {"dashboard_id": 2, "chart_id": 15}
|
||||
@@ -585,7 +604,7 @@ class TestAddChartToExistingDashboard:
|
||||
):
|
||||
"""Test adding chart to a dashboard that uses tabs."""
|
||||
mock_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
|
||||
mock_dashboard.slices = [Mock(id=10)]
|
||||
mock_dashboard.slices = [_mock_chart(id=10)]
|
||||
mock_dashboard.position_json = json.dumps(
|
||||
{
|
||||
"ROOT_ID": {
|
||||
@@ -646,7 +665,7 @@ class TestAddChartToExistingDashboard:
|
||||
mock_db_session.get.return_value = mock_chart
|
||||
|
||||
updated_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
|
||||
updated_dashboard.slices = [Mock(id=10), Mock(id=25)]
|
||||
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=25)]
|
||||
mock_update_command.return_value.run.return_value = updated_dashboard
|
||||
|
||||
request = {"dashboard_id": 3, "chart_id": 25}
|
||||
@@ -685,7 +704,7 @@ class TestAddChartToExistingDashboard:
|
||||
):
|
||||
"""Test adding chart to a specific tab using target_tab name."""
|
||||
mock_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
|
||||
mock_dashboard.slices = [Mock(id=10)]
|
||||
mock_dashboard.slices = [_mock_chart(id=10)]
|
||||
mock_dashboard.position_json = json.dumps(
|
||||
{
|
||||
"ROOT_ID": {
|
||||
@@ -746,7 +765,7 @@ class TestAddChartToExistingDashboard:
|
||||
mock_db_session.get.return_value = mock_chart
|
||||
|
||||
updated_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
|
||||
updated_dashboard.slices = [Mock(id=10), Mock(id=30)]
|
||||
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=30)]
|
||||
mock_update_command.return_value.run.return_value = updated_dashboard
|
||||
|
||||
request = {"dashboard_id": 3, "chart_id": 30, "target_tab": "Customers"}
|
||||
@@ -786,7 +805,7 @@ class TestAddChartToExistingDashboard:
|
||||
):
|
||||
"""Test adding chart to dashboard that has nanoid-style ROW IDs."""
|
||||
mock_dashboard = _mock_dashboard(id=4, title="Nanoid Dashboard")
|
||||
mock_dashboard.slices = [Mock(id=10)]
|
||||
mock_dashboard.slices = [_mock_chart(id=10)]
|
||||
mock_dashboard.position_json = json.dumps(
|
||||
{
|
||||
"ROOT_ID": {
|
||||
@@ -821,7 +840,7 @@ class TestAddChartToExistingDashboard:
|
||||
mock_db_session.get.return_value = mock_chart
|
||||
|
||||
updated_dashboard = _mock_dashboard(id=4, title="Nanoid Dashboard")
|
||||
updated_dashboard.slices = [Mock(id=10), Mock(id=50)]
|
||||
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=50)]
|
||||
mock_update_command.return_value.run.return_value = updated_dashboard
|
||||
|
||||
request = {"dashboard_id": 4, "chart_id": 50}
|
||||
|
||||
@@ -1319,7 +1319,7 @@ class TestDatasetDefaultColumnFiltering:
|
||||
dataset_item = data["datasets"][0]
|
||||
|
||||
# Verify ONLY default columns are present in the response item
|
||||
expected_keys = {"id", "table_name", "schema_name", "changed_on_humanized"}
|
||||
expected_keys = {"id", "table_name", "schema", "changed_on_humanized"}
|
||||
actual_keys = set(dataset_item.keys())
|
||||
|
||||
# The response should only contain the default columns, NOT all columns
|
||||
|
||||
@@ -702,6 +702,41 @@ class TestExecuteSql:
|
||||
with pytest.raises(ToolError, match="less than or equal to 10000"):
|
||||
await client.call_tool("execute_sql", {"request": request})
|
||||
|
||||
@patch("superset.security_manager")
|
||||
@patch("superset.db")
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sql_no_limit_respects_sql(
|
||||
self, mock_db, mock_security_manager, mcp_server
|
||||
):
|
||||
"""Test that omitting limit lets the SQL LIMIT clause be respected."""
|
||||
mock_database = _mock_database()
|
||||
mock_database.execute.return_value = _create_select_result(
|
||||
rows=[{"id": i} for i in range(5)],
|
||||
columns=["id"],
|
||||
original_sql="SELECT id FROM users LIMIT 5",
|
||||
)
|
||||
mock_db.session.query.return_value.filter_by.return_value.first.return_value = (
|
||||
mock_database
|
||||
)
|
||||
mock_security_manager.can_access_database.return_value = True
|
||||
|
||||
# No 'limit' key — should default to None (no override)
|
||||
request = {
|
||||
"database_id": 1,
|
||||
"sql": "SELECT id FROM users LIMIT 5",
|
||||
}
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool("execute_sql", {"request": request})
|
||||
|
||||
data = result.structured_content
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify limit=None was passed to QueryOptions (no override)
|
||||
call_args = mock_database.execute.call_args
|
||||
options = call_args[0][1]
|
||||
assert options.limit is None
|
||||
|
||||
@patch("superset.security_manager")
|
||||
@patch("superset.db")
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -366,6 +366,6 @@ def test_chart_filter_existing_columns_still_work():
|
||||
|
||||
def test_dashboard_filter_existing_columns_still_work():
|
||||
"""Test that pre-existing dashboard filter columns are not broken."""
|
||||
for col in ("dashboard_title", "published", "favorite"):
|
||||
for col in ("dashboard_title", "published", "created_by_fk"):
|
||||
f = DashboardFilter(col=col, opr="eq", value="test")
|
||||
assert f.col == col
|
||||
|
||||
@@ -95,10 +95,11 @@ def test_model_list_tool_basic():
|
||||
assert result.count == 2
|
||||
assert result.total_count == 2
|
||||
assert isinstance(result.items[0], DummyOutputSchema)
|
||||
assert result.page == 1
|
||||
# run_tool receives 0-based page; response reports 1-based (page+1)
|
||||
assert result.page == 2
|
||||
assert result.page_size == 2
|
||||
assert result.total_pages == 1
|
||||
# For page=1, ModelListCore sets has_previous=True
|
||||
# For page=1 (0-based), ModelListCore sets has_previous=True
|
||||
assert result.has_previous is True
|
||||
assert result.has_next is False
|
||||
assert result.columns_requested == ["id", "name"]
|
||||
|
||||
Reference in New Issue
Block a user