Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217# 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`` alreadyrebinds ``_data_home`` and ``_user_rules_home`` to ``tmp_path``; thesetests 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 pathsfrom storied.content import ContentResolver
# ---------------------------------------------------------------------------# Fixtures — build a fake three-layer setup under tmp_path.# ---------------------------------------------------------------------------
@pytest.fixturedef 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.fixturedef 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.fixturedef 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.fixturedef 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.fixturedef 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.fixturedef 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