A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 123 lines 4.3 kB view raw
1"""Tests for the storied.tools.names MCP adapter.""" 2 3from pathlib import Path 4 5import pytest 6 7from storied.testing import call_tool 8from storied.tools import ToolContext 9from storied.tools.names import forge_culture as _forge_culture 10from storied.tools.names import generate_names as _generate_names 11 12 13def forge_culture(feel: str | None = None) -> str: 14 return call_tool(_forge_culture, feel=feel) 15 16 17def generate_names( 18 culture: str, 19 count: int = 5, 20 gender: str | None = None, 21 rarity: str = "common", 22 kind: str = "person", 23) -> list[str]: 24 return call_tool( 25 _generate_names, 26 culture=culture, 27 count=count, 28 gender=gender, 29 rarity=rarity, 30 kind=kind, 31 ) 32 33 34def _extract_culture_name(result: str) -> str: 35 """Pull the culture name out of the forge tool's confirmation.""" 36 # Format: "Forged culture 'name' (source: ...). Sample names: ..." 37 after_quote = result.split("'", 2) 38 return after_quote[1] if len(after_quote) > 1 else "" 39 40 41class TestForgeCultureTool: 42 def test_forge_creates_yaml(self, ctx: ToolContext, tmp_path: Path): 43 result = forge_culture(feel="coastal") 44 name = _extract_culture_name(result) 45 yaml_path = tmp_path / "worlds" / ctx.world_id / "cultures" / f"{name}.yaml" 46 assert yaml_path.exists() 47 48 def test_forge_creates_entity_md(self, ctx: ToolContext, tmp_path: Path): 49 result = forge_culture(feel="coastal") 50 name = _extract_culture_name(result) 51 md_path = tmp_path / "worlds" / ctx.world_id / "cultures" / f"{name}.md" 52 assert md_path.exists() 53 54 def test_forge_returns_sample_names(self, ctx: ToolContext): 55 result = forge_culture(feel="coastal") 56 assert "Sample names:" in result 57 58 def test_forge_no_feel(self, ctx: ToolContext): 59 result = forge_culture(feel=None) 60 assert "Forged culture" in result 61 62 def test_forge_indexes_into_search(self, ctx: ToolContext, tmp_path: Path): 63 forge_culture(feel="coastal") 64 # The _do_establish call should have upserted the culture into 65 # the world search index with content_type="cultures". 66 results = ctx.vector_index.search("freshly-forged culture", limit=5) 67 assert results 68 assert any(r.content_type == "cultures" for r in results) 69 70 71class TestGenerateNamesTool: 72 def _forge_one(self) -> str: 73 return _extract_culture_name(forge_culture(feel="coastal")) 74 75 def test_generate_returns_names(self, ctx: ToolContext): 76 culture = self._forge_one() 77 names = generate_names(culture=culture, count=5) 78 assert len(names) == 5 79 80 def test_generate_unknown_culture_returns_empty(self, ctx: ToolContext): 81 names = generate_names(culture="ghostculture", count=5) 82 assert names == [] 83 84 def test_generate_place_kind(self, ctx: ToolContext): 85 culture = self._forge_one() 86 places = generate_names(culture=culture, count=3, kind="place") 87 assert len(places) == 3 88 89 def test_generate_with_gender(self, ctx: ToolContext): 90 culture = self._forge_one() 91 females = generate_names(culture=culture, count=3, gender="female") 92 males = generate_names(culture=culture, count=3, gender="male") 93 assert len(females) == 3 94 assert len(males) == 3 95 96 97class TestNamesRoleSurface: 98 """The names tools must be visible to dm/seeder/planner only, 99 NOT arc_architect (whose surface stays at commit_arc + recall).""" 100 101 def _names_for_role(self, role: str) -> set[str]: 102 import asyncio 103 104 from storied.mcp_server import _compose_server 105 106 async def _gather() -> set[str]: 107 server = await _compose_server(role) 108 tools = await server.list_tools() 109 return { 110 t.name for t in tools if t.name in ("forge_culture", "generate_names") 111 } 112 113 return asyncio.run(_gather()) 114 115 @pytest.mark.parametrize("role", ["dm", "planner", "seeder"]) 116 def test_role_has_both_names_tools(self, role: str): 117 assert self._names_for_role(role) == { 118 "forge_culture", 119 "generate_names", 120 } 121 122 def test_arc_architect_has_no_names_tools(self): 123 assert self._names_for_role("arc_architect") == set()