Files
semantica/tests/explorer/test_ontology_subissue3.py
T
63acc7a66e fix(ontology): address Qodo automated review findings from PR #524
Backend (semantica/explorer/routes/ontology.py):
- suggest-alignments: add TF-IDF character-ngram embeddings via sklearn
  (SimilarityCalculator-compatible cosine scoring) so embedding_similarity
  is populated in results; combined score = 0.4*label + 0.6*embedding when
  available, falling back to label-only when sklearn is absent
- suggest-alignments: add token-overlap prefilter before SequenceMatcher so
  zero-Jaccard pairs are skipped without computing full similarity; add
  _MAX_ENTITIES_PER_SIDE=500 per-ontology cap on top of the existing
  _MAX_ANALYSIS_NODES global cap
- suggest-alignments: remove dead try/except OntologyEngine.create_alignment
  block that always failed silently (no TripletStore configured); replace
  with a comment explaining the intentional ephemeral-only storage model
- health: replace O(alignments x entities) any() scans for alignment coverage
  with O(1) set membership checks via assessed_ids
- shacl/validate: run rdflib.Graph().parse(format='turtle') syntax check on
  the submitted Turtle before returning; invalid syntax now raises 422 instead
  of returning a misleading unavailable/success response

Frontend:
- AlignmentsTab: add pairwise alignment matrix section that groups recorded
  alignments by (source_ontology, target_ontology) pair; each cell shows
  color-coded relation badges per RELATION_COLORS; clicking a badge populates
  the create/edit form for quick editing; matrix is shown when at least two
  ontologies are loaded
- ShaclStudio: add selectedShapeId state and fullShacl ref; each shape row in
  the library is now a clickable button that extracts its Turtle block from
  the full SHACL and pre-populates the Monaco editor; a "View all" toggle
  restores the full SHACL; selected shape ID is shown in the editor header
- GraphWorkspace: fix viewMode race in external focus effect — call
  setSelectedNodeId directly instead of going through focusNode(), which
  captured a stale viewMode in its closure; remove focusNode from the
  dependency array since it is no longer called

Tests (14 passing, was 11):
- Add test_suggest_alignments_returns_embedding_similarity: asserts
  embedding_similarity is non-null when sklearn is available
- Add test_shacl_validate_rejects_invalid_turtle_syntax: asserts 422 on
  syntactically invalid Turtle
- Add test_health_alignment_coverage_uses_set_lookup: asserts alignment
  dimension score is non-zero after recording an alignment, verifying the
  O(1) set lookup path works correctly end-to-end

Co-authored-by: KaifAhmad1 <mohammadk78600@gmail.com>
Co-authored-by: ZohaibHassan16 <zohaib@hawksight.ai>
2026-05-02 16:38:09 +05:30

264 lines
9.5 KiB
Python

"""Tests for Ontology Hub subissue 3 APIs."""
import pytest
from semantica.context.context_graph import ContextGraph
from semantica.explorer.app import create_app
from semantica.explorer.session import GraphSession
try:
from starlette.testclient import TestClient
except ImportError:
pytest.skip(
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
allow_module_level=True,
)
def _build_ontology_graph() -> ContextGraph:
graph = ContextGraph(advanced_analytics=False)
onto_a = "http://example.org/onto-a"
onto_b = "http://example.org/onto-b"
person_a = "http://example.org/onto-a#Person"
person_b = "http://example.org/onto-b#PersonRecord"
name_a = "http://example.org/onto-a#name"
graph.add_node(
onto_a,
node_type="owl:Ontology",
content="Ontology A",
**{"rdfs:label": "Ontology A", "rdfs:comment": "Primary ontology", "version": "1.0.0"},
)
graph.add_node(
onto_b,
node_type="owl:Ontology",
content="Ontology B",
**{"rdfs:label": "Ontology B", "rdfs:comment": "Partner ontology", "version": "1.0.0"},
)
graph.add_node(
person_a,
node_type="owl:Class",
content="Person",
scheme_uri=onto_a,
**{"rdfs:label": "Person", "rdfs:comment": "A person", "skos:definition": "Human actor"},
)
graph.add_node(
name_a,
node_type="owl:DatatypeProperty",
content="name",
scheme_uri=onto_a,
**{"rdfs:label": "name", "rdfs:comment": "Display name"},
)
graph.add_node(
person_b,
node_type="owl:Class",
content="Person Record",
scheme_uri=onto_b,
**{"rdfs:label": "Person Record", "rdfs:comment": "A person profile"},
)
graph.add_edge(name_a, person_a, edge_type="rdfs:domain")
return graph
@pytest.fixture()
def client():
app = create_app(session=GraphSession(_build_ontology_graph()))
with TestClient(app) as test_client:
yield test_client
def test_alignment_round_trip(client):
payload = {
"source_uri": "http://example.org/onto-a#Person",
"target_uri": "http://example.org/onto-b#PersonRecord",
"relation": "owl:equivalentClass",
"confidence": 0.91,
"provenance": "Reviewed from source mapping table",
"source": "test",
"reviewer": "qa",
}
created = client.post("/api/ontology/alignments", json=payload)
assert created.status_code == 200
alignment = created.json()
assert alignment["confidence"] == 0.91
assert alignment["provenance"] == "Reviewed from source mapping table"
listed = client.get("/api/ontology/alignments")
assert listed.status_code == 200
assert [item["id"] for item in listed.json()] == [alignment["id"]]
removed = client.delete(f"/api/ontology/alignments?id={alignment['id']}")
assert removed.status_code == 200
assert client.get("/api/ontology/alignments").json() == []
def test_alignment_suggestions_are_ranked(client):
response = client.post(
"/api/ontology/suggest-alignments",
json={
"source_ontology_uri": "http://example.org/onto-a",
"target_ontology_uri": "http://example.org/onto-b",
"threshold": 0.35,
"limit": 5,
},
)
assert response.status_code == 200
suggestions = response.json()
assert suggestions
# Top suggestion should be the Person→PersonRecord pair (highest label similarity).
top = suggestions[0]
assert "Person" in top["source_label"]
assert "Person" in top["target_label"]
# Results must be sorted descending by score.
assert suggestions == sorted(suggestions, key=lambda item: item["score"], reverse=True)
def test_health_returns_dimensions_and_issues(client):
response = client.get("/api/ontology/health?uri=http%3A%2F%2Fexample.org%2Fonto-a")
assert response.status_code == 200
payload = response.json()
assert payload["total_score"] >= 0
assert {dimension["key"] for dimension in payload["dimensions"]} == {
"completeness",
"consistency",
"shacl",
"alignment",
"documentation",
}
assert isinstance(payload["issues"], list)
def test_shacl_generate_and_shapes(client):
response = client.post(
"/api/ontology/shacl/generate",
json={"uri": "http://example.org/onto-a", "quality_tier": "strict"},
)
assert response.status_code == 200
payload = response.json()
assert "sh:NodeShape" in payload["shacl_turtle"]
assert payload["shape_count"] >= 1
shapes = client.get("/api/ontology/shacl/shapes?uri=http%3A%2F%2Fexample.org%2Fonto-a")
assert shapes.status_code == 200
assert shapes.json()["shapes"]
def test_shacl_validate_returns_unavailable(client):
response = client.post(
"/api/ontology/shacl/validate",
json={
"uri": "http://example.org/onto-a",
"shacl_turtle": "@prefix sh: <http://www.w3.org/ns/shacl#> .",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "unavailable", "stub must not report conforms=True before validation is wired"
assert payload["conforms"] is False
assert isinstance(payload["violations"], list)
def test_shacl_validate_rejects_empty_turtle(client):
response = client.post(
"/api/ontology/shacl/validate",
json={"uri": "http://example.org/onto-a", "shacl_turtle": " "},
)
assert response.status_code == 422
def test_health_returns_404_for_unknown_ontology(client):
response = client.get("/api/ontology/health?uri=http%3A%2F%2Fnot-loaded.example%2Fonto")
assert response.status_code == 404
def test_health_shacl_dimension_is_zero_when_unavailable(client):
payload = client.get("/api/ontology/health?uri=http%3A%2F%2Fexample.org%2Fonto-a").json()
shacl_dim = next(d for d in payload["dimensions"] if d["key"] == "shacl")
assert shacl_dim["status"] == "unavailable"
assert shacl_dim["score"] == 0.0
# Total score must NOT include the unavailable dimension in its average.
scoreable = [d for d in payload["dimensions"] if d["status"] != "unavailable"]
expected_total = round(sum(d["score"] for d in scoreable) / len(scoreable), 1)
assert payload["total_score"] == expected_total
def test_delete_unknown_alignment_returns_404(client):
response = client.delete("/api/ontology/alignments?id=does-not-exist")
assert response.status_code == 404
def test_alignment_upsert_is_idempotent(client):
payload = {
"source_uri": "http://example.org/onto-a#Person",
"target_uri": "http://example.org/onto-b#PersonRecord",
"relation": "owl:equivalentClass",
"confidence": 0.80,
}
first = client.post("/api/ontology/alignments", json=payload).json()
updated_payload = {**payload, "confidence": 0.95}
second = client.post("/api/ontology/alignments", json=updated_payload).json()
assert first["id"] == second["id"], "upsert must reuse the same deterministic ID"
assert second["confidence"] == 0.95
assert second["created_at"] == first["created_at"], "created_at must not change on update"
listed = client.get("/api/ontology/alignments").json()
assert len(listed) == 1
def test_alignment_accepts_external_uri(client):
payload = {
"source_uri": "http://example.org/onto-a#Person",
"target_uri": "http://schema.org/Person", # not in local graph
"relation": "owl:equivalentClass",
"confidence": 0.75,
}
response = client.post("/api/ontology/alignments", json=payload)
assert response.status_code == 200
alignment = response.json()
assert alignment["target_label"] == "Person" # derived from URI fragment
def test_suggest_alignments_returns_embedding_similarity(client):
response = client.post(
"/api/ontology/suggest-alignments",
json={
"source_ontology_uri": "http://example.org/onto-a",
"target_ontology_uri": "http://example.org/onto-b",
"threshold": 0.20,
"limit": 10,
},
)
assert response.status_code == 200
suggestions = response.json()
assert suggestions
# When sklearn is available, embedding_similarity should be populated.
top = suggestions[0]
assert top["embedding_similarity"] is not None, (
"TF-IDF embedding similarity must be returned when sklearn is installed"
)
# Combined score must be a weighted blend, not purely the label score.
assert top["score"] != top["label_similarity"] or top["embedding_similarity"] == top["label_similarity"]
def test_shacl_validate_rejects_invalid_turtle_syntax(client):
response = client.post(
"/api/ontology/shacl/validate",
json={
"uri": "http://example.org/onto-a",
"shacl_turtle": "this is not valid turtle !!!",
},
)
assert response.status_code == 422
def test_health_alignment_coverage_uses_set_lookup(client):
# Create an alignment first so coverage score can be non-zero.
client.post("/api/ontology/alignments", json={
"source_uri": "http://example.org/onto-a#Person",
"target_uri": "http://example.org/onto-b#PersonRecord",
"relation": "owl:equivalentClass",
"confidence": 0.9,
})
payload = client.get("/api/ontology/health?uri=http%3A%2F%2Fexample.org%2Fonto-a").json()
alignment_dim = next(d for d in payload["dimensions"] if d["key"] == "alignment")
assert alignment_dim["score"] > 0.0, "alignment coverage must be non-zero after recording an alignment"