"""Tests for the relation registry (Phase A).""" import pytest from shared.sx.evaluator import evaluate, EvalError from shared.sx.parser import parse from shared.sx.relations import ( _RELATION_REGISTRY, clear_registry, get_relation, load_relation_registry, relations_from, relations_to, all_relations, ) from shared.sx.types import RelationDef @pytest.fixture(autouse=True) def _clean_registry(): """Clear registry before each test.""" clear_registry() # --------------------------------------------------------------------------- # defrelation parsing # --------------------------------------------------------------------------- class TestDefrelation: def test_basic_defrelation(self): tree = parse(''' (defrelation :page->market :from "page" :to "market" :cardinality :one-to-many :inverse :market->page :nav :submenu :nav-icon "fa fa-shopping-bag" :nav-label "markets") ''') result = evaluate(tree) assert isinstance(result, RelationDef) assert result.name == "page->market" assert result.from_type == "page" assert result.to_type == "market" assert result.cardinality == "one-to-many" assert result.inverse == "market->page" assert result.nav == "submenu" assert result.nav_icon == "fa fa-shopping-bag" assert result.nav_label == "markets" def test_defrelation_registered(self): tree = parse(''' (defrelation :a->b :from "a" :to "b" :cardinality :one-to-one :nav :hidden) ''') evaluate(tree) assert get_relation("a->b") is not None assert get_relation("a->b").cardinality == "one-to-one" def test_defrelation_one_to_one(self): tree = parse(''' (defrelation :page->menu_node :from "page" :to "menu_node" :cardinality :one-to-one :nav :hidden) ''') result = evaluate(tree) assert result.cardinality == "one-to-one" assert result.inverse is None assert result.nav == "hidden" def test_defrelation_many_to_many(self): tree = parse(''' (defrelation :post->entry :from "post" :to "calendar_entry" :cardinality :many-to-many :inverse :entry->post :nav :inline :nav-icon "fa fa-file-alt" :nav-label "events") ''') result = evaluate(tree) assert result.cardinality == "many-to-many" def test_default_nav_is_hidden(self): tree = parse(''' (defrelation :x->y :from "x" :to "y" :cardinality :one-to-many) ''') result = evaluate(tree) assert result.nav == "hidden" def test_invalid_cardinality_raises(self): tree = parse(''' (defrelation :bad :from "a" :to "b" :cardinality :wrong) ''') with pytest.raises(EvalError, match="invalid cardinality"): evaluate(tree) def test_invalid_nav_raises(self): tree = parse(''' (defrelation :bad :from "a" :to "b" :cardinality :one-to-one :nav :bogus) ''') with pytest.raises(EvalError, match="invalid nav"): evaluate(tree) def test_missing_from_raises(self): tree = parse(''' (defrelation :bad :to "b" :cardinality :one-to-one) ''') with pytest.raises(EvalError, match="missing required :from"): evaluate(tree) def test_missing_to_raises(self): tree = parse(''' (defrelation :bad :from "a" :cardinality :one-to-one) ''') with pytest.raises(EvalError, match="missing required :to"): evaluate(tree) def test_missing_cardinality_raises(self): tree = parse(''' (defrelation :bad :from "a" :to "b") ''') with pytest.raises(EvalError, match="missing required :cardinality"): evaluate(tree) def test_name_must_be_keyword(self): tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)') with pytest.raises(EvalError, match="must be a keyword"): evaluate(tree) # --------------------------------------------------------------------------- # Registry queries # --------------------------------------------------------------------------- class TestRegistry: def _load_sample(self): tree = parse(''' (begin (defrelation :page->market :from "page" :to "market" :cardinality :one-to-many :nav :submenu :nav-icon "fa fa-shopping-bag" :nav-label "markets") (defrelation :page->calendar :from "page" :to "calendar" :cardinality :one-to-many :nav :submenu :nav-icon "fa fa-calendar" :nav-label "calendars") (defrelation :post->entry :from "post" :to "calendar_entry" :cardinality :many-to-many :nav :inline) (defrelation :page->menu_node :from "page" :to "menu_node" :cardinality :one-to-one :nav :hidden)) ''') evaluate(tree) def test_get_relation(self): self._load_sample() rel = get_relation("page->market") assert rel is not None assert rel.to_type == "market" def test_get_relation_not_found(self): assert get_relation("nonexistent") is None def test_relations_from_page(self): self._load_sample() rels = relations_from("page") names = {r.name for r in rels} assert names == {"page->market", "page->calendar", "page->menu_node"} def test_relations_from_post(self): self._load_sample() rels = relations_from("post") assert len(rels) == 1 assert rels[0].name == "post->entry" def test_relations_from_empty(self): self._load_sample() assert relations_from("nonexistent") == [] def test_relations_to_market(self): self._load_sample() rels = relations_to("market") assert len(rels) == 1 assert rels[0].name == "page->market" def test_relations_to_calendar_entry(self): self._load_sample() rels = relations_to("calendar_entry") assert len(rels) == 1 assert rels[0].name == "post->entry" def test_all_relations(self): self._load_sample() assert len(all_relations()) == 4 def test_clear_registry(self): self._load_sample() assert len(all_relations()) == 4 clear_registry() assert len(all_relations()) == 0 # --------------------------------------------------------------------------- # load_relation_registry() — built-in definitions # --------------------------------------------------------------------------- class TestLoadBuiltins: def test_loads_builtin_relations(self): load_relation_registry() assert get_relation("page->market") is not None assert get_relation("page->calendar") is not None assert get_relation("post->calendar_entry") is not None assert get_relation("page->menu_node") is not None def test_builtin_page_market(self): load_relation_registry() rel = get_relation("page->market") assert rel.from_type == "page" assert rel.to_type == "market" assert rel.cardinality == "one-to-many" assert rel.inverse == "market->page" assert rel.nav == "submenu" assert rel.nav_icon == "fa fa-shopping-bag" def test_builtin_post_entry(self): load_relation_registry() rel = get_relation("post->calendar_entry") assert rel.cardinality == "many-to-many" assert rel.nav == "inline" def test_builtin_page_menu_node(self): load_relation_registry() rel = get_relation("page->menu_node") assert rel.cardinality == "one-to-one" assert rel.nav == "hidden" def test_frozen_dataclass(self): load_relation_registry() rel = get_relation("page->market") with pytest.raises(AttributeError): rel.name = "changed"