# pyright: reportOptionalMemberAccess=false # Tests reach into ContentResolver.load() results without null-narrowing. """Tests for three-layer content resolution (world > user > shipped). The autouse ``_isolate_storied_paths`` fixture in ``conftest.py`` already rebinds ``_data_home`` and ``_user_rules_home`` to ``tmp_path``; these tests additionally monkeypatch ``shipped_rules_path`` so the "shipped" layer can be faked per-test under the same tmp dir. """ from pathlib import Path import pytest from storied import paths from storied.content import ContentResolver # --------------------------------------------------------------------------- # Fixtures — build a fake three-layer setup under tmp_path. # --------------------------------------------------------------------------- @pytest.fixture def shipped_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Redirect the shipped rules layer to a tmp subdir so tests can write fake SRD content without touching the real package rules.""" root = tmp_path / "shipped" root.mkdir() monkeypatch.setattr(paths, "shipped_rules_path", lambda: root) return root @pytest.fixture def shipped_goblin(shipped_root: Path) -> Path: """Standard SRD goblin — the bottom layer.""" monsters = shipped_root / "srd-5.2.1" / "sections" / "monsters" monsters.mkdir(parents=True) path = monsters / "goblin.md" path.write_text("# Goblin\n\n_Small Humanoid, Neutral Evil_\n") return path @pytest.fixture def shipped_fireball(shipped_root: Path) -> Path: spells = shipped_root / "srd-5.2.1" / "sections" / "spells" spells.mkdir(parents=True) path = spells / "fireball.md" path.write_text("# Fireball\n\n_Level 3 Evocation_\n") return path @pytest.fixture def user_goblin(tmp_path: Path) -> Path: """User homebrew goblin — middle layer.""" monsters = tmp_path / "rules" / "monsters" monsters.mkdir(parents=True) path = monsters / "goblin.md" path.write_text("# Homebrew Goblin\n\n_Slightly meaner_\n") return path @pytest.fixture def world_goblin(tmp_path: Path) -> Path: """World-specific goblin override — top layer.""" monsters = tmp_path / "worlds" / "test-world" / "monsters" monsters.mkdir(parents=True) path = monsters / "goblin.md" path.write_text("# Island Goblin\n\n_Tougher variant_\n") return path @pytest.fixture def world_vera(tmp_path: Path) -> Path: """Narrative content only exists in the world layer.""" npcs = tmp_path / "worlds" / "test-world" / "npcs" npcs.mkdir(parents=True) path = npcs / "vera.md" path.write_text("# Vera\n\nInnkeeper at the Rusty Anchor.\n") return path # --------------------------------------------------------------------------- # Layer priority tests. # --------------------------------------------------------------------------- class TestLayerPriority: def test_shipped_only(self, shipped_goblin: Path): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin", content_type="monsters") assert hit == shipped_goblin def test_user_overrides_shipped( self, shipped_goblin: Path, user_goblin: Path, ): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin", content_type="monsters") assert hit == user_goblin assert "Homebrew" in hit.read_text() def test_world_overrides_user( self, shipped_goblin: Path, user_goblin: Path, world_goblin: Path, ): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin", content_type="monsters") assert hit == world_goblin assert "Island Goblin" in hit.read_text() def test_world_overrides_shipped_without_user( self, shipped_goblin: Path, world_goblin: Path, ): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin", content_type="monsters") assert hit == world_goblin def test_user_overrides_shipped_without_world( self, shipped_goblin: Path, user_goblin: Path, ): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin", content_type="monsters") assert hit == user_goblin # --------------------------------------------------------------------------- # Narrative content (world-only) tests. # --------------------------------------------------------------------------- class TestNarrativeContent: def test_find_world_npc(self, world_vera: Path): resolver = ContentResolver(world_id="test-world") hit = resolver.find("vera", content_type="npcs") assert hit == world_vera def test_narrative_misses_fall_through_cleanly(self, shipped_root: Path): """Looking up a narrative entity that doesn't exist returns None without error, even though the user/shipped layers have no ``npcs/`` subdir.""" resolver = ContentResolver(world_id="test-world") hit = resolver.find("nonexistent", content_type="npcs") assert hit is None def test_no_world_id_still_searches_rule_layers( self, shipped_fireball: Path, ): """A resolver with no world still finds rule content at the user and shipped layers.""" resolver = ContentResolver() hit = resolver.find("fireball", content_type="spells") assert hit == shipped_fireball # --------------------------------------------------------------------------- # Loading content (parsed dict). # --------------------------------------------------------------------------- class TestLoadContent: def test_load_plain_markdown(self, shipped_goblin: Path): resolver = ContentResolver(world_id="test-world") data = resolver.load("goblin", content_type="monsters") assert data is not None assert "Goblin" in data["body"] def test_load_with_frontmatter(self, shipped_root: Path): monsters = shipped_root / "srd-5.2.1" / "sections" / "monsters" monsters.mkdir(parents=True) (monsters / "orc.md").write_text( "---\ntype: monster\ncr: 0.5\ntags: [humanoid]\n---\n\n" "# Orc\n\nBig and mean.\n" ) resolver = ContentResolver(world_id="test-world") data = resolver.load("orc", content_type="monsters") assert data is not None assert data.get("type") == "monster" assert data.get("cr") == 0.5 assert data.get("tags") == ["humanoid"] assert "Orc" in data["body"] def test_load_not_found(self, shipped_root: Path): resolver = ContentResolver(world_id="test-world") data = resolver.load("nonexistent", content_type="monsters") assert data is None # --------------------------------------------------------------------------- # Untyped search (walks every content_type subdir in every layer). # --------------------------------------------------------------------------- class TestUntypedSearch: def test_finds_goblin_without_content_type(self, shipped_goblin: Path): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin") assert hit == shipped_goblin def test_world_wins_in_untyped_search( self, shipped_goblin: Path, world_goblin: Path, ): resolver = ContentResolver(world_id="test-world") hit = resolver.find("goblin") assert hit == world_goblin