Files
talemate/tests/prompts/baselines/test_world_state_baselines.py
veguAI 53ecfe5029 fix: the primed Name line no longer consumes a max_attributes slot (#162) (#163)
* fix: exclude the character's own name from the max_attributes budget (#162)

* fix: stop the unified character prompt spending an attribute slot on the name (#162)

* fix: keep an unbudgeted Name wherever it sits in the sheet; pin the cap instruction (#162 review)

* docs: describe the Fast-mode prompt change rather than the sheet it produces (#162 review)
2026-07-27 22:00:07 +03:00

439 lines
16 KiB
Python

"""
Baseline snapshot tests for world_state agent prompt templates.
Captures the rendered prompt text passed to client.send_prompt() and compares
against stored baseline files. Run with --update-baselines to create/update.
"""
import pytest
from unittest.mock import AsyncMock
from talemate.scene_message import CharacterMessage, NarratorMessage
from talemate.world_state import (
CharacterState,
ContextPin,
ObjectState,
PlaceState,
Reinforcement,
)
from ..conftest import mock_llm_client # noqa: F401
from ..test_world_state_templates import ( # noqa: F401
mock_scene,
mock_memory_agent,
mock_creator_agent,
mock_summarizer_agent,
mock_director_agent,
mock_editor_agent,
world_state_agent,
setup_agents,
active_context,
)
from .conftest import capture_prompt
AGENT = "world_state"
class TestWorldStateAnalyzeBaselines:
"""Baseline tests for world_state analyze methods."""
@pytest.mark.asyncio
async def test_analyze_and_follow_instruction(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="Analysis result.")
await agent.analyze_and_follow_instruction(
text="The hero discovered a hidden passage behind the waterfall.",
instruction="Identify all locations mentioned in the text.",
)
baseline_checker(capture_prompt(agent), AGENT, "analyze_and_follow_instruction")
@pytest.mark.asyncio
async def test_analyze_and_follow_instruction__short(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="Brief summary.")
await agent.analyze_and_follow_instruction(
text="Short text.", instruction="Summarize.", short=True
)
baseline_checker(
capture_prompt(agent), AGENT, "analyze_and_follow_instruction__short"
)
@pytest.mark.asyncio
async def test_analyze_text_and_answer_question(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value="Elena is using an ancient sword."
)
await agent.analyze_text_and_answer_question(
text="Elena wielded the ancient sword with great skill.",
query="What weapon is Elena using?",
)
baseline_checker(
capture_prompt(agent), AGENT, "analyze_text_and_answer_question"
)
@pytest.mark.asyncio
async def test_analyze_text_and_extract_context(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="Political turmoil context.")
await agent.analyze_text_and_extract_context(
text="The kingdom has been at war for a decade.",
goal="Understanding the political situation",
num_queries=3,
)
# This method may make multiple calls; capture the last one
baseline_checker(
capture_prompt(agent), AGENT, "analyze_text_and_extract_context"
)
@pytest.mark.asyncio
async def test_analyze_text_and_extract_context_via_queries(
self, active_context, mock_memory_agent, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value="1. What is the history?\n2. Who are the factions?"
)
await agent.analyze_text_and_extract_context_via_queries(
text="The sorcerer's tower loomed over the ancient forest.",
goal="Gather information about the sorcerer.",
num_queries=2,
)
baseline_checker(
capture_prompt(agent), AGENT, "analyze_text_and_extract_context_via_queries"
)
@pytest.mark.asyncio
async def test_analyze_history_and_follow_instructions(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="History analysis.")
entries = [
{"ts": "PT1H", "text": "The hero arrived at the village."},
{"ts": "PT2H", "text": "The hero met with the village elder."},
]
await agent.analyze_history_and_follow_instructions(
entries=entries,
instructions="Summarize the hero's journey so far.",
response_length=256,
)
baseline_checker(
capture_prompt(agent), AGENT, "analyze_history_and_follow_instructions"
)
class TestWorldStateIdentifyBaselines:
"""Baseline tests for world_state identify methods."""
@pytest.mark.asyncio
async def test_identify_characters(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value='{"characters": [{"name": "Elena", "description": "A healer"}]}'
)
await agent.identify_characters(
text="Elena spoke to the village elder, while Marcus stood guard."
)
baseline_checker(capture_prompt(agent), AGENT, "identify_characters")
class TestWorldStateExtractBaselines:
"""Baseline tests for world_state extract methods."""
@pytest.mark.asyncio
async def test_extract_character_sheet(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value="name: Elena\nage: 25\noccupation: Healer"
)
await agent.extract_character_sheet(
name="Elena", text="A skilled healer with gentle manners."
)
baseline_checker(capture_prompt(agent), AGENT, "extract_character_sheet")
@pytest.mark.asyncio
async def test_extract_character_sheet__with_max_attributes(
self, active_context, baseline_checker
):
# the cap instruction only renders when a limit is set, so without
# this the wording is unpinned by any test
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value="name: Elena\nage: 25\noccupation: Healer"
)
await agent.extract_character_sheet(
name="Elena",
text="A skilled healer with gentle manners.",
max_attributes=3,
)
baseline_checker(
capture_prompt(agent), AGENT, "extract_character_sheet__with_max_attributes"
)
@pytest.mark.asyncio
async def test_extract_character_sheet__with_alteration(
self, active_context, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="name: Elena\nage: 30")
await agent.extract_character_sheet(
name="Elena",
text="",
alteration_instructions="Update age to reflect time passing.",
)
baseline_checker(
capture_prompt(agent), AGENT, "extract_character_sheet__with_alteration"
)
class TestWorldStateRequestBaselines:
"""Baseline tests for world_state request methods."""
@pytest.mark.asyncio
async def test_request_world_state(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
# The agent short-circuits when there are no narrative messages to
# anchor highlights to, so seed a few. The most-recent message
# determines the anchor and what "line N" refers to in the prompt.
mock_scene.history = [
NarratorMessage(message="The clearing opens onto a moss-covered shrine."),
CharacterMessage(
message="Elena: Have you seen markings like these before?",
source="Elena",
),
CharacterMessage(
message="Hero: Not since the road to Aldermere.",
source="Hero",
),
]
# The prompt seeds the response with `set_data_response`, which prepends
# `\`\`\`json\n{ "characters": {` to whatever the model returns. To
# exercise the actual world_state template (not the JSON-fix retry that
# fires when the response can't be parsed), the mock must return a
# completion that closes the seeded prefix into valid JSON.
agent.client.send_prompt = AsyncMock(
return_value=(
'"Hero": {"emotion": "determined", "snapshot": "Standing ready",'
' "mentions": []}}, "items": {}, "places": {}, "location": "test"}'
)
)
await agent.request_world_state()
baseline_checker(capture_prompt(agent), AGENT, "request_world_state")
@pytest.mark.asyncio
async def test_request_world_state_with_current_state(
self, active_context, mock_scene, baseline_checker
):
"""Exercises the populated CURRENT STATE path — when prior entities
exist, the template renders them so the LLM can patch on top."""
agent = active_context
mock_scene.history = [
NarratorMessage(message="The clearing opens onto a moss-covered shrine."),
CharacterMessage(
message="Elena: Have you seen markings like these before?",
source="Elena",
),
CharacterMessage(
message="Hero: Not since the road to Aldermere.",
source="Hero",
),
]
# Seed an existing snapshot the LLM should be shown for patching.
mock_scene.world_state.characters = {
"Elena": CharacterState(
emotion="curious",
snapshot="A healer who recognizes shrine markings.",
mentions=["Elena"],
)
}
mock_scene.world_state.items = {
"Moss-Covered Shrine": ObjectState(
snapshot="A stone shrine half-reclaimed by moss; its carvings hint at an older language.",
mentions=["a moss-covered shrine"],
)
}
mock_scene.world_state.places = {
"Aldermere Road": PlaceState(
snapshot="A trade road south of the forest, known for similar markings on its waystones.",
mentions=["the road to Aldermere"],
)
}
mock_scene.world_state.location = "Forest clearing with the shrine"
agent.client.send_prompt = AsyncMock(
return_value=(
'"Hero": {"emotion": "determined", "snapshot": "Standing ready",'
' "mentions": []}}, "items": {}, "places": {}, "location": "test"}'
)
)
await agent.request_world_state()
baseline_checker(
capture_prompt(agent), AGENT, "request_world_state_with_current_state"
)
class TestWorldStateExamineBaselines:
"""Baseline tests for world_state examine_entity."""
@pytest.mark.asyncio
async def test_examine_entity(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(
return_value="A simple silver dagger, plain in appearance."
)
await agent.examine_entity(
entity_name="The Silver Dagger",
entity_kind="item",
snapshot_text=(
"A modest silver dagger with worn leather wrapping the hilt;"
" unremarkable until you notice the faintly etched sigil"
" on the pommel."
),
)
baseline_checker(capture_prompt(agent), AGENT, "examine_entity")
class TestWorldStateReinforcementBaselines:
"""Baseline tests for world_state reinforcement methods."""
@pytest.mark.asyncio
async def test_update_reinforcement(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="The hero feels determined.")
reinforcement = Reinforcement(
question="What is the hero's mood?",
answer="",
interval=10,
due=0,
character=None,
instructions="",
insert="sequential",
)
mock_scene.world_state.reinforce = [reinforcement]
await agent.update_reinforcement(
question="What is the hero's mood?", character=None
)
baseline_checker(capture_prompt(agent), AGENT, "update_reinforcement")
@pytest.mark.asyncio
async def test_update_reinforcement__with_character(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="Elena appears calm.")
reinforcement = Reinforcement(
question="current mood",
answer="",
interval=10,
due=0,
character="Elena",
instructions="",
insert="conversation-context",
)
mock_scene.world_state.reinforce = [reinforcement]
await agent.update_reinforcement(question="current mood", character="Elena")
baseline_checker(
capture_prompt(agent), AGENT, "update_reinforcement__with_character"
)
class TestWorldStatePinBaselines:
"""Baseline tests for world_state pin condition methods."""
@pytest.mark.asyncio
async def test_check_pin_conditions(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
pin = ContextPin(
entry_id="test_pin",
condition="The hero is in danger",
condition_state=False,
active=False,
)
mock_scene.world_state.pins = {"test_pin": pin}
agent.client.send_prompt = AsyncMock(
return_value='{"test_pin": {"condition": "The hero is in danger", "state": true}}'
)
await agent.check_pin_conditions()
baseline_checker(capture_prompt(agent), AGENT, "check_pin_conditions")
class TestWorldStatePresenceBaselines:
"""Baseline tests for world_state character presence methods."""
@pytest.mark.asyncio
async def test_is_character_present(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="yes")
await agent.is_character_present("Elena")
baseline_checker(capture_prompt(agent), AGENT, "is_character_present")
@pytest.mark.asyncio
async def test_is_character_leaving(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="yes")
await agent.is_character_leaving("Elena")
baseline_checker(capture_prompt(agent), AGENT, "is_character_leaving")
class TestWorldStateQueryBaselines:
"""Baseline tests for world_state query methods."""
@pytest.mark.asyncio
async def test_answer_query_true_or_false(self, active_context, baseline_checker):
agent = active_context
agent.client.send_prompt = AsyncMock(return_value="yes")
await agent.answer_query_true_or_false(
query="Is the door open?", text="The door stood ajar."
)
baseline_checker(capture_prompt(agent), AGENT, "answer_query_true_or_false")
class TestWorldStateCharacterProgressionBaselines:
"""Baseline tests for world_state character progression methods."""
@pytest.mark.asyncio
async def test_determine_character_development(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
character = mock_scene.get_character("Elena")
agent.client.send_prompt = AsyncMock(return_value="[]")
await agent.determine_character_development(character=character)
baseline_checker(
capture_prompt(agent), AGENT, "determine_character_development"
)
@pytest.mark.asyncio
async def test_determine_character_development__with_instructions(
self, active_context, mock_scene, baseline_checker
):
agent = active_context
character = mock_scene.get_character("Elena")
agent.client.send_prompt = AsyncMock(return_value="[]")
await agent.determine_character_development(
character=character, instructions="Focus on combat improvements."
)
baseline_checker(
capture_prompt(agent),
AGENT,
"determine_character_development__with_instructions",
)