"""test_layout_contract.py — SCAFFOLD-TRANCHE-003 §29 test plan (A-L).""" import os import sys import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from rune import layout_contract as lc # noqa: E402 from rune import structural_store as ss # noqa: E402 def _doc(): doc = ss.new_doc() lc.define_region(doc, "header", order=0, orientation="horizontal", provenance="user", evidence_refs=["user-design"]) lc.define_region(doc, "main", order=1) lc.define_region(doc, "footer", order=2) lc.define_region(doc, "sidebar", parent="main", order=0) return doc # A. region creation/roundtrip def test_region_roundtrip(tmp_path): doc = _doc() ss.save(doc, tmp_path / "d.json") doc2 = ss.load(tmp_path / "d.json") r = ss.entity(doc2, "region:header") assert r["kind"] == "layout_region" assert r["identities"]["order"] == 0 assert r["identities"]["provenance"] == "user" # B. component identity survives implementation-path change def test_component_identity_survives_path_change(tmp_path): doc = _doc() lc.bind_component( doc, "version_display", "header", lock=True, provenance="user", order=2, evidence_ref="user-2026-09", implementation="src/tui/widgets/version.py", ) ss.save(doc, tmp_path / "d.json") doc2 = ss.load(tmp_path / "d.json") e = ss.entity(doc2, "layout:ui:version_display") # implementation path changes — identity does not e["identities"]["implementation"] = "src/tui/version_widget.rs" assert e["id"] == "layout:ui:version_display" assert e["identities"]["component"] == "ui:version_display" assert e["identities"]["region"] == "header" # C. FLEXIBLE element may move inside allowed region def test_flexible_move_within_region_ok(tmp_path): doc = _doc() lc.bind_component(doc, "help_hint", "main", provenance="builder") obs = {"ui:help_hint": {"region": "main", "order": 5}} assert lc.check_layout(doc, obs)["verdict"] == "PASS" # moved position within same region — still fine obs2 = {"ui:help_hint": {"region": "main", "order": 9}} assert lc.check_layout(doc, obs2)["verdict"] == "PASS" # D. USER-LOCKED element may not move to another region def test_locked_region_move_fails(tmp_path): doc = _doc() lc.bind_component(doc, "version_display", "header", lock=True, provenance="user", order=2) obs = {"ui:version_display": {"region": "footer", "order": 2}} result = lc.check_layout(doc, obs) assert result["verdict"] == "FAIL" check = result["checks"][0] assert check["drift_class"] == "USER_LOCKED_VIOLATION" # E. USER-LOCKED ordering remains stable def test_locked_order_stable(tmp_path): doc = _doc() lc.bind_component(doc, "version_display", "header", lock=True, provenance="user", order=2) ok = {"ui:version_display": {"region": "header", "order": 2}} assert lc.check_layout(doc, ok)["verdict"] == "PASS" bad = {"ui:version_display": {"region": "header", "order": 0}} assert lc.check_layout(doc, bad)["verdict"] == "FAIL" # F. model/scaffold-owned flexible rule can be revised def test_flexible_amendable_by_builder(tmp_path): doc = _doc() lc.bind_component(doc, "help_hint", "main", provenance="builder") lc.amend_layout_contract( doc, "help_hint", new_region="sidebar", authorized_by="builder", reason="layout improvement" ) obs = {"ui:help_hint": {"region": "sidebar"}} assert lc.check_layout(doc, obs)["verdict"] == "PASS" # G. user-locked rule requires explicit user-authorized amendment def test_locked_amendment_requires_user_authority(tmp_path): doc = _doc() lc.bind_component( doc, "version_display", "header", lock=True, provenance="user", order=2, evidence_ref="user-2026-09" ) with pytest.raises(ValueError, match="USER-LOCKED"): lc.amend_layout_contract( doc, "version_display", new_region="footer", authorized_by="builder", reason="I prefer it there" ) with pytest.raises(ValueError, match="USER-LOCKED"): lc.amend_layout_contract(doc, "version_display", new_region="footer", authorized_by="", reason="x") # builder attempt must not have mutated the contract assert ss.entity(doc, "layout:ui:version_display")["identities"]["region"] == "header" # with user authorization it works and records the amendment lc.amend_layout_contract( doc, "version_display", new_region="footer", authorized_by="user", reason="user changed mind", evidence_ref="user-2026-09-13", ) e = ss.entity(doc, "layout:ui:version_display") assert e["identities"]["region"] == "footer" assert e["ext"]["amendments"][0]["authorized_by"] == "user" assert e["ext"]["amendments"][0]["changes"]["region"] == "footer" # H. layout violation produces FAIL def test_violation_fail(tmp_path): doc = _doc() lc.bind_component(doc, "status_bar", "footer", provenance="scaffold") obs = {"ui:status_bar": {"region": "header"}} assert lc.check_layout(doc, obs)["verdict"] == "FAIL" # I. unavailable adapter → UNKNOWN, never PASS def test_missing_observation_unknown_never_pass(tmp_path): doc = _doc() lc.bind_component(doc, "version_display", "header", lock=True, provenance="user") lc.bind_component(doc, "status_bar", "footer", provenance="scaffold") # no observations at all → locked contract cannot be verified r = lc.check_layout(doc, {}) assert r["verdict"] == "UNKNOWN" assert all(c["verdict"] == "UNKNOWN" for c in r["checks"]) # partial observation: locked unknown → overall UNKNOWN r2 = lc.check_layout(doc, {"ui:status_bar": {"region": "footer"}}) assert r2["verdict"] == "UNKNOWN" # flexible-only unknown never escalates verdict doc2 = _doc() lc.bind_component(doc2, "help_hint", "main", provenance="builder") r3 = lc.check_layout(doc2, {}) assert r3["verdict"] == "PASS" # flexible has no observation → not a gate # J. layout drift does NOT overwrite the user's contract def test_drift_preserves_user_contract(tmp_path): doc = _doc() lc.bind_component( doc, "version_display", "header", lock=True, provenance="user", order=2, evidence_ref="user-2026-09" ) ss.save(doc, tmp_path / "d.json") doc2 = ss.load(tmp_path / "d.json") # a later model moved it to footer violating = {"ui:version_display": {"region": "footer", "order": 2}} out = lc.layout_drift(doc2, violating) assert out["gate"] == "FAIL" assert out["drifted_locked"] == ["ui:version_display"] # THE CONTRACT IS PRESERVED — not rewritten to match reality e = ss.entity(doc2, "layout:ui:version_display") assert e["identities"]["region"] == "header" # user intent wins assert e["status"] == "DRIFTING" assert e["identities"]["lock"] is True # violation recorded as evidence + maintenance item created assert ss.entity(doc2, "maintenance:layout-drift-ui:version_display") is not None obs_kinds = [o["kind"] for o in doc2["observations"]] assert "layout_observed" in obs_kinds # persisted across restart ss.save(doc2, tmp_path / "d.json") doc3 = ss.load(tmp_path / "d.json") assert ss.entity(doc3, "layout:ui:version_display")["identities"]["region"] == "header" # repair: reality returns to the contract → status returns to ACTIVE repaired = {"ui:version_display": {"region": "header", "order": 2}} out2 = lc.layout_drift(doc3, repaired) assert out2["gate"] == "PASS" assert ss.entity(doc3, "layout:ui:version_display")["status"] == "ACTIVE" # K. framework-specific metadata roundtrips without core schema change def test_framework_attrs_roundtrip(tmp_path): doc = _doc() lc.bind_component( doc, "version_display", "header", provenance="user", align="right", css_class="ver", grid_column=3 ) ss.save(doc, tmp_path / "d.json") doc2 = ss.load(tmp_path / "d.json") fa = ss.entity(doc2, "layout:ui:version_display")["identities"]["framework_attrs"] assert fa == {"align": "right", "css_class": "ver", "grid_column": 3} # L. web/GUI/TUI kinds coexist without core rewrite def test_multi_tech_coexistence(tmp_path): doc = _doc() # TUI region tree lc.define_region(doc, "tui_top_bar", tech={"kind": "tui", "widget": "pane"}, order=0) lc.define_region(doc, "tui_chat", parent="tui_top_bar", tech={"kind": "tui"}) # web region tree lc.define_region(doc, "web_header", tech={"kind": "web", "element": "header"}) lc.define_region(doc, "web_main", tech={"kind": "web"}) # qt/desktop region lc.define_region(doc, "qt_statusbar", tech={"kind": "qt", "widget": "QStatusBar"}) lc.bind_component(doc, "version_display", "web_header", provenance="user", css_class="ver") lc.bind_component(doc, "tui_version", "tui_top_bar", provenance="user") ss.save(doc, tmp_path / "d.json") doc2 = ss.load(tmp_path / "d.json") kinds = {e["identities"].get("tech", {}).get("kind") for e in doc2["entities"] if e["kind"] == "layout_region"} assert {"tui", "web", "qt"} <= kinds r = lc.check_layout( doc2, {"ui:version_display": {"region": "web_header"}, "ui:tui_version": {"region": "tui_top_bar"}} ) assert r["verdict"] == "PASS" # lock provenance guard def test_lock_requires_user_provenance(tmp_path): doc = _doc() with pytest.raises(ValueError, match="user provenance"): lc.bind_component(doc, "sneaky", "header", lock=True, provenance="builder") with pytest.raises(ValueError, match="user provenance"): lc.bind_component(doc, "sneaky", "header", lock=True, provenance="scaffold") def test_unknown_region_rejected(tmp_path): doc = _doc() with pytest.raises(ValueError, match="unknown region"): lc.bind_component(doc, "x", "nonexistent")