@@ -1377,3 +1377,248 @@ def test_image_matches_groups_style_no_match(self) -> None:
13771377 impl_lookup = {("scatter-basic" , "matplotlib" ): {"styling" : ["alpha-blending" ]}}
13781378 groups = [{"category" : "style" , "values" : ["minimal-chrome" ]}]
13791379 assert _image_matches_groups ("scatter-basic" , "matplotlib" , groups , spec_lookup , impl_lookup ) is False
1380+
1381+
1382+ class TestInsightsRouter :
1383+ """Tests for insights router."""
1384+
1385+ def test_dashboard_without_db (self , client : TestClient ) -> None :
1386+ """Dashboard should return 503 when DB not configured."""
1387+ with patch (DB_CONFIG_PATCH , return_value = False ):
1388+ response = client .get ("/insights/dashboard" )
1389+ assert response .status_code == 503
1390+
1391+ def test_dashboard_with_db (self , client : TestClient , mock_spec ) -> None :
1392+ """Dashboard should return aggregated stats."""
1393+ mock_spec_repo = MagicMock ()
1394+ mock_spec_repo .get_all = AsyncMock (return_value = [mock_spec ])
1395+ mock_impl_repo = MagicMock ()
1396+ mock_impl_repo .get_total_code_lines = AsyncMock (return_value = 500 )
1397+ mock_impl_repo .get_loc_per_impl = AsyncMock (return_value = [("matplotlib" , 50 )])
1398+
1399+ with (
1400+ patch (DB_CONFIG_PATCH , return_value = True ),
1401+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1402+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1403+ patch ("api.routers.insights.ImplRepository" , return_value = mock_impl_repo ),
1404+ ):
1405+ response = client .get ("/insights/dashboard" )
1406+ assert response .status_code == 200
1407+ data = response .json ()
1408+ assert data ["total_specs" ] == 1
1409+ assert data ["total_implementations" ] == 1
1410+ assert data ["total_lines_of_code" ] == 500
1411+ assert data ["total_interactive" ] == 0
1412+ assert len (data ["library_stats" ]) == 9
1413+ assert isinstance (data ["coverage_matrix" ], list )
1414+ assert isinstance (data ["score_distribution" ], dict )
1415+ assert isinstance (data ["tag_distribution" ], dict )
1416+
1417+ def test_potd_without_db (self , client : TestClient ) -> None :
1418+ """Plot of the day should return 503 when DB not configured."""
1419+ with patch (DB_CONFIG_PATCH , return_value = False ):
1420+ response = client .get ("/insights/plot-of-the-day" )
1421+ assert response .status_code == 503
1422+
1423+ def test_potd_with_db (self , client : TestClient , mock_spec ) -> None :
1424+ """Plot of the day should return a featured implementation."""
1425+ mock_spec_repo = MagicMock ()
1426+ mock_spec_repo .get_all = AsyncMock (return_value = [mock_spec ])
1427+ mock_impl = MagicMock ()
1428+ mock_impl .code = "import matplotlib"
1429+ mock_impl .review_image_description = "A scatter plot"
1430+ mock_impl_repo = MagicMock ()
1431+ mock_impl_repo .get_by_spec_and_library = AsyncMock (return_value = mock_impl )
1432+
1433+ with (
1434+ patch (DB_CONFIG_PATCH , return_value = True ),
1435+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1436+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1437+ patch ("api.routers.insights.ImplRepository" , return_value = mock_impl_repo ),
1438+ ):
1439+ response = client .get ("/insights/plot-of-the-day" )
1440+ assert response .status_code == 200
1441+ data = response .json ()
1442+ assert data ["spec_id" ] == "scatter-basic"
1443+ assert data ["library_id" ] == "matplotlib"
1444+ assert data ["quality_score" ] == 92.5
1445+
1446+ def test_potd_no_candidates (self , client : TestClient ) -> None :
1447+ """Plot of the day should return null when no high-quality implementations."""
1448+ mock_impl = MagicMock ()
1449+ mock_impl .library_id = "matplotlib"
1450+ mock_impl .quality_score = 50.0 # Below threshold
1451+ mock_impl .preview_url = TEST_IMAGE_URL
1452+ mock_spec = MagicMock ()
1453+ mock_spec .id = "low-quality"
1454+ mock_spec .impls = [mock_impl ]
1455+
1456+ mock_spec_repo = MagicMock ()
1457+ mock_spec_repo .get_all = AsyncMock (return_value = [mock_spec ])
1458+ mock_impl_repo = MagicMock ()
1459+
1460+ with (
1461+ patch (DB_CONFIG_PATCH , return_value = True ),
1462+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1463+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1464+ patch ("api.routers.insights.ImplRepository" , return_value = mock_impl_repo ),
1465+ ):
1466+ response = client .get ("/insights/plot-of-the-day" )
1467+ assert response .status_code == 200
1468+ assert response .json () is None
1469+
1470+ def test_related_without_db (self , client : TestClient ) -> None :
1471+ """Related should return 503 when DB not configured."""
1472+ with patch (DB_CONFIG_PATCH , return_value = False ):
1473+ response = client .get ("/insights/related/scatter-basic" )
1474+ assert response .status_code == 503
1475+
1476+ def test_related_spec_mode (self , client : TestClient ) -> None :
1477+ """Related in spec mode should return similar specs based on spec tags."""
1478+ mock_impl1 = MagicMock ()
1479+ mock_impl1 .library_id = "matplotlib"
1480+ mock_impl1 .quality_score = 90.0
1481+ mock_impl1 .preview_url = TEST_IMAGE_URL
1482+ mock_impl1 .impl_tags = {}
1483+
1484+ mock_spec1 = MagicMock ()
1485+ mock_spec1 .id = "scatter-basic"
1486+ mock_spec1 .title = "Basic Scatter"
1487+ mock_spec1 .tags = {"plot_type" : ["scatter" ], "domain" : ["statistics" ]}
1488+ mock_spec1 .impls = [mock_impl1 ]
1489+
1490+ mock_impl2 = MagicMock ()
1491+ mock_impl2 .library_id = "matplotlib"
1492+ mock_impl2 .quality_score = 88.0
1493+ mock_impl2 .preview_url = TEST_IMAGE_URL
1494+ mock_impl2 .impl_tags = {}
1495+
1496+ mock_spec2 = MagicMock ()
1497+ mock_spec2 .id = "scatter-regression"
1498+ mock_spec2 .title = "Scatter with Regression"
1499+ mock_spec2 .tags = {"plot_type" : ["scatter" ], "domain" : ["machine-learning" ]}
1500+ mock_spec2 .impls = [mock_impl2 ]
1501+
1502+ mock_spec_repo = MagicMock ()
1503+ mock_spec_repo .get_all = AsyncMock (return_value = [mock_spec1 , mock_spec2 ])
1504+
1505+ with (
1506+ patch (DB_CONFIG_PATCH , return_value = True ),
1507+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1508+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1509+ ):
1510+ response = client .get ("/insights/related/scatter-basic?mode=spec&limit=3" )
1511+ assert response .status_code == 200
1512+ data = response .json ()
1513+ assert len (data ["related" ]) == 1
1514+ assert data ["related" ][0 ]["id" ] == "scatter-regression"
1515+ assert data ["related" ][0 ]["similarity" ] > 0
1516+ assert "scatter" in data ["related" ][0 ]["shared_tags" ]
1517+
1518+ def test_related_not_found (self , client : TestClient ) -> None :
1519+ """Related should return empty list for nonexistent spec."""
1520+ mock_spec_repo = MagicMock ()
1521+ mock_spec_repo .get_all = AsyncMock (return_value = [])
1522+
1523+ with (
1524+ patch (DB_CONFIG_PATCH , return_value = True ),
1525+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1526+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1527+ ):
1528+ response = client .get ("/insights/related/nonexistent" )
1529+ assert response .status_code == 200
1530+ assert response .json ()["related" ] == []
1531+
1532+ def test_related_full_mode_with_library (self , client : TestClient ) -> None :
1533+ """Related in full mode should include impl tags."""
1534+ mock_impl1 = MagicMock ()
1535+ mock_impl1 .library_id = "matplotlib"
1536+ mock_impl1 .quality_score = 90.0
1537+ mock_impl1 .preview_url = TEST_IMAGE_URL
1538+ mock_impl1 .impl_tags = {"techniques" : ["annotations" ], "patterns" : ["data-generation" ]}
1539+
1540+ mock_spec1 = MagicMock ()
1541+ mock_spec1 .id = "scatter-basic"
1542+ mock_spec1 .title = "Basic Scatter"
1543+ mock_spec1 .tags = {"plot_type" : ["scatter" ]}
1544+ mock_spec1 .impls = [mock_impl1 ]
1545+
1546+ mock_impl2 = MagicMock ()
1547+ mock_impl2 .library_id = "matplotlib"
1548+ mock_impl2 .quality_score = 85.0
1549+ mock_impl2 .preview_url = TEST_IMAGE_URL
1550+ mock_impl2 .impl_tags = {"techniques" : ["annotations" ], "patterns" : ["other" ]}
1551+
1552+ mock_spec2 = MagicMock ()
1553+ mock_spec2 .id = "bar-annotated"
1554+ mock_spec2 .title = "Annotated Bar"
1555+ mock_spec2 .tags = {"plot_type" : ["bar" ]}
1556+ mock_spec2 .impls = [mock_impl2 ]
1557+
1558+ mock_spec_repo = MagicMock ()
1559+ mock_spec_repo .get_all = AsyncMock (return_value = [mock_spec1 , mock_spec2 ])
1560+
1561+ with (
1562+ patch (DB_CONFIG_PATCH , return_value = True ),
1563+ patch ("api.routers.insights.get_or_set_cache" , side_effect = _passthrough_cache ),
1564+ patch ("api.routers.insights.SpecRepository" , return_value = mock_spec_repo ),
1565+ ):
1566+ response = client .get ("/insights/related/scatter-basic?mode=full&library=matplotlib" )
1567+ assert response .status_code == 200
1568+ data = response .json ()
1569+ assert len (data ["related" ]) == 1
1570+ assert "annotations" in data ["related" ][0 ]["shared_tags" ]
1571+
1572+
1573+ class TestSpecCodeEndpoint :
1574+ """Tests for the /specs/{spec_id}/{library}/code endpoint."""
1575+
1576+ def test_code_without_db (self , client : TestClient ) -> None :
1577+ """Code endpoint should return 503 when DB not configured."""
1578+ with patch (DB_CONFIG_PATCH , return_value = False ):
1579+ response = client .get ("/specs/scatter-basic/matplotlib/code" )
1580+ assert response .status_code == 503
1581+
1582+ def test_code_with_db (self , client : TestClient ) -> None :
1583+ """Code endpoint should return code for a specific implementation."""
1584+ mock_impl = MagicMock ()
1585+ mock_impl .code = "import matplotlib.pyplot as plt\n plt.plot([1,2,3])"
1586+ mock_impl_repo = MagicMock ()
1587+ mock_impl_repo .get_by_spec_and_library = AsyncMock (return_value = mock_impl )
1588+
1589+ with (
1590+ patch (DB_CONFIG_PATCH , return_value = True ),
1591+ patch ("api.routers.specs.get_cache" , return_value = None ),
1592+ patch ("api.routers.specs.set_cache" ),
1593+ patch ("api.routers.specs.ImplRepository" , return_value = mock_impl_repo ),
1594+ ):
1595+ response = client .get ("/specs/scatter-basic/matplotlib/code" )
1596+ assert response .status_code == 200
1597+ data = response .json ()
1598+ assert data ["spec_id" ] == "scatter-basic"
1599+ assert data ["library" ] == "matplotlib"
1600+ assert "matplotlib" in data ["code" ]
1601+
1602+ def test_code_not_found (self , client : TestClient ) -> None :
1603+ """Code endpoint should return 404 when implementation not found."""
1604+ mock_impl_repo = MagicMock ()
1605+ mock_impl_repo .get_by_spec_and_library = AsyncMock (return_value = None )
1606+
1607+ with (
1608+ patch (DB_CONFIG_PATCH , return_value = True ),
1609+ patch ("api.routers.specs.get_cache" , return_value = None ),
1610+ patch ("api.routers.specs.ImplRepository" , return_value = mock_impl_repo ),
1611+ ):
1612+ response = client .get ("/specs/nonexistent/matplotlib/code" )
1613+ assert response .status_code == 404
1614+
1615+ def test_code_cache_hit (self , client : TestClient ) -> None :
1616+ """Code endpoint should return cached data when available."""
1617+ cached = {"spec_id" : "scatter-basic" , "library" : "matplotlib" , "code" : "cached code" }
1618+ with (
1619+ patch (DB_CONFIG_PATCH , return_value = True ),
1620+ patch ("api.routers.specs.get_cache" , return_value = cached ),
1621+ ):
1622+ response = client .get ("/specs/scatter-basic/matplotlib/code" )
1623+ assert response .status_code == 200
1624+ assert response .json ()["code" ] == "cached code"
0 commit comments