Skip to content

Commit 5e820d4

Browse files
test: add tests for insights router, code endpoint, and related specs
Covers dashboard, plot-of-the-day, related specs (spec/full modes), code lazy-loading endpoint with cache hit/miss/404 scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 57108ab commit 5e820d4

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed

tests/unit/api/test_routers.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nplt.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

Comments
 (0)