content-explorer/tests/test_contract.py
tegwick 072a7fa550 Freeze the v0.1 contract and the local ingest connector.
The snapshot and manifest schemas, both example dataspaces, and the shared text renderer now pass the fixture validator and the negative contract tests. JSON, NDJSON, and Markdown frontmatter publish a snapshot only when rights, identity, and references allow it. CONTENT-WP-0001 and CONTENT-WP-0003 are marked finished against those artifacts. CONTENT-WP-0000-T04 stays open. SCOPE.md and the 19:05 scope-against-intent note record the stop.

Assistant: grok
Assistant-Session: 01a130f1-abfb-7d71-9cd6-a8b75ec7f95a
2026-10-12 19:07:32 +02:00

147 lines
5.9 KiB
Python

"""Contract checks against the shipped validator, not a copy of it."""
from __future__ import annotations
import copy
import json
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
import sys
sys.path.insert(0, str(ROOT))
from content_explorer.contract import ( # noqa: E402
resolve_claims,
validate_dataspace,
validate_documents,
)
from content_explorer.render import render_explorer # noqa: E402
EXAMPLES = ROOT / "examples" / "dataspaces"
RENDERER = ROOT / "content_explorer" / "render.py"
def load_pair(name: str) -> tuple[dict, dict]:
directory = EXAMPLES / name
snapshot = json.loads((directory / "snapshot.json").read_text(encoding="utf-8"))
manifest = json.loads((directory / "manifest.json").read_text(encoding="utf-8"))
return snapshot, manifest
class PublishedFixtures(unittest.TestCase):
def test_each_example_passes_the_file_entry_point(self) -> None:
names = sorted(path.name for path in EXAMPLES.iterdir() if (path / "snapshot.json").is_file())
self.assertGreaterEqual(len(names), 2)
for name in names:
errors = validate_dataspace(EXAMPLES / name, require_size=True)
self.assertEqual(errors, [], name)
def test_renderer_switches_with_the_manifest(self) -> None:
systems, systems_manifest = load_pair("systems-thinking")
software, software_manifest = load_pair("software-architecture")
systems_view = render_explorer(systems, systems_manifest)
software_view = render_explorer(software, software_manifest)
switched = render_explorer(systems, software_manifest)
self.assertIn(systems_manifest["home"]["heading"], systems_view)
self.assertIn(software_manifest["home"]["heading"], software_view)
self.assertIn(software_manifest["home"]["heading"], switched)
self.assertNotIn(systems_manifest["home"]["heading"], switched)
self.assertNotIn(software_manifest["home"]["heading"], systems_view)
def test_renderer_source_has_no_domain_vocabulary(self) -> None:
source = RENDERER.read_text(encoding="utf-8").lower()
for word in (
"cancer",
"oncolog",
"feedback",
"coupling",
"ashby",
"meadows",
"alexander",
"systems-thinking",
"software-architecture",
"causal",
"cohesion",
):
self.assertNotIn(word, source, word)
class NegativeGraph(unittest.TestCase):
def setUp(self) -> None:
self.snapshot, self.manifest = load_pair("systems-thinking")
def test_duplicate_entity_id(self) -> None:
snapshot = copy.deepcopy(self.snapshot)
snapshot["entities"].append(copy.deepcopy(snapshot["entities"][0]))
errors = validate_documents(snapshot, self.manifest)
self.assertTrue(any("duplicate entity id" in error for error in errors))
def test_dangling_relation(self) -> None:
snapshot = copy.deepcopy(self.snapshot)
snapshot["relations"][0]["to"] = "urn:ce:systems-thinking:concept:missing"
errors = validate_documents(snapshot, self.manifest)
self.assertTrue(any("dangling to" in error for error in errors))
def test_missing_source(self) -> None:
snapshot = copy.deepcopy(self.snapshot)
snapshot["entities"][0]["sourceRefs"] = ["src:missing"]
errors = validate_documents(snapshot, self.manifest)
self.assertTrue(any("missing source src:missing" in error for error in errors))
def test_taxonomic_cycle_fails_and_other_cycles_do_not(self) -> None:
snapshot = copy.deepcopy(self.snapshot)
left = snapshot["entities"][0]["id"]
right = snapshot["entities"][1]["id"]
source = snapshot["sources"][0]["id"]
snapshot["relations"].extend([
{
"id": "cycle-a",
"predicate": "related-to",
"from": left,
"to": right,
"sourceRefs": [source],
},
{
"id": "cycle-b",
"predicate": "related-to",
"from": right,
"to": left,
"sourceRefs": [source],
},
])
manifest = copy.deepcopy(self.manifest)
manifest["relationLabels"]["related-to"] = {"forward": "Related to", "inverse": "Related to"}
manifest["acyclicPredicates"] = []
open_cycle = validate_documents(snapshot, manifest)
self.assertFalse(any("taxonomic cycle" in error for error in open_cycle))
manifest["acyclicPredicates"] = ["related-to"]
closed = validate_documents(snapshot, manifest)
self.assertTrue(any("taxonomic cycle" in error for error in closed))
class FactPolicy(unittest.TestCase):
def test_missing_unknown_and_conflict(self) -> None:
claims = [
{"key": "measure", "status": "unknown", "value": None},
{"key": "name", "status": "asserted", "value": "one"},
{"key": "name", "status": "asserted", "value": "two"},
]
self.assertEqual(resolve_claims(claims, "absent", "show-all")["state"], "missing")
self.assertEqual(resolve_claims(claims, "measure", "show-all")["state"], "unknown")
shown = resolve_claims(claims, "name", "show-all")
self.assertEqual(shown["state"], "contradicted")
self.assertEqual(shown["values"], ["one", "two"])
withheld = resolve_claims(claims, "name", "withhold")
self.assertEqual(withheld["values"], [])
preferred_claims = [
{"key": "name", "status": "asserted", "value": "one", "preferred": True},
{"key": "name", "status": "asserted", "value": "two"},
]
chosen = resolve_claims(preferred_claims, "name", "preferred")
self.assertEqual(chosen, {"state": "present", "values": ["one"]})
if __name__ == "__main__":
unittest.main()