"""test_structural_model.py — T18 §18E/§18F: structural Scaffold held-outs. Modeled on Scaffolding ITSELF (the 18O self-scaffold shape).""" from __future__ import annotations import sys from pathlib import Path import pytest REPO = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO / "src")) from rune import structural_model as sm # noqa: E402 @pytest.fixture() def model(tmp_path): m = sm.StructuralModel(tmp_path / "store", "scaffolding") # shape: project → domain → subsystem → component (Scaffolding itself) m.add("domain", "construction", parent="project:scaffolding") m.add("subsystem", "kernel", parent="domain:construction") m.add("component", "scaffold", parent="subsystem:kernel") m.add("component", "construction_interface", parent="subsystem:kernel") m.add("component", "construction_mode", parent="subsystem:kernel") m.add("component", "programme_continuation", parent="subsystem:kernel") m.add("component", "event_wait", parent="subsystem:kernel") m.add("interface", "construction-api", parent="subsystem:kernel") m.add("state_store", "scaffold-store", parent="project:scaffolding") m.add("test_suite", "kernel-tests", parent="project:scaffolding") m.add("deployment_surface", "rune-code-current", parent="project:scaffolding") m.add("environment", "production", parent="project:scaffolding") # relationships (Backstage + Kythe vocabulary) m.relate("component:construction_interface", "provides", "interface:construction-api") m.relate("component:scaffold", "consumes", "interface:construction-api") m.relate("component:programme_continuation", "consumes", "interface:construction-api") m.relate("component:scaffold", "depends_on", "component:construction_mode") m.relate("component:scaffold", "owns", "state_store:scaffold-store") m.relate("component:construction_interface", "reads", "state_store:scaffold-store") m.relate("component:construction_interface", "writes", "state_store:scaffold-store") m.relate("component:event_wait", "calls", "component:event_wait") if False else None m.relate("component:scaffold", "verified_by", "test_suite:kernel-tests") m.relate("component:programme_continuation", "verified_by", "test_suite:kernel-tests") m.relate("deployment_surface:rune-code-current", "connects_to", "environment:production") m.relate("component:scaffold", "deployed_by", "deployment_surface:rune-code-current") m.save() return m def test_18e_stable_deterministic_ids(model): assert model.get("component:scaffold") is not None # same name → same id, no duplicates (Backstage entity-ref discipline) model.add("component", "scaffold", evidence_ref="re-add") ids = [e["id"] for e in model.doc["entities"] if e["id"] == "component:scaffold"] assert len(ids) == 1 def test_18e_every_edge_carries_authority_status_evidence(model): for r in model.doc["relationships"]: assert r.get("evidence_refs"), r assert r.get("authority") in ("model-author", "importer", "user"), r assert "status" in r, r def test_18e_illegal_kind_and_rel_refused(model): with pytest.raises(ValueError): model.add("vibe", "x") with pytest.raises(ValueError): model.relate("component:scaffold", "vibes_with", "domain:construction") def test_18f_queries(model): assert model.children("domain:construction") == ["subsystem:kernel"] beneath = model.beneath("domain:construction") assert "component:scaffold" in beneath and "component:event_wait" in beneath assert model.owning_domain("component:scaffold") == "domain:construction" assert model.dependencies("component:scaffold") == [ "component:construction_mode"] assert set(model.consumers("interface:construction-api")) == { "component:scaffold", "component:programme_continuation"} assert model.state_owned("component:scaffold") == [ "state_store:scaffold-store"] assert model.canonical_state_owner("component:scaffold") == \ "state_store:scaffold-store" assert model.canonical_state_owner("state_store:scaffold-store") == \ "state_store:scaffold-store" sup = model.supersession("component:scaffold") assert sup == {"supersedes": [], "superseded_by": [], "legacy_of": []} def test_18f_impact_closure_shape(model): c = model.impact_closure("component:scaffold") assert c["target"] == "component:scaffold" assert c["owning_domain"] == "domain:construction" assert "component:scaffold" in c["mutation_scope"] assert "component:construction_mode" in c["observation_scope"][ "direct_dependencies"] assert "interface:construction-api" in c["observation_scope"][ "interfaces_consumed"] # sibling consumers of a shared interface are NOT in a component's # closure (changing scaffold doesn't change the interface) — the # ripple happens at the INTERFACE level (transitive test below) assert "component:programme_continuation" not in c["mutation_scope"] assert "test_suite:kernel-tests" in c["verification_scope"] assert "deployment_surface:rune-code-current" in c["deployment_scope"] assert c["invariants"] and isinstance(c["uncertainty"], list) # dynamic-consumer uncertainty is EXPLICIT, never silent (rule 18F) assert any(u["kind"] == "dynamic-consumer" for u in c["uncertainty"]) def test_18f_impact_closure_transitive_and_bounded(model): # chain: pc consumes iface; scaffold consumes iface → both in scope c = model.impact_closure("interface:construction-api") assert "component:scaffold" in c["mutation_scope"] assert "component:programme_continuation" in c["mutation_scope"] assert c["closure_depth"] <= 8 assert model.impact_closure("component:ghost").get("error") def test_18e_persistence_round_trip(tmp_path): m = sm.StructuralModel(tmp_path / "store", "scaffolding") m.add("domain", "d1", parent="project:scaffolding") m.save() m2 = sm.StructuralModel(tmp_path / "store", "scaffolding") assert m2.get("domain:d1") is not None assert m2.get("project:scaffolding") is not None # schema version stamped assert m2.doc.get("schema_version") == sm.SCHEMA_VERSION