mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Address review: harden analytics status codes, add failure-path tests
207 alone is indistinguishable from 200 to callers that only check response.ok, so /api/analytics now raises 500 when every requested metric fails and reserves 207 for genuine partial failure. Adds regression tests for the temporal, analytics, and ontology-create failure paths introduced in this PR, and logs the fix in the changelog's Unreleased section.
This commit is contained in:
@@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Explorer backend routes returned HTTP 200 with error/empty bodies on failure, defeating frontend error handling** (#770, #787) by @Sameer6305 and @KaifAhmad1
|
||||
- `GET /api/temporal/patterns` now raises `HTTPException(500)` on a genuine computation failure instead of silently returning an empty-but-valid `TemporalPatternResponse`; the `ImportError` fallback (optional `kg` extra not installed) is unchanged and still degrades gracefully to an empty list
|
||||
- `POST /api/ontology/create` now raises `HTTPException(500)` when ontology generation fails in either the `sample_data` or `schema_text` mode, instead of silently falling back to a partial/minimal ontology with a misleading `nodes_added` count
|
||||
- `GET /api/analytics` sets `response.status_code = 207` (Multi-Status) when some, but not all, of the requested metrics fail, and raises `HTTPException(500)` when every requested metric fails — a plain 2xx (including 207) reads as success to callers that only check `response.ok`, so an all-failed request now surfaces as a hard error rather than a body full of `{"error": ...}`
|
||||
- Added regression tests covering all three failure paths (`test_patterns_failure_returns_500`, `test_analytics_partial_failure_returns_207`, `test_analytics_total_failure_returns_500`, and two `TestOntologyCreateFailures` cases)
|
||||
|
||||
## [0.6.0] - 2026-07-21
|
||||
|
||||
### Added
|
||||
|
||||
@@ -5,7 +5,7 @@ Analytics routes for graph metrics and validation.
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||
|
||||
from ..dependencies import get_session
|
||||
from ..schemas import AnalyticsResponse, ValidationIssue, ValidationReportResponse
|
||||
@@ -26,9 +26,11 @@ async def get_analytics(
|
||||
requested = set((metrics or "centrality,community,connectivity").split(","))
|
||||
graph_dict = await asyncio.to_thread(session.build_graph_dict)
|
||||
result: dict = {}
|
||||
any_failed = False
|
||||
attempted = 0
|
||||
failed = 0
|
||||
|
||||
if "centrality" in requested and session.centrality is not None:
|
||||
attempted += 1
|
||||
try:
|
||||
result["centrality"] = await asyncio.to_thread(
|
||||
session.centrality.calculate_degree_centrality,
|
||||
@@ -36,9 +38,10 @@ async def get_analytics(
|
||||
)
|
||||
except Exception as exc:
|
||||
result["centrality"] = {"error": str(exc)}
|
||||
any_failed = True
|
||||
failed += 1
|
||||
|
||||
if "community" in requested and session.community is not None:
|
||||
attempted += 1
|
||||
try:
|
||||
result["community"] = await asyncio.to_thread(
|
||||
session.community.detect_communities,
|
||||
@@ -46,9 +49,10 @@ async def get_analytics(
|
||||
)
|
||||
except Exception as exc:
|
||||
result["community"] = {"error": str(exc)}
|
||||
any_failed = True
|
||||
failed += 1
|
||||
|
||||
if "connectivity" in requested and session.connectivity is not None:
|
||||
attempted += 1
|
||||
try:
|
||||
result["connectivity"] = await asyncio.to_thread(
|
||||
session.connectivity.analyze_connectivity,
|
||||
@@ -56,9 +60,15 @@ async def get_analytics(
|
||||
)
|
||||
except Exception as exc:
|
||||
result["connectivity"] = {"error": str(exc)}
|
||||
any_failed = True
|
||||
failed += 1
|
||||
|
||||
if any_failed:
|
||||
if attempted and failed == attempted:
|
||||
# Every requested metric raised: a plain 2xx (even 207) reads as
|
||||
# success to callers that only check `response.ok`, so surface this
|
||||
# as a hard failure rather than a body full of {"error": ...}.
|
||||
raise HTTPException(status_code=500, detail="All requested analytics metrics failed to compute")
|
||||
|
||||
if failed:
|
||||
response.status_code = 207
|
||||
|
||||
return AnalyticsResponse(**result)
|
||||
|
||||
@@ -474,6 +474,16 @@ class TestTemporal:
|
||||
assert response.status_code == 200
|
||||
assert "patterns" in response.json()
|
||||
|
||||
def test_patterns_failure_returns_500(self, client, monkeypatch):
|
||||
session = client.app.state.session
|
||||
|
||||
def _boom():
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(session, "build_graph_dict", _boom)
|
||||
response = client.get("/api/temporal/patterns")
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_bounds(self, client):
|
||||
response = client.get("/api/temporal/bounds")
|
||||
assert response.status_code == 200
|
||||
@@ -488,6 +498,29 @@ class TestAnalytics:
|
||||
assert response.status_code == 200
|
||||
assert "centrality" in response.json()
|
||||
|
||||
def test_analytics_partial_failure_returns_207(self, client, monkeypatch):
|
||||
session = client.app.state.session
|
||||
|
||||
def _boom(*_args, **_kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(session.centrality, "calculate_degree_centrality", _boom)
|
||||
response = client.get("/api/analytics?metrics=centrality,community")
|
||||
assert response.status_code == 207
|
||||
payload = response.json()
|
||||
assert payload["centrality"]["error"]
|
||||
assert payload["community"] is not None and "error" not in payload["community"]
|
||||
|
||||
def test_analytics_total_failure_returns_500(self, client, monkeypatch):
|
||||
session = client.app.state.session
|
||||
|
||||
def _boom(*_args, **_kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(session.centrality, "calculate_degree_centrality", _boom)
|
||||
response = client.get("/api/analytics?metrics=centrality")
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_validation(self, client):
|
||||
response = client.get("/api/analytics/validation")
|
||||
assert response.status_code == 200
|
||||
@@ -496,6 +529,44 @@ class TestAnalytics:
|
||||
assert "issues" in payload
|
||||
|
||||
|
||||
class TestOntologyCreateFailures:
|
||||
def test_create_from_sample_data_failure_returns_500(self, client, monkeypatch):
|
||||
from semantica.ontology import OntologyEngine
|
||||
|
||||
def _boom(self, *_args, **_kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(OntologyEngine, "from_data", _boom)
|
||||
response = client.post(
|
||||
"/api/ontology/create",
|
||||
json={
|
||||
"mode": "data",
|
||||
"namespace": "http://example.org/create-failure-data",
|
||||
"name": "Create Failure (data)",
|
||||
"sample_data": "id,name\n1,Alice\n2,Bob",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
|
||||
def test_create_from_text_failure_returns_500(self, client, monkeypatch):
|
||||
from semantica.ontology import OntologyEngine
|
||||
|
||||
def _boom(self, *_args, **_kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(OntologyEngine, "from_text", _boom)
|
||||
response = client.post(
|
||||
"/api/ontology/create",
|
||||
json={
|
||||
"mode": "text",
|
||||
"namespace": "http://example.org/create-failure-text",
|
||||
"name": "Create Failure (text)",
|
||||
"schema_text": "Class: Person\nProperty: knows",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500
|
||||
|
||||
|
||||
class TestEnrichment:
|
||||
def test_reasoning(self, client):
|
||||
response = client.post(
|
||||
|
||||
Reference in New Issue
Block a user