mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-05-18 05:05:39 +02:00
183 lines
7.0 KiB
Python
183 lines
7.0 KiB
Python
|
|
"""
|
||
|
|
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
|