Files
ss-tools/backend/tests/plugins/test_llm_analysis_topology.py

520 lines
19 KiB
Python

# #region Test.DashboardTopology [C:3] [TYPE Module] [SEMANTICS test,topology,dashboard,superset]
# @BRIEF Comprehensive tests for _topology.py — chart name/params, topology building, layout parsing.
# @RELATION BINDS_TO -> [DashboardTopology]
# @TEST_EDGE: chart_name_fallback -> Falls back to title or "Chart id" when slice_name missing
# @TEST_EDGE: params_string_parse -> String params parsed, invalid string returns {}
# @TEST_EDGE: describe_empty_optional -> No metrics/groupby/filters returns empty string
# @TEST_EDGE: resolve_child_string -> String ref resolved from pos_json, missing ref returns None
# @TEST_EDGE: topology_without_grid -> No GRID/ROOT nodes falls back to flat iteration
# @TEST_EDGE: build_topology_string_posjson -> position_json as string is parsed
import json
from unittest.mock import MagicMock
# ── _chart_name ──
class TestChartName:
"""Verify _chart_name."""
def test_slice_name_used(self):
from src.plugins.llm_analysis._topology import _chart_name
assert _chart_name({"slice_name": "Revenue"}) == "Revenue"
def test_title_fallback(self):
from src.plugins.llm_analysis._topology import _chart_name
assert _chart_name({"title": "Chart Title"}) == "Chart Title"
def test_slice_name_overrides_title(self):
from src.plugins.llm_analysis._topology import _chart_name
assert _chart_name({"slice_name": "Actual", "title": "Fallback"}) == "Actual"
def test_id_fallback(self):
from src.plugins.llm_analysis._topology import _chart_name
result = _chart_name({"id": 42})
assert result == "Chart 42"
def test_empty_dict(self):
from src.plugins.llm_analysis._topology import _chart_name
result = _chart_name({})
assert result == "Chart None"
# ── _chart_params ──
class TestChartParams:
"""Verify _chart_params."""
def test_dict_params(self):
from src.plugins.llm_analysis._topology import _chart_params
result = _chart_params({"params": {"metrics": ["count"]}})
assert result == {"metrics": ["count"]}
def test_string_params(self):
from src.plugins.llm_analysis._topology import _chart_params
result = _chart_params({"params": '{"metrics": ["count"]}'})
assert result == {"metrics": ["count"]}
def test_invalid_string_params(self):
from src.plugins.llm_analysis._topology import _chart_params
result = _chart_params({"params": "not-json"})
assert result == {}
def test_empty_params(self):
from src.plugins.llm_analysis._topology import _chart_params
assert _chart_params({}) == {}
def test_none_params(self):
from src.plugins.llm_analysis._topology import _chart_params
# When params is None, returns None (params value as-is)
assert _chart_params({"params": None}) is None
# ── _describe_chart_dataset ──
class TestDescribeChartDataset:
"""Verify _describe_chart_dataset."""
def test_with_datasource_id(self):
from src.plugins.llm_analysis._topology import _describe_chart_dataset
result = _describe_chart_dataset({"datasource_id": 5}, {})
assert "Dataset: 5" in result
def test_datasource_id_in_params(self):
from src.plugins.llm_analysis._topology import _describe_chart_dataset
result = _describe_chart_dataset({}, {"datasource_id": 10})
assert "Dataset: 10" in result
def test_no_datasource(self):
from src.plugins.llm_analysis._topology import _describe_chart_dataset
assert _describe_chart_dataset({}, {}) == ""
# ── _describe_chart_metrics ──
class TestDescribeChartMetrics:
"""Verify _describe_chart_metrics."""
def test_dict_metrics(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
result = _describe_chart_metrics({"metrics": [{"label": "SUM(revenue)"}]})
assert "SUM(revenue)" in result
def test_string_metrics(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
result = _describe_chart_metrics({"metrics": ["count"]})
assert "count" in result
def test_dict_metric_expression_fallback(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
result = _describe_chart_metrics({"metrics": [{"expression": "SUM(x)"}]})
assert "SUM(x)" in result
def test_empty_metrics(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
assert _describe_chart_metrics({"metrics": []}) == ""
def test_missing_metrics(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
assert _describe_chart_metrics({}) == ""
def test_truncated_to_three(self):
from src.plugins.llm_analysis._topology import _describe_chart_metrics
metrics = [str(i) for i in range(10)]
result = _describe_chart_metrics({"metrics": metrics})
assert len(result.split(",")) <= 4 # "Metrics: " prefix + 3 items max
# ── _describe_chart_groupby ──
class TestDescribeChartGroupby:
"""Verify _describe_chart_groupby."""
def test_with_groupby(self):
from src.plugins.llm_analysis._topology import _describe_chart_groupby
result = _describe_chart_groupby({"groupby": ["category", "region"]})
assert "category" in result
assert "region" in result
def test_truncated_to_three(self):
from src.plugins.llm_analysis._topology import _describe_chart_groupby
result = _describe_chart_groupby({"groupby": ["a", "b", "c", "d"]})
assert "d" not in result
def test_empty_groupby(self):
from src.plugins.llm_analysis._topology import _describe_chart_groupby
assert _describe_chart_groupby({"groupby": []}) == ""
def test_missing_groupby(self):
from src.plugins.llm_analysis._topology import _describe_chart_groupby
assert _describe_chart_groupby({}) == ""
# ── _describe_chart_filters ──
class TestDescribeChartFilters:
"""Verify _describe_chart_filters."""
def test_full_filter(self):
from src.plugins.llm_analysis._topology import _describe_chart_filters
result = _describe_chart_filters({
"adhoc_filters": [
{"subject": "revenue", "operator": ">", "comparator": "1000"}
]
})
assert "revenue" in result
assert ">" in result
def test_filter_without_subject_comparator(self):
from src.plugins.llm_analysis._topology import _describe_chart_filters
result = _describe_chart_filters({
"adhoc_filters": [
{"clause": "having"}
]
})
assert "having" in result
def test_empty_filters(self):
from src.plugins.llm_analysis._topology import _describe_chart_filters
assert _describe_chart_filters({"adhoc_filters": []}) == ""
def test_missing_filters(self):
from src.plugins.llm_analysis._topology import _describe_chart_filters
assert _describe_chart_filters({}) == ""
def test_non_dict_filter(self):
from src.plugins.llm_analysis._topology import _describe_chart_filters
result = _describe_chart_filters({"adhoc_filters": ["raw_string"]})
assert result == "" # not a dict, no clause key
# ── _describe_chart ──
class TestDescribeChart:
"""Verify _describe_chart orchestration."""
def test_describe_integration(self):
from src.plugins.llm_analysis._topology import _describe_chart
chart = {
"slice_name": "Sales",
"viz_type": "bar",
"params": {"metrics": [{"label": "SUM(rev)"}]},
}
result = _describe_chart(chart)
assert 'Chart "Sales" (bar)' in result
assert "SUM(rev)" in result
def test_describe_minimal(self):
from src.plugins.llm_analysis._topology import _describe_chart
result = _describe_chart({"id": 99})
assert 'Chart "Chart 99" (unknown)' in result
# ── _sort_chart_key ──
class TestSortChartKey:
"""Verify _sort_chart_key."""
def test_numeric_key(self):
from src.plugins.llm_analysis._topology import _sort_chart_key
result = _sort_chart_key(("42", {}))
assert result == (0, 42)
def test_string_key(self):
from src.plugins.llm_analysis._topology import _sort_chart_key
result = _sort_chart_key(("chart_1", {}))
assert result == (1, "chart_1")
def test_non_numeric_key(self):
from src.plugins.llm_analysis._topology import _sort_chart_key
result = _sort_chart_key(("abc", {}))
assert result == (1, "abc")
# ── _resolve_child ──
class TestResolveChild:
"""Verify _resolve_child."""
def test_dict_ref_returns_as_is(self):
from src.plugins.llm_analysis._topology import _resolve_child
ref = {"type": "CHART"}
assert _resolve_child(ref, {}) is ref
def test_string_ref_found(self):
from src.plugins.llm_analysis._topology import _resolve_child
pos_json = {"key1": {"type": "CHART"}}
result = _resolve_child("key1", pos_json)
assert result == {"type": "CHART"}
def test_string_ref_not_found(self):
from src.plugins.llm_analysis._topology import _resolve_child
assert _resolve_child("missing", {}) is None
# ── _process_tab ──
class TestProcessTab:
"""Verify _process_tab."""
def test_tab_with_children(self):
from src.plugins.llm_analysis._topology import _process_tab
tab_node = {
"type": "TAB",
"meta": {"text": "Revenue Tab"},
"children": [
{"type": "CHART", "meta": {"chartId": 1}},
]
}
result = _process_tab(tab_node, depth=1, pos_json={}, chart_lookup={"1": {"slice_name": "Rev Chart"}})
assert len(result) == 2
assert "Revenue Tab" in result[0]
assert "Rev Chart" in result[1]
def test_tab_meta_label_fallback(self):
from src.plugins.llm_analysis._topology import _process_tab
tab_node = {"type": "TAB", "meta": {"label": "Tab Label"}, "children": []}
result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
assert "Tab Label" in result[0]
def test_tab_empty_meta(self):
from src.plugins.llm_analysis._topology import _process_tab
tab_node = {"children": []}
result = _process_tab(tab_node, depth=0, pos_json={}, chart_lookup={})
assert "Tab" in result[0]
# ── _process_chart ──
class TestProcessChart:
"""Verify _process_chart."""
def test_chart_found_in_lookup(self):
from src.plugins.llm_analysis._topology import _process_chart
child = {"meta": {"chartId": 5}}
chart_lookup = {"5": {"slice_name": "Test Chart", "viz_type": "line"}}
result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
assert len(result) == 1
assert "Test Chart" in result[0]
def test_chart_not_in_lookup(self):
from src.plugins.llm_analysis._topology import _process_chart
child = {"meta": {"chartId": 99}}
result = _process_chart(child, depth=0, chart_lookup={})
assert result == []
def test_chart_with_slice_id(self):
from src.plugins.llm_analysis._topology import _process_chart
child = {"meta": {"slice_id": 10}}
chart_lookup = {"10": {"slice_name": "Slice Chart"}}
result = _process_chart(child, depth=0, chart_lookup=chart_lookup)
assert len(result) == 1
def test_chart_missing_meta(self):
from src.plugins.llm_analysis._topology import _process_chart
result = _process_chart({}, depth=0, chart_lookup={"1": {}})
assert result == []
# ── _process_children ──
class TestProcessChildren:
"""Verify _process_children."""
def test_tab_child(self):
from src.plugins.llm_analysis._topology import _process_children
parent = {
"children": [
{"type": "TAB", "meta": {"text": "Tab1"}, "children": []}
]
}
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
assert len(result) == 1
assert "Tab1" in result[0]
def test_row_child(self):
from src.plugins.llm_analysis._topology import _process_children
parent = {
"children": [
{"type": "ROW", "children": [
{"type": "CHART", "meta": {"chartId": 1}}
]}
]
}
chart_lookup = {"1": {"slice_name": "C1"}}
result = _process_children(parent, depth=0, pos_json={}, chart_lookup=chart_lookup)
assert len(result) == 1
def test_unknown_type_skipped(self):
from src.plugins.llm_analysis._topology import _process_children
parent = {"children": [{"type": "HEADER", "meta": {}}]}
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
assert result == []
def test_child_ref_resolved(self):
from src.plugins.llm_analysis._topology import _process_children
parent = {"children": ["ref_key"]}
pos_json = {"ref_key": {"type": "CHART", "meta": {"chartId": 1}}}
chart_lookup = {"1": {"slice_name": "Ref C"}}
result = _process_children(parent, depth=0, pos_json=pos_json, chart_lookup=chart_lookup)
assert len(result) == 1
def test_child_ref_not_resolved(self):
from src.plugins.llm_analysis._topology import _process_children
parent = {"children": ["missing_key"]}
result = _process_children(parent, depth=0, pos_json={}, chart_lookup={})
assert result == []
def test_non_dict_parent(self):
from src.plugins.llm_analysis._topology import _process_children
result = _process_children("not_a_dict", depth=0, pos_json={}, chart_lookup={})
assert result == []
# ── _extract_topology_children ──
class TestExtractTopologyChildren:
"""Verify _extract_topology_children."""
def test_grid_nodes(self):
from src.plugins.llm_analysis._topology import _extract_topology_children
pos_json = {
"GRID_ID": {"type": "GRID", "children": [
{"type": "CHART", "meta": {"chartId": 1}}
]}
}
chart_lookup = {"1": {"slice_name": "GChart"}}
result = _extract_topology_children(pos_json, chart_lookup)
assert len(result) >= 1
def test_no_grid_fallback(self):
from src.plugins.llm_analysis._topology import _extract_topology_children
pos_json = {
"tab1": {"type": "TAB", "children": []},
"DASHBOARD_VERSION_KEY": "v2",
}
result = _extract_topology_children(pos_json, chart_lookup={})
assert isinstance(result, list)
def test_no_grid_skips_version_key(self):
from src.plugins.llm_analysis._topology import _extract_topology_children
pos_json = {"DASHBOARD_VERSION_KEY": "v2"}
result = _extract_topology_children(pos_json, chart_lookup={})
assert result == []
def test_empty_position_json(self):
from src.plugins.llm_analysis._topology import _extract_topology_children
assert _extract_topology_children({}, {}) == []
# ── build_dashboard_topology (integration) ──
class TestBuildDashboardTopology:
"""Verify build_dashboard_topology full function."""
def test_happy_path(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"id": 1,
"dashboard_title": "Sales Overview",
"position_json": {
"ROOT_ID": {
"type": "ROOT",
"children": [
{"type": "CHART", "meta": {"chartId": 10}}
]
}
},
}
charts = [{"id": 10, "slice_name": "Revenue", "viz_type": "big_number"}]
result = build_dashboard_topology(dashboard, charts)
assert 'Dashboard: "Sales Overview"' in result
assert "Revenue" in result
assert "Charts: 1 total" in result
def test_title_fallback(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {"title": "Alt Title", "position_json": {}}
result = build_dashboard_topology(dashboard, [])
assert "Alt Title" in result
def test_no_title_fallback(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {"id": 5, "position_json": {}}
result = build_dashboard_topology(dashboard, [])
assert "Dashboard 5" in result
def test_position_json_as_string(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "Test",
"position_json": json.dumps({
"ROOT_ID": {"type": "ROOT", "children": []}
}),
}
result = build_dashboard_topology(dashboard, [])
assert "Test" in result
def test_position_json_invalid_string(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "Broken",
"position_json": "not-valid-json{{",
}
result = build_dashboard_topology(dashboard, [])
assert "Broken" in result # gracefully handles parse failure
def test_position_json_not_dict(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "NoPos",
"position_json": [],
}
result = build_dashboard_topology(dashboard, [])
assert "NoPos" in result
def test_chart_lookup_with_slice_id(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "Test",
"position_json": {},
}
charts = [{"slice_id": 1, "slice_name": "Slice Chart", "viz_type": "table"}]
result = build_dashboard_topology(dashboard, charts)
assert "Slice Chart" in result
def test_topology_parse_warning(self):
"""Exception during _extract_topology_children is caught and noted."""
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "Err",
"position_json": {
"ROOT_ID": {"type": "ROOT", "children": ["missing"]}
},
}
# missing key in position_json triggers no error since _resolve_child returns None
result = build_dashboard_topology(dashboard, [])
assert "Err" in result
def test_charts_sorted_by_key(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {
"dashboard_title": "Sorted",
"position_json": {},
}
charts = [
{"id": 10, "slice_name": "Z Chart"},
{"id": 5, "slice_name": "A Chart"},
]
result = build_dashboard_topology(dashboard, charts)
# Both appear
assert "A Chart" in result
assert "Z Chart" in result
def test_empty_charts(self):
from src.plugins.llm_analysis._topology import build_dashboard_topology
dashboard = {"dashboard_title": "Empty", "position_json": {}}
result = build_dashboard_topology(dashboard, [])
assert "Empty" in result
assert "Charts: 0 total" not in result # no chart_lookup when charts empty
# #endregion Test.DashboardTopology