- capability_fit.py: open-world CapabilityRequirement; deterministic candidate discovery over Spine/Scaffold stores (kind + providing- relationship eligible; environment-contradiction excluded); fit levels EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN from contracts+health, never names; resolution surface SATISFIED/REUSE/CONFIGURE/COMPOSE/ADAPT_EXISTING/ ACQUIRE_EXTERNAL/BUILD_NEW/INSUFFICIENT_EVIDENCE — BUILD_NEW never silent (insufficient evidence blocks it), brittle-coupling check (load-bearing missing contracts) forces BUILD_NEW or ACQUIRE with recorded rationale; composition covers via contract union, flagged temporary (permanence user-controlled); LLM semantic-fit seam records provider/model/reason/evidence/confidence — prose never authority - toolchain.py: EXISTING PROJECT TOOLCHAIN WINS; constraint keys fixed and language-free, values open-world; language change = CHANGE_MAJOR requiring user approval, never silent; unknown constraints -> INSUFFICIENT_EVIDENCE (no invented certainty); Python only selected because the project uses it — fixtures prove rust/ts/zig resolve - layout_contract.py: open-world regions (web/tui/qt coexist); stable ui:<component> identity surviving implementation changes; LOCKED requires user provenance (mechanically enforced); locked amendment requires authorized_by=user (enforced, reason required); deterministic adapter-observation gate PASS/FAIL/UNKNOWN — UNKNOWN never PASS; USER-LOCKED drift preserves the contract (deliberate exception to reality-wins), marks DRIFTING, creates maintenance item; flexible violation fails gate until repaired - REAL TUI PROOF: Rune's actual version display (issue #105, plan plans/2026-09-03-tui-footer.md) observed from source via deterministic adapter — footer-bar first child; gate cycle PASS -> controlled violation FAIL -> restore PASS; binding encoded scaffold-provenance (durable records establish placement; USER LOCK upgrade NEEDS CONFIRMATION — not fabricated) - capability/layout integration proven: version-display requirement discovers the existing capability + layout binding (REUSE), never builds another widget - 3 modules registered in Spine authority + canonical build + manifest regenerated (Tranche 2 lesson applied: no hand-edited structural facts) Tests: 53 new (fit 24 + toolchain 9 + layout 16 + real proof 4); suite 1439 green; henxels rc=0; pyright 0 errors on new modules.
237 lines
9.7 KiB
Python
237 lines
9.7 KiB
Python
"""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")
|