Files
semantica/tests/export/test_jsonld_document_iri.py
T

135 lines
5.0 KiB
Python

"""The document IRI of a JSON-LD export must depend on content, not the clock
(issue #1147).
``_convert_kg_to_jsonld`` minted the graph's ``@id`` from ``utc_now_iso()``,
and the generic ``_attach_document_metadata`` path did the same for a plain
document ``@id``. Exporting an unchanged graph therefore produced a new
subject every time: three exports of one one-entity graph merged into 3
``semantica:KnowledgeGraph`` nodes and 15 triples for what should have been a
single graph. Neither identifier resolves and the timestamp is already
recorded correctly in ``semantica:exportedAt``, so the fix mints the IRI from
the exported content instead (mirroring ``mint_entity_iri``, #1109), with an
optional caller-supplied override for callers who already name their graphs.
"""
import json
from rdflib import RDF, Graph, URIRef
from semantica.export.json_exporter import JSONExporter
KG = {
"entities": [{"id": "https://example.org/e1", "text": "Acme Corp", "type": "ORG"}],
"relationships": [],
}
OTHER_KG = {
"entities": [
{"id": "https://example.org/e1", "text": "Acme Corp Renamed", "type": "ORG"}
],
"relationships": [],
}
def _export(kg, tmp_path, name="out.jsonld", **options):
path = tmp_path / name
JSONExporter().export_knowledge_graph(kg, path, format="json-ld", **options)
return path
def test_reexporting_an_unchanged_graph_is_idempotent(tmp_path):
"""The whole point of an identifier: same content, same @id."""
first = json.loads(_export(KG, tmp_path, "a.jsonld").read_text())
second = json.loads(_export(KG, tmp_path, "b.jsonld").read_text())
assert first["@id"] == second["@id"]
def test_a_changed_graph_gets_a_different_id(tmp_path):
unchanged = json.loads(_export(KG, tmp_path, "a.jsonld").read_text())
changed = json.loads(_export(OTHER_KG, tmp_path, "b.jsonld").read_text())
assert unchanged["@id"] != changed["@id"]
def test_merging_repeated_exports_yields_one_graph_node(tmp_path):
"""Regression for the exact repro in #1147: churn no longer multiplies nodes."""
merged = Graph()
for i in range(3):
path = _export(KG, tmp_path, f"churn{i}.jsonld")
merged.parse(str(path), format="json-ld")
# Exactly one subject typed as a KnowledgeGraph, regardless of how many
# times the unchanged graph was exported and merged.
kg_nodes = set(
merged.subjects(RDF.type, URIRef("https://semantica.dev/ns#KnowledgeGraph"))
)
assert len(kg_nodes) == 1
entity_nodes = set(
merged.subjects(RDF.type, URIRef("https://semantica.dev/vocab/ORG"))
)
assert len(entity_nodes) == 1
def test_exported_at_still_varies_between_exports(tmp_path):
"""Identity is now content-derived, but provenance still records each run."""
first = json.loads(_export(KG, tmp_path, "a.jsonld").read_text())
second = json.loads(_export(KG, tmp_path, "b.jsonld").read_text())
assert first["@id"] == second["@id"]
assert first["semantica:exportedAt"] != second["semantica:exportedAt"]
def test_caller_supplied_graph_uri_is_honored(tmp_path):
path = _export(KG, tmp_path, graph_uri="https://example.org/my-graph")
document = json.loads(path.read_text())
assert document["@id"] == "https://example.org/my-graph"
def _document_node_id(document):
"""The generic (non-knowledge-graph) path hangs its own @id off a member
of @graph rather than the top level, to avoid re-creating the named-graph
bug fixed by #1145. Find that member and return its @id."""
for node in document["@graph"]:
if "semantica:exportedAt" in node:
return node["@id"]
raise AssertionError(f"no document metadata node in @graph: {document}")
def test_caller_supplied_document_uri_is_honored_for_a_generic_export(tmp_path):
payload = {"note": "no entities or relationships here"}
path = tmp_path / "generic.jsonld"
JSONExporter().export(
payload, path, format="json-ld", document_uri="https://example.org/my-doc"
)
document = json.loads(path.read_text())
assert _document_node_id(document) == "https://example.org/my-doc"
def test_generic_document_id_is_also_content_derived(tmp_path):
"""The non-knowledge-graph path (_attach_document_metadata) gets the same fix."""
payload = {"note": "plain data, no @id of its own"}
first = tmp_path / "a.jsonld"
second = tmp_path / "b.jsonld"
JSONExporter().export(payload, first, format="json-ld")
JSONExporter().export(dict(payload), second, format="json-ld")
first_id = _document_node_id(json.loads(first.read_text()))
second_id = _document_node_id(json.loads(second.read_text()))
assert first_id == second_id
def test_document_id_still_differs_for_different_generic_payloads(tmp_path):
a = tmp_path / "a.jsonld"
b = tmp_path / "b.jsonld"
JSONExporter().export({"note": "one"}, a, format="json-ld")
JSONExporter().export({"note": "two"}, b, format="json-ld")
a_id = _document_node_id(json.loads(a.read_text()))
b_id = _document_node_id(json.loads(b.read_text()))
assert a_id != b_id