fix(explorer): resolve merge-artifact syntax errors and clean up all route files

app.py:
- Fix unclosed '(' in generic_error_handler (two implementations were merged,
  leaving the return JSONResponse( call with no closing paren)
- Remove duplicate 'from fastapi import FastAPI, Request' import
- Remove unused 'import traceback'
- Remove duplicate static file mount (was mounted twice: once conditionally,
  once unconditionally creating the dir — FastAPI raises on duplicate mounts)

decisions.py:
- Remove stub 'return ComplianceResponse(compliant=True)' with unclosed '('
  that was left in front of the real edge-scan implementation

temporal.py:
- Remove blocking get_nodes/get_edges calls (without asyncio.to_thread) that
  were left as dead code above the correct async versions
- Fix empty 'except Exception:' clause before 'except ImportError:' that
  caused a SyntaxError

tests/explorer/test_explorer_api.py:
- Remove all merge-artifact duplicate class definitions (TestAnalytics x2,
  TestReasoning x2, TestAnnotations x2) — Python silently used the second
  definition, hiding the first; collapsed into single canonical classes
- Fix test_snapshot_at referencing undefined 'body' (no request was made);
  merged its assertions into test_snapshot_now
- Fix test_compliance asserting isinstance(body, list) on a dict response;
  the displaced precedents-check code is now in test_precedents where it
  belongs
- Fix test_compliance_with_violation using wrong session reference
- Remove duplicate node-lookup and duplicate assertions throughout
- Add test_search_content_populated: asserts search results carry non-empty
  content (regression guard for the to_dict envelope fix)
- Add test_import_edge_metadata_preserved: asserts edge metadata survives the
  import round-trip (regression guard for the properties/metadata fallback fix)

All 51 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
KaifAhmad1
2026-03-20 00:18:08 +05:30
co-authored by Claude Sonnet 4.6
parent 7b6e74d042
commit ac047f917a
4 changed files with 46 additions and 93 deletions
-18
View File
@@ -6,12 +6,10 @@ static file serving, route registration, and WebSocket support.
"""
import os
import traceback
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, Request
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
@@ -33,12 +31,10 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
@asynccontextmanager
async def lifespan(app: FastAPI):
if session is not None:
app.state.session = session
app.state.ws_manager = ConnectionManager()
yield
app = FastAPI(
title="Semantica Knowledge Explorer",
@@ -47,7 +43,6 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
lifespan=lifespan,
)
cors_origins = os.environ.get("EXPLORER_CORS_ORIGINS", "*")
app.add_middleware(
CORSMiddleware,
@@ -57,7 +52,6 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
allow_headers=["*"],
)
@app.exception_handler(KeyError)
async def key_error_handler(request: Request, exc: KeyError):
return JSONResponse(
@@ -74,10 +68,6 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
tb = traceback.format_exc()
return JSONResponse(
status_code=500,
content={"detail": f"Internal Server Error: {exc}", "traceback": tb},
# Let FastAPI's built-in HTTPException handler take precedence so that
# responses from dependency injection (e.g. 503 from get_session) are
# not swallowed and converted to 500.
@@ -104,7 +94,6 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
app.include_router(export_import_router)
app.include_router(annotations_router)
from fastapi import WebSocket, WebSocketDisconnect
@app.websocket("/ws/graph-updates")
@@ -113,12 +102,10 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
await manager.connect(websocket)
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
@app.get("/api/health")
async def health():
return {"status": "healthy"}
@@ -131,14 +118,9 @@ def create_app(session: Optional[GraphSession] = None) -> FastAPI:
"status": "active",
}
static_dir = Path(__file__).resolve().parent.parent / "static"
if static_dir.is_dir():
from fastapi.staticfiles import StaticFiles
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
static_dir = Path(__file__).resolve().parent.parent / "static"
static_dir.mkdir(parents=True, exist_ok=True)
from fastapi.staticfiles import StaticFiles
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
return app
-9
View File
@@ -160,15 +160,6 @@ async def check_compliance(
raise KeyError(decision_id)
try:
from ...context.policy_engine import PolicyEngine
except ImportError:
pass
return ComplianceResponse(
decision_id=decision_id,
compliant=True,
violations=[],
edges, _ = await asyncio.to_thread(session.get_edges, skip=0, limit=999_999)
_VIOLATION_TYPES = {"violates", "non_compliant", "breaches"}
-3
View File
@@ -103,8 +103,6 @@ async def temporal_patterns(
detector = TemporalPatternDetector()
nodes, _ = session.get_nodes(skip=0, limit=999_999)
edges, _ = session.get_edges(skip=0, limit=999_999)
nodes, _ = await asyncio.to_thread(session.get_nodes, skip=0, limit=999_999)
edges, _ = await asyncio.to_thread(session.get_edges, skip=0, limit=999_999)
graph_dict = {
@@ -123,7 +121,6 @@ async def temporal_patterns(
if isinstance(patterns, dict):
patterns = patterns.get("patterns", [])
return TemporalPatternResponse(patterns=patterns if isinstance(patterns, list) else [])
except Exception:
except ImportError:
# TemporalPatternDetector is an optional KG extra; return empty gracefully.
return TemporalPatternResponse(patterns=[])
+46 -63
View File
@@ -22,7 +22,6 @@ except ImportError:
)
def _build_sample_graph() -> ContextGraph:
"""Create a small ContextGraph with a handful of nodes and edges."""
g = ContextGraph(advanced_analytics=False)
@@ -75,8 +74,6 @@ class TestHealthInfo:
body = r.json()
assert body["name"] == "Semantica Knowledge Explorer"
assert "version" in body
assert body["status"] == "active"
@@ -135,10 +132,6 @@ class TestGraphNodes:
assert len(body) >= 1
ids = [nb["id"] for nb in body]
assert "ml" in ids or "web_dev" in ids
# Each neighbour must have required fields
for nb in body:
assert "id" in nb and "type" in nb and "hop" in nb
@@ -172,10 +165,6 @@ class TestGraphEdges:
r = client.get("/api/graph/edges?source=python")
assert r.status_code == 200
body = r.json()
assert all(e["source"] == "python" for e in body["edges"])
assert len(body["edges"]) >= 1
assert all(e["source"] == "python" for e in body["edges"])
@@ -197,11 +186,21 @@ class TestSearchStats:
body = r.json()
assert body["query"] == "programming"
assert len(body["results"]) >= 1
# Each result must carry a node and a score
for item in body["results"]:
assert "node" in item and "score" in item
assert item["node"]["id"] # non-empty id
def test_search_content_populated(self, client):
"""Bug fix: search results must carry non-empty content (not empty string)."""
r = client.post("/api/graph/search", json={"query": "programming", "limit": 5})
assert r.status_code == 200
for item in r.json()["results"]:
node = item["node"]
assert node.get("content"), (
f"Node {node.get('id')!r} has empty content in search result — "
"node.to_dict() 'properties' envelope was not normalised"
)
def test_search_no_results(self, client):
r = client.post("/api/graph/search", json={"query": "zzznomatchzzz"})
assert r.status_code == 200
@@ -214,7 +213,6 @@ class TestSearchStats:
assert body["node_count"] >= 5
assert body["edge_count"] >= 3
assert "density" in body
assert "node_types" in body and "edge_types" in body
assert body["density"] >= 0.0
@@ -243,7 +241,6 @@ class TestDecisions:
def test_get_decision(self, client):
r = client.get("/api/decisions/decision_1")
assert r.status_code == 200
assert r.json()["decision_id"] == "decision_1"
body = r.json()
assert body["decision_id"] == "decision_1"
assert body["outcome"] == "approved"
@@ -262,16 +259,6 @@ class TestDecisions:
def test_precedents(self, client):
r = client.get("/api/decisions/decision_1/precedents")
assert r.status_code == 200
assert isinstance(r.json(), list)
def test_compliance(self, client):
r = client.get("/api/decisions/decision_1/compliance")
assert r.status_code == 200
body = r.json()
assert "compliant" in body
body = r.json()
assert isinstance(body, list)
# decision_2 shares category "tech" so should appear
@@ -289,8 +276,7 @@ class TestDecisions:
def test_compliance_with_violation(self, client):
"""Add a violation edge then check compliance detects it."""
# Add a policy node and a violates edge directly on the underlying graph
session = GraphSession(client.app.state.session.graph)
session = client.app.state.session
session.graph.add_node("policy_1", node_type="policy", content="Data policy")
session.graph.add_edge("decision_1", "policy_1", edge_type="violates")
@@ -301,10 +287,6 @@ class TestDecisions:
assert len(body["violations"]) >= 1
assert body["violations"][0]["policy_id"] == "policy_1"
# Clean up — remove the test edge so other tests are not affected
# (ContextGraph doesn't expose edge removal; re-create the session fixture
# to isolate: this test intentionally runs after all other decision tests)
# ---------------------------------------------------------------------------
# Temporal
@@ -316,8 +298,6 @@ class TestTemporal:
assert r.status_code == 200
body = r.json()
assert "active_node_count" in body
def test_snapshot_at(self, client):
assert "timestamp" in body
assert isinstance(body["active_nodes"], list)
@@ -343,16 +323,6 @@ class TestTemporal:
)
assert r.status_code == 200
body = r.json()
assert "added_nodes" in body
assert "removed_nodes" in body
class TestAnalytics:
def test_analytics(self, client):
r = client.get("/api/analytics")
assert r.status_code == 200
assert "added_nodes" in body and "removed_nodes" in body
# temporal_node became active between t1 and t2
assert "temporal_node" in body["added_nodes"]
@@ -381,7 +351,6 @@ class TestAnalytics:
def test_analytics_select_metric(self, client):
r = client.get("/api/analytics?metrics=centrality")
assert r.status_code == 200
# When KG extras are absent the key is still present but None; otherwise a dict
body = r.json()
assert "centrality" in body
@@ -390,11 +359,6 @@ class TestAnalytics:
assert r.status_code == 200
body = r.json()
assert "valid" in body
class TestReasoning:
def test_reason(self, client):
assert "error_count" in body and "warning_count" in body
assert isinstance(body["issues"], list)
@@ -413,16 +377,7 @@ class TestReasoning:
"mode": "forward",
},
)
assert r.status_code in (200, 422)
class TestAnnotations:
def test_create_and_list(self, client):
# 200 when reasoning module is available; 422 when it's not installed.
# Either is acceptable — the endpoint must not 500.
assert r.status_code in (200, 422), (
f"Unexpected status {r.status_code}: {r.text}"
)
@@ -572,10 +527,8 @@ class TestExport:
def test_export_json(self, client):
r = client.post("/api/export", json={"format": "json"})
assert r.status_code == 200
assert "json" in r.headers.get("content-type", "").lower() or len(r.content) > 0
ct = r.headers.get("content-type", "")
assert "json" in ct.lower()
# Payload must be valid JSON with entities and relationships
data = r.json()
assert "entities" in data and "relationships" in data
assert len(data["entities"]) >= 5
@@ -592,7 +545,6 @@ class TestExport:
assert r.status_code == 422
# ---------------------------------------------------------------------------
# Import
# ---------------------------------------------------------------------------
@@ -614,9 +566,6 @@ class TestImport:
assert body["status"] == "success"
assert body["nodes_added"] >= 1
r2 = client.get("/api/graph/node/imported_node")
assert r2.status_code == 200
# Verify the node is now in the graph
r2 = client.get("/api/graph/node/imported_node")
assert r2.status_code == 200
assert r2.json()["id"] == "imported_node"
@@ -639,6 +588,40 @@ class TestImport:
body = r.json()
assert body["status"] == "success"
assert body["nodes_added"] >= 2
assert body["edges_added"] >= 1
def test_import_edge_metadata_preserved(self, client):
"""Bug fix: edge metadata must survive the import round-trip."""
payload = json.dumps({
"nodes": [
{"id": "meta_src", "type": "test", "properties": {"content": "src"}},
{"id": "meta_tgt", "type": "test", "properties": {"content": "tgt"}},
],
"edges": [
{
"source": "meta_src",
"target": "meta_tgt",
"type": "tagged",
"metadata": {"label": "important", "weight": 0.7},
}
],
})
r = client.post(
"/api/import",
files={"file": ("meta_import.json", payload, "application/json")},
)
assert r.status_code == 200
assert r.json()["edges_added"] >= 1
# Retrieve the edge and verify metadata survived
r2 = client.get("/api/graph/edges?source=meta_src&target=meta_tgt")
assert r2.status_code == 200
edges = r2.json()["edges"]
assert len(edges) >= 1
props = edges[0].get("properties", {})
assert props.get("label") == "important", (
"Edge metadata dropped during import — add_edges() 'properties'/'metadata' fallback not working"
)
def test_import_unsupported_format(self, client):
r = client.post(