Files
talemate/tests/test_world_state_reinforcements.py

183 lines
7.0 KiB
Python
Raw Normal View History

0.36.0 (#255) Major Features - API key encryption at rest using Fernet (OS keyring with file fallback) - Prompt Manager: unified UI with template groups, priority ordering, override tracking, response extractors - Scene context history review panel with token budgets and best-fit mode - Multiple concurrent director chats with auto-generated titles - Granular scene state reset dialog - Time passage insert/edit/delete in scene view - Image analysis via OpenAI-compatible and Talemate Client backends - Volatile context placement after scene history for improved prompt caching Improvements - Configurable narrator generation length per narration type - AI Aware conversation mode - Summarizer: custom instructions, writing style inclusion, short line filtering - Anthropic: adaptive thinking support, updated model list (opus-4-5/4-6, haiku-4-5) - Google: gemini-3.1 support - World editor: generate from topic, quick create state reinforcement, reorganized menus - Node editor: promote scene modules to global - Frontend: version mismatch detection, hideable bracket content, required scene name - TTS: improved pause handling, audio tag support for vocal markers (ElevenLabs v3) - Writing style template for AI-generated instructions - Added Kimi.jinja2 LLM prompt template - Option to disable character names in stopping strings - Client response length enforcement options - Graduated token count sliders - Increased summarizer token threshold max Bugfixes - Fix bracket/paren/brace terminators stripped from message ends - Fix colon in conversation causing content loss - Fix "Use as reference" navigating to blank page - Fix avatar regeneration and manual regenerate - Fix conversation agent ignoring generation length - Fix duplicate length instructions with reasoning enabled - Fix trailing newline on message edits - Fix summarize dialogue sending too much context with layered history - Fix layered history inspection and construction issues - Fix empty response handling in summarization - Fix context ID dot notation with dotted character names - Fix recursive retry in focal agent - Fix leading whitespace causing duplicate prepared responses - Fix summarization not stripping ANALYSIS OF lines - Fix template group selection/removal in prompt manager - Fix multiline text in parentheses/brackets parser - Fix determine_character_name resolution - Fix character activate/deactivate desyncing creative menu - Fix character image generation missing context - Fix LMStudio client not sending token limits - Fix Recent Scene images on newer Chromium - Fix sequential reinforcement messages cut off at first linebreak - Fix reinforcement removal not clearing state - Fixes #252, #256, #258 Deprecations - Removed context investigations (replaced by AI-assisted RAG mixin) - Removed deprecated prompt templates (fix-continuity-errors, fix-exposition, etc.) - Removed conversation/edit.jinja2, auto break repetition, CLI reset layered history --------- Co-authored-by: theDTV2 <47825738+theDTV2@users.noreply.github.com>
2026-03-15 12:00:57 +02:00
"""
Tests for world state reinforcement bugs:
1. Sequential mode truncates multiline output after first newline
2. Removing a world-level reinforcement doesn't clean up its manual_context entry
"""
import pytest
from conftest import MockScene, bootstrap_scene
from talemate.world_state import ManualContext, Reinforcement
from talemate.character import Character
from talemate.tale_mate import Actor
@pytest.fixture
def scene():
mock_scene = MockScene()
bootstrap_scene(mock_scene)
return mock_scene
# ---------------------------------------------------------------------------
# Bug 1: Sequential reinforcement output truncation
# ---------------------------------------------------------------------------
class TestSequentialReinforcementTruncation:
"""
Bug: When 'Context Attach Method' is 'Sequential', the generated answer
is split on newline and only the first line is kept, discarding the rest.
The truncation happens in agents/world_state/__init__.py update_reinforcement():
if reinforcement.insert == "sequential":
answer = answer.split("\\n")[0]
We test the Reinforcement model's answer storage directly to verify
the fix doesn't strip newlines.
"""
def test_reinforcement_stores_multiline_answer(self):
"""A Reinforcement object should be able to store multiline answers."""
r = Reinforcement(
question="What is the current time of day?",
insert="sequential",
answer="It is early morning.\nThe sun is just rising.",
)
assert "\n" in r.answer
assert r.answer == "It is early morning.\nThe sun is just rising."
def test_as_context_line_preserves_multiline(self):
"""as_context_line should work with multiline answers."""
r = Reinforcement(
question="What is the current time of day?",
insert="sequential",
answer="It is early morning.\nThe sun is just rising.",
)
context_line = r.as_context_line
assert "It is early morning." in context_line
assert "The sun is just rising." in context_line
# ---------------------------------------------------------------------------
# Bug 2: Removing world-level reinforcement doesn't clean up manual_context
# ---------------------------------------------------------------------------
class TestReinforcementRemovalCleanup:
"""
Bug: Removing a world-level reinforcement (e.g., 'Time of Day' tracker)
removes it from self.reinforce but leaves the corresponding entry in
world_state.manual_context, so it continues to appear in scene content.
"""
@pytest.mark.asyncio
async def test_remove_world_reinforcement_cleans_manual_context(self, scene):
"""Removing a world-level reinforcement should also remove its manual_context entry."""
world_state = scene.world_state
# Add a world-level reinforcement (no character)
await world_state.add_reinforcement(
question="What is the current time of day?",
character=None,
insert="sequential",
answer="It is early morning.",
)
# Simulate what update_reinforcement does: create a manual_context entry
world_state.manual_context["What is the current time of day?"] = ManualContext(
id="What is the current time of day?",
text="What is the current time of day? It is early morning.",
meta={"source": "manual", "typ": "world_state"},
)
# Verify setup
assert len(world_state.reinforce) == 1
assert "What is the current time of day?" in world_state.manual_context
# Now remove the reinforcement
idx, _ = await world_state.find_reinforcement(
"What is the current time of day?", None
)
await world_state.remove_reinforcement(idx)
# The reinforcement should be gone from the list
assert len(world_state.reinforce) == 0
# The manual_context entry should ALSO be gone
assert "What is the current time of day?" not in world_state.manual_context, (
"manual_context entry should be removed when reinforcement is removed"
)
@pytest.mark.asyncio
async def test_remove_character_reinforcement_cleans_detail(self, scene):
"""Removing a character-level reinforcement should also remove its character detail."""
world_state = scene.world_state
character = Character(name="Alice")
actor = Actor(character=character, agent=None)
scene.actors.append(actor)
character.details["What is Alice's mood?"] = "Happy and content."
# Add a character-level reinforcement
await world_state.add_reinforcement(
question="What is Alice's mood?",
character="Alice",
insert="sequential",
answer="Happy and content.",
)
# Verify setup
assert character.get_detail("What is Alice's mood?") == "Happy and content."
# Remove the reinforcement
idx, _ = await world_state.find_reinforcement("What is Alice's mood?", "Alice")
await world_state.remove_reinforcement(idx)
# The character detail should also be cleaned up
assert character.get_detail("What is Alice's mood?") is None, (
"Character detail should be removed when reinforcement is removed"
)
@pytest.mark.asyncio
async def test_remove_preserves_other_manual_context(self, scene):
"""Removing one reinforcement shouldn't affect other manual_context entries."""
world_state = scene.world_state
# Add two world-level reinforcements
await world_state.add_reinforcement(
question="What is the current time of day?",
character=None,
insert="sequential",
answer="It is early morning.",
)
await world_state.add_reinforcement(
question="What is the current weather?",
character=None,
insert="sequential",
answer="Clear skies.",
)
# Simulate manual_context entries (as update_reinforcement would create)
world_state.manual_context["What is the current time of day?"] = ManualContext(
id="What is the current time of day?",
text="What is the current time of day? It is early morning.",
meta={"source": "manual", "typ": "world_state"},
)
world_state.manual_context["What is the current weather?"] = ManualContext(
id="What is the current weather?",
text="What is the current weather? Clear skies.",
meta={"source": "manual", "typ": "world_state"},
)
# Remove only the first reinforcement
idx, _ = await world_state.find_reinforcement(
"What is the current time of day?", None
)
await world_state.remove_reinforcement(idx)
# The other manual_context entry should still exist
assert "What is the current weather?" in world_state.manual_context
assert "What is the current time of day?" not in world_state.manual_context