mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-09-01 19:48:52 +02:00
- **Per-Scene Agent Overrides** — Override agent configuration for a single scene without touching your global config. - **Message Revision History** — Browse and continue from previous regenerations / revisions. - **Scene Perspective Overrides** — Set distinct narrative perspectives for the player, NPCs, and narrator. - **Durable World State Snapshot** — The world state snapshot now persists and updates entities across refreshes. - **World State Highlights** — Characters, objects and places marked by the current world state snapshot are now clickable in the recent message(s) for additional detail. Plus 30 improvements and 17 bug fixes
1596 lines
57 KiB
Python
1596 lines
57 KiB
Python
import pytest
|
|
|
|
import talemate.instance as instance
|
|
from conftest import MockMemoryAgent, MockScene
|
|
|
|
from talemate.game.engine.context_id import (
|
|
ContextID,
|
|
CharacterDescriptionContextID,
|
|
CharacterAttributeContextID,
|
|
CharacterDetailContextID,
|
|
WorldEntryContextID,
|
|
WorldEntryManualContextID,
|
|
StaticHistoryEntryContextID,
|
|
DynamicHistoryEntryContextID,
|
|
get_context_id_type,
|
|
context_id_from_string,
|
|
context_id_from_object,
|
|
context_id_item_from_string,
|
|
context_id_handler_from_string,
|
|
compress_name,
|
|
CONTEXT_ID_TYPES,
|
|
CONTEXT_ID_PATH_HANDLERS,
|
|
ContextIDHandlerError,
|
|
ContextIDNoHandlerFound,
|
|
CharacterContext,
|
|
)
|
|
from talemate.game.engine.context_id.story_configuration import (
|
|
DirectorInstructionsContextID,
|
|
ScenePerspectiveContextID,
|
|
StoryConfigurationContext,
|
|
StoryConfigurationContextItem,
|
|
)
|
|
from talemate.character import Character
|
|
from talemate.history import HistoryEntry
|
|
from talemate.world_state import ManualContext
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path, expected_path_str",
|
|
[
|
|
(["test"], "context:test"),
|
|
(["test", "path"], "context:test.path"),
|
|
(["nested", "test", "path"], "context:nested.test.path"),
|
|
([], "context:"),
|
|
],
|
|
)
|
|
def test_context_id_basic_functionality(path, expected_path_str):
|
|
"""Test basic ContextID functionality with various paths."""
|
|
context_id = ContextID(path=path)
|
|
assert context_id.path == path
|
|
assert context_id.context_type == "context"
|
|
assert context_id.path_to_str == expected_path_str
|
|
assert context_id.id == expected_path_str
|
|
assert str(context_id) == expected_path_str
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
["test"],
|
|
["test", "path"],
|
|
["nested", "test", "path"],
|
|
],
|
|
)
|
|
def test_context_id_class_methods(path):
|
|
"""Test ContextID class methods."""
|
|
context_id_make = ContextID.make(path)
|
|
|
|
assert context_id_make.path == path
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"character_input, expected_character",
|
|
[
|
|
("TestChar", "TestChar"),
|
|
(Character(name="RealChar"), "RealChar"),
|
|
],
|
|
)
|
|
def test_character_description_context_id(character_input, expected_character):
|
|
"""Test CharacterDescriptionContextID creation."""
|
|
context_id = CharacterDescriptionContextID.make(character_input)
|
|
assert context_id.character == expected_character
|
|
assert context_id.path == [expected_character, "description"]
|
|
assert context_id.context_type == "character.description"
|
|
assert (
|
|
context_id.path_to_str
|
|
== f"character.description:{expected_character}.description"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"character_input, attribute, expected_character",
|
|
[
|
|
("TestChar", "strength", "TestChar"),
|
|
(Character(name="RealChar"), "intelligence", "RealChar"),
|
|
("Player", "dexterity", "Player"),
|
|
],
|
|
)
|
|
def test_character_attribute_context_id(character_input, attribute, expected_character):
|
|
"""Test CharacterAttributeContextID creation."""
|
|
context_id = CharacterAttributeContextID.make(character_input, attribute)
|
|
assert context_id.character == expected_character
|
|
assert context_id.attribute == attribute
|
|
# Path should use compressed attribute name
|
|
assert context_id.path == [expected_character, compress_name(attribute)]
|
|
assert context_id.context_type == "character.attribute"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"character_input, detail, expected_character",
|
|
[
|
|
("TestChar", "appearance", "TestChar"),
|
|
(Character(name="RealChar"), "personality", "RealChar"),
|
|
("Player", "background", "Player"),
|
|
],
|
|
)
|
|
def test_character_detail_context_id(character_input, detail, expected_character):
|
|
"""Test CharacterDetailContextID creation."""
|
|
context_id = CharacterDetailContextID.make(character_input, detail)
|
|
assert context_id.character == expected_character
|
|
assert context_id.detail == detail
|
|
# Path should use compressed detail name
|
|
assert context_id.path == [expected_character, compress_name(detail)]
|
|
assert context_id.context_type == "character.detail"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry_id, expected_path",
|
|
[
|
|
("test_entry_123", []),
|
|
("world_item_456", []),
|
|
],
|
|
)
|
|
def test_world_entry_context_id(entry_id, expected_path):
|
|
"""Test WorldEntryContextID direct creation (base class)."""
|
|
context_id = WorldEntryContextID(entry_id=entry_id, path=expected_path)
|
|
assert context_id.entry_id == entry_id
|
|
assert context_id.path == expected_path
|
|
assert context_id.context_type == "world_entry"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry_input, expected_entry_id",
|
|
[
|
|
("test_entry", "test_entry"),
|
|
(ManualContext(id="manual_123", text="test text"), "manual_123"),
|
|
],
|
|
)
|
|
def test_world_entry_manual_context_id(entry_input, expected_entry_id):
|
|
"""Test WorldEntryManualContextID creation with string or ManualContext."""
|
|
context_id = WorldEntryManualContextID.make(entry_input)
|
|
assert context_id.context_type == "world_entry.manual"
|
|
assert context_id.entry_id == expected_entry_id
|
|
# Path should use compressed entry ID
|
|
assert context_id.path == [compress_name(expected_entry_id)]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry_id, layer, expected_path",
|
|
[
|
|
("entry_123", 0, ["layer", "0", "id", "entry_123"]),
|
|
("entry_456", 5, ["layer", "5", "id", "entry_456"]),
|
|
("entry_789", 10, ["layer", "10", "id", "entry_789"]),
|
|
],
|
|
)
|
|
def test_dynamic_history_entry_context_id(entry_id, layer, expected_path):
|
|
"""Test DynamicHistoryEntryContextID creation."""
|
|
entry = HistoryEntry(text="test", ts="PT1S", index=0, layer=layer, id=entry_id)
|
|
context_id = DynamicHistoryEntryContextID.make(entry)
|
|
assert context_id.entry_id == entry_id
|
|
assert context_id.layer == layer
|
|
assert context_id.path == expected_path
|
|
assert context_id.context_type == "history_entry.dynamic"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"entry_id",
|
|
[
|
|
"entry_123",
|
|
"static_entry_456",
|
|
],
|
|
)
|
|
def test_static_history_entry_context_id(entry_id):
|
|
"""Test StaticHistoryEntryContextID creation."""
|
|
entry = HistoryEntry(text="test", ts="PT1S", index=0, layer=0, id=entry_id)
|
|
context_id = StaticHistoryEntryContextID.make(entry)
|
|
assert context_id.entry_id == entry_id
|
|
assert context_id.path == [entry_id]
|
|
assert context_id.context_type == "history_entry.static"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"context_type, expected_class",
|
|
[
|
|
("character.description", CharacterDescriptionContextID),
|
|
("character.attribute", CharacterAttributeContextID),
|
|
("character.detail", CharacterDetailContextID),
|
|
("world_entry.manual", WorldEntryManualContextID),
|
|
("history_entry.static", StaticHistoryEntryContextID),
|
|
("history_entry.dynamic", DynamicHistoryEntryContextID),
|
|
],
|
|
)
|
|
def test_get_context_id_type(context_type, expected_class):
|
|
"""Test getting context ID type by string."""
|
|
cls = get_context_id_type(context_type)
|
|
assert cls == expected_class
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"context_type",
|
|
[
|
|
"character.description",
|
|
"character.attribute",
|
|
"character.detail",
|
|
"world_entry.manual",
|
|
"history_entry",
|
|
"history_entry.static",
|
|
"history_entry.dynamic",
|
|
],
|
|
)
|
|
def test_context_id_types_registration(context_type):
|
|
"""Test that context types are properly registered."""
|
|
assert context_type in CONTEXT_ID_TYPES
|
|
|
|
|
|
def test_get_context_id_type_missing():
|
|
"""Test that getting non-existent context type raises KeyError."""
|
|
with pytest.raises(KeyError):
|
|
get_context_id_type("nonexistent.type")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"context_type, object_input, expected_type, expected_attrs",
|
|
[
|
|
(
|
|
"character.description",
|
|
"TestChar",
|
|
CharacterDescriptionContextID,
|
|
{"character": "TestChar", "path": ["TestChar", "description"]},
|
|
),
|
|
(
|
|
"character.description",
|
|
Character(name="RealChar"),
|
|
CharacterDescriptionContextID,
|
|
{"character": "RealChar", "path": ["RealChar", "description"]},
|
|
),
|
|
(
|
|
"world_entry.manual",
|
|
"manual_entry_123",
|
|
WorldEntryManualContextID,
|
|
{
|
|
"entry_id": "manual_entry_123",
|
|
"path": [compress_name("manual_entry_123")],
|
|
},
|
|
),
|
|
(
|
|
"world_entry.manual",
|
|
ManualContext(id="ctx_456", text="test context"),
|
|
WorldEntryManualContextID,
|
|
{"entry_id": "ctx_456", "path": [compress_name("ctx_456")]},
|
|
),
|
|
(
|
|
"history_entry.static",
|
|
HistoryEntry(text="test", ts="PT1S", index=0, layer=0, id="hist_123"),
|
|
StaticHistoryEntryContextID,
|
|
{"entry_id": "hist_123", "path": ["hist_123"]},
|
|
),
|
|
(
|
|
"history_entry.dynamic",
|
|
HistoryEntry(text="test", ts="PT1S", index=0, layer=3, id="hist_456"),
|
|
DynamicHistoryEntryContextID,
|
|
{
|
|
"entry_id": "hist_456",
|
|
"layer": 3,
|
|
"path": ["layer", "3", "id", "hist_456"],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_context_id_from_object(
|
|
context_type, object_input, expected_type, expected_attrs
|
|
):
|
|
"""Test creating context IDs from objects using context_id_from_object."""
|
|
context_id = context_id_from_object(context_type, object_input)
|
|
assert isinstance(context_id, expected_type)
|
|
|
|
for attr, expected_value in expected_attrs.items():
|
|
assert getattr(context_id, attr) == expected_value
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"context_type, object_input, kwargs, expected_type, expected_attrs",
|
|
[
|
|
(
|
|
"character.attribute",
|
|
"TestChar",
|
|
{"attribute": "strength"},
|
|
CharacterAttributeContextID,
|
|
{
|
|
"character": "TestChar",
|
|
"attribute": "strength",
|
|
"path": ["TestChar", compress_name("strength")],
|
|
},
|
|
),
|
|
(
|
|
"character.attribute",
|
|
Character(name="RealChar"),
|
|
{"attribute": "intelligence"},
|
|
CharacterAttributeContextID,
|
|
{
|
|
"character": "RealChar",
|
|
"attribute": "intelligence",
|
|
"path": ["RealChar", compress_name("intelligence")],
|
|
},
|
|
),
|
|
(
|
|
"character.detail",
|
|
"TestChar",
|
|
{"detail": "appearance"},
|
|
CharacterDetailContextID,
|
|
{
|
|
"character": "TestChar",
|
|
"detail": "appearance",
|
|
"path": ["TestChar", compress_name("appearance")],
|
|
},
|
|
),
|
|
(
|
|
"character.detail",
|
|
Character(name="RealChar"),
|
|
{"detail": "personality"},
|
|
CharacterDetailContextID,
|
|
{
|
|
"character": "RealChar",
|
|
"detail": "personality",
|
|
"path": ["RealChar", compress_name("personality")],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
def test_context_id_from_object_with_kwargs(
|
|
context_type, object_input, kwargs, expected_type, expected_attrs
|
|
):
|
|
"""Test context_id_from_object with kwargs for context types that require multiple arguments."""
|
|
context_id = context_id_from_object(context_type, object_input, **kwargs)
|
|
assert isinstance(context_id, expected_type)
|
|
|
|
for attr, expected_value in expected_attrs.items():
|
|
assert getattr(context_id, attr) == expected_value
|
|
|
|
|
|
def test_context_id_from_object_comprehensive():
|
|
"""Test that context_id_from_object works for all registered context types."""
|
|
# Test all single-argument context types
|
|
char_desc = context_id_from_object("character.description", "TestChar")
|
|
assert isinstance(char_desc, CharacterDescriptionContextID)
|
|
|
|
manual_entry = context_id_from_object("world_entry.manual", "test_entry")
|
|
assert isinstance(manual_entry, WorldEntryManualContextID)
|
|
|
|
static_hist = context_id_from_object(
|
|
"history_entry.static",
|
|
HistoryEntry(text="test", ts="PT1S", index=0, layer=0, id="test_123"),
|
|
)
|
|
assert isinstance(static_hist, StaticHistoryEntryContextID)
|
|
assert static_hist.path == ["test_123"]
|
|
|
|
dynamic_hist = context_id_from_object(
|
|
"history_entry.dynamic",
|
|
HistoryEntry(text="test", ts="PT1S", index=0, layer=2, id="test_456"),
|
|
)
|
|
assert isinstance(dynamic_hist, DynamicHistoryEntryContextID)
|
|
|
|
# Test multi-argument context types with kwargs
|
|
char_attr = context_id_from_object(
|
|
"character.attribute", "TestChar", attribute="strength"
|
|
)
|
|
assert isinstance(char_attr, CharacterAttributeContextID)
|
|
assert char_attr.attribute == "strength"
|
|
|
|
char_detail = context_id_from_object(
|
|
"character.detail", "TestChar", detail="appearance"
|
|
)
|
|
assert isinstance(char_detail, CharacterDetailContextID)
|
|
assert char_detail.detail == "appearance"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"context_id, expected_str",
|
|
[
|
|
(ContextID(path=["test", "path"]), "context:test.path"),
|
|
(
|
|
CharacterDescriptionContextID(
|
|
character="TestChar", path=["TestChar", "description"]
|
|
),
|
|
"character.description:TestChar.description",
|
|
),
|
|
(
|
|
CharacterAttributeContextID(
|
|
character="Player",
|
|
attribute="strength",
|
|
path=["Player", compress_name("strength")],
|
|
),
|
|
f"character.attribute:Player.{compress_name('strength')}",
|
|
),
|
|
(
|
|
CharacterDetailContextID(
|
|
character="Hero",
|
|
detail="appearance",
|
|
path=["Hero", compress_name("appearance")],
|
|
),
|
|
f"character.detail:Hero.{compress_name('appearance')}",
|
|
),
|
|
(
|
|
WorldEntryContextID(entry_id="world_123", path=["world_123"]),
|
|
"world_entry:world_123",
|
|
),
|
|
(
|
|
WorldEntryManualContextID(
|
|
entry_id="manual_456", path=[compress_name("manual_456")]
|
|
),
|
|
f"world_entry.manual:{compress_name('manual_456')}",
|
|
),
|
|
(
|
|
StaticHistoryEntryContextID(entry_id="hist_789", path=["hist_789"]),
|
|
"history_entry.static:hist_789",
|
|
),
|
|
(
|
|
DynamicHistoryEntryContextID(
|
|
entry_id="dyn_abc", layer=3, path=["layer", "3", "id", "dyn_abc"]
|
|
),
|
|
"history_entry.dynamic:layer.3.id.dyn_abc",
|
|
),
|
|
],
|
|
)
|
|
def test_context_id_str_method(context_id, expected_str):
|
|
"""Test that __str__ method works correctly for all context ID types."""
|
|
assert str(context_id) == expected_str
|
|
# Also verify that it's consistent with id property
|
|
assert str(context_id) == context_id.id
|
|
assert str(context_id) == context_id.path_to_str
|
|
|
|
|
|
def test_context_id_string_formatting():
|
|
"""Test that context IDs work correctly in string formatting contexts."""
|
|
context_id = CharacterDescriptionContextID(
|
|
character="TestChar", path=["TestChar", "description"]
|
|
)
|
|
|
|
# Test f-string formatting
|
|
formatted = f"Context: {context_id}"
|
|
assert formatted == "Context: character.description:TestChar.description"
|
|
|
|
# Test string concatenation
|
|
concatenated = "Prefix-" + str(context_id) + "-Suffix"
|
|
assert concatenated == "Prefix-character.description:TestChar.description-Suffix"
|
|
|
|
# Test in collections that might call __str__
|
|
context_list = [context_id]
|
|
assert str(context_list[0]) == "character.description:TestChar.description"
|
|
|
|
|
|
def test_context_id_from_object_invalid_context_type():
|
|
"""Test context_id_from_object with invalid context type."""
|
|
with pytest.raises(KeyError):
|
|
context_id_from_object("nonexistent.type", "test_object")
|
|
|
|
|
|
# Tests for Character Context Handler and Context ID Value Flow
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _real_memory_agent():
|
|
"""Register a real `MockMemoryAgent` (a `MemoryAgent` subclass) for tests.
|
|
|
|
Replaces the previous `AsyncMock()` + `patch('talemate.instance.get_agent')`
|
|
pattern, which was a stand-in for the `MemoryAgent` domain type — that
|
|
silently absorbed any change to MemoryAgent's contract (renamed methods,
|
|
new validators, removed methods). `MockMemoryAgent` is a real subclass
|
|
that only stubs the persistence side-effects.
|
|
"""
|
|
original = instance.AGENTS.copy()
|
|
instance.AGENTS["memory"] = MockMemoryAgent()
|
|
yield
|
|
instance.AGENTS.clear()
|
|
instance.AGENTS.update(original)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene():
|
|
"""Create a mock scene with test characters."""
|
|
scene = MockScene()
|
|
|
|
# Create test character with attributes and details
|
|
char = Character(
|
|
name="TestCharacter",
|
|
description="A brave warrior",
|
|
base_attributes={"strength": 15, "intelligence": 12, "dexterity": 14},
|
|
details={
|
|
"appearance": "Tall and muscular",
|
|
"personality": "Brave and loyal",
|
|
"background": "Former knight",
|
|
},
|
|
)
|
|
scene.character_data[char.name] = char
|
|
|
|
# Create second character for testing
|
|
char2 = Character(
|
|
name="Wizard",
|
|
description="A wise spellcaster",
|
|
base_attributes={"intelligence": 18, "wisdom": 16},
|
|
details={"appearance": "Old with a long beard", "specialty": "Fire magic"},
|
|
)
|
|
scene.character_data[char2.name] = char2
|
|
|
|
return scene
|
|
|
|
|
|
def test_character_context_handler_registration():
|
|
"""Test that CharacterContext handler is properly registered."""
|
|
|
|
# Verify handler is registered for all expected context types
|
|
expected_types = [
|
|
"character.description",
|
|
"character.attribute",
|
|
"character.detail",
|
|
]
|
|
|
|
for context_type in expected_types:
|
|
assert context_type in CONTEXT_ID_PATH_HANDLERS
|
|
assert CONTEXT_ID_PATH_HANDLERS[context_type] == CharacterContext
|
|
|
|
|
|
def test_character_context_instance_from_path(mock_scene):
|
|
"""Test CharacterContext.instance_from_path method."""
|
|
|
|
# Test successful character lookup
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
assert handler is not None
|
|
assert handler.character.name == "TestCharacter"
|
|
|
|
# Test with non-existent character should raise ContextIDHandlerError
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'NonExistent' not found in scene"
|
|
):
|
|
CharacterContext.instance_from_path(["NonExistent"], mock_scene)
|
|
|
|
|
|
def test_character_context_properties(mock_scene):
|
|
"""Test CharacterContext property accessors."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test description property
|
|
desc = handler.description
|
|
assert desc.context_type == "description"
|
|
assert desc.character.name == "TestCharacter"
|
|
assert desc.name == "description"
|
|
assert desc.value == "A brave warrior"
|
|
|
|
# Test attributes generator
|
|
attributes = list(handler.attributes)
|
|
assert len(attributes) == 3
|
|
attr_names = [attr.name for attr in attributes]
|
|
assert "strength" in attr_names
|
|
assert "intelligence" in attr_names
|
|
assert "dexterity" in attr_names
|
|
|
|
for attr in attributes:
|
|
assert attr.context_type == "attribute"
|
|
assert attr.character.name == "TestCharacter"
|
|
|
|
# Test details generator
|
|
details = list(handler.details)
|
|
assert len(details) == 3
|
|
detail_names = [detail.name for detail in details]
|
|
assert "appearance" in detail_names
|
|
assert "personality" in detail_names
|
|
assert "background" in detail_names
|
|
|
|
for detail in details:
|
|
assert detail.context_type == "detail"
|
|
assert detail.character.name == "TestCharacter"
|
|
|
|
|
|
def test_character_context_get_methods(mock_scene):
|
|
"""Test CharacterContext.get_attribute and get_detail methods."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test get_attribute
|
|
strength = handler.get_attribute("strength")
|
|
assert strength is not None
|
|
assert strength.name == "strength"
|
|
assert strength.value == 15
|
|
|
|
# Test get_attribute for non-existent attribute
|
|
missing = handler.get_attribute("charisma")
|
|
assert missing is None
|
|
|
|
# Test get_detail
|
|
appearance = handler.get_detail("appearance")
|
|
assert appearance is not None
|
|
assert appearance.name == "appearance"
|
|
assert appearance.value == "Tall and muscular"
|
|
|
|
# Test get_detail for non-existent detail
|
|
missing = handler.get_detail("missing_detail")
|
|
assert missing is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_character_context_item_operations(mock_scene):
|
|
"""Test CharacterContextItem get and set operations."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
character = handler.character
|
|
|
|
# Memory agent is registered as a real `MockMemoryAgent` via the
|
|
# module-scoped autouse fixture; production calls to `add_many`/`delete`
|
|
# are no-ops on it.
|
|
# Test description operations
|
|
desc_item = handler.description
|
|
value = await desc_item.get(mock_scene)
|
|
assert value == "A brave warrior"
|
|
|
|
await desc_item.set(mock_scene, "Updated description")
|
|
assert character.description == "Updated description"
|
|
|
|
# Test attribute operations
|
|
strength_item = handler.get_attribute("strength")
|
|
value = await strength_item.get(mock_scene)
|
|
assert value == 15
|
|
|
|
await strength_item.set(mock_scene, 18)
|
|
assert character.base_attributes["strength"] == 18
|
|
|
|
# Test detail operations
|
|
appearance_item = handler.get_detail("appearance")
|
|
value = await appearance_item.get(mock_scene)
|
|
assert value == "Tall and muscular"
|
|
|
|
await appearance_item.set(mock_scene, "Short and thin")
|
|
assert character.details["appearance"] == "Short and thin"
|
|
|
|
|
|
def test_character_context_item_context_id_property(mock_scene):
|
|
"""Test that CharacterContextItem.context_id returns correct ContextID types."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test description context_id
|
|
desc_item = handler.description
|
|
context_id = desc_item.context_id
|
|
assert isinstance(context_id, CharacterDescriptionContextID)
|
|
assert context_id.character == "TestCharacter"
|
|
|
|
# Test attribute context_id
|
|
strength_item = handler.get_attribute("strength")
|
|
context_id = strength_item.context_id
|
|
assert isinstance(context_id, CharacterAttributeContextID)
|
|
assert context_id.character == "TestCharacter"
|
|
assert context_id.attribute == "strength"
|
|
|
|
# Test detail context_id
|
|
appearance_item = handler.get_detail("appearance")
|
|
context_id = appearance_item.context_id
|
|
assert isinstance(context_id, CharacterDetailContextID)
|
|
assert context_id.character == "TestCharacter"
|
|
assert context_id.detail == "appearance"
|
|
|
|
|
|
def test_character_context_item_human_id_property(mock_scene):
|
|
"""Test that CharacterContextItem.human_id returns human-readable descriptions."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test description human_id
|
|
desc_item = handler.description
|
|
assert desc_item.human_id == "Information about TestCharacter - 'description'"
|
|
|
|
# Test attribute human_id
|
|
strength_item = handler.get_attribute("strength")
|
|
assert strength_item.human_id == "Information about TestCharacter - 'strength'"
|
|
|
|
# Test detail human_id
|
|
appearance_item = handler.get_detail("appearance")
|
|
assert appearance_item.human_id == "Information about TestCharacter - 'appearance'"
|
|
|
|
|
|
def test_character_context_compressed_path(mock_scene):
|
|
"""Test that CharacterContextItem.compressed_path returns correct path strings."""
|
|
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test description compressed path
|
|
desc_item = handler.description
|
|
assert (
|
|
desc_item.compressed_path == "character.description:TestCharacter.description"
|
|
)
|
|
|
|
# Test attribute compressed path
|
|
strength_item = handler.get_attribute("strength")
|
|
expected_path = f"character.attribute:TestCharacter.{compress_name('strength')}"
|
|
assert strength_item.compressed_path == expected_path
|
|
|
|
# Test detail compressed path
|
|
appearance_item = handler.get_detail("appearance")
|
|
expected_path = f"character.detail:TestCharacter.{compress_name('appearance')}"
|
|
assert appearance_item.compressed_path == expected_path
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_character_context_id_item_from_path(mock_scene):
|
|
"""Test CharacterContext.context_id_item_from_path method."""
|
|
|
|
# Create handler instance first
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
|
|
# Test description context
|
|
desc_value = await handler.context_id_item_from_path(
|
|
"character.description",
|
|
["TestCharacter", "description"],
|
|
"character.description:TestCharacter.description",
|
|
mock_scene,
|
|
)
|
|
assert desc_value is not None
|
|
assert desc_value.context_type == "description"
|
|
assert desc_value.character.name == "TestCharacter"
|
|
|
|
# Test attribute context
|
|
strength_compressed = compress_name("strength")
|
|
attr_path_str = f"character.attribute:TestCharacter.{strength_compressed}"
|
|
attr_value = await handler.context_id_item_from_path(
|
|
"character.attribute",
|
|
["TestCharacter", strength_compressed],
|
|
attr_path_str,
|
|
mock_scene,
|
|
)
|
|
assert attr_value is not None
|
|
assert attr_value.context_type == "attribute"
|
|
assert attr_value.name == "strength"
|
|
assert attr_value.value == 15
|
|
|
|
# Test detail context
|
|
appearance_compressed = compress_name("appearance")
|
|
detail_path_str = f"character.detail:TestCharacter.{appearance_compressed}"
|
|
detail_value = await handler.context_id_item_from_path(
|
|
"character.detail",
|
|
["TestCharacter", appearance_compressed],
|
|
detail_path_str,
|
|
mock_scene,
|
|
)
|
|
assert detail_value is not None
|
|
assert detail_value.context_type == "detail"
|
|
assert detail_value.name == "appearance"
|
|
assert detail_value.value == "Tall and muscular"
|
|
|
|
# Test non-existent character should raise ContextIDHandlerError
|
|
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'NonExistent' not found in scene"
|
|
):
|
|
non_existent_handler = CharacterContext.instance_from_path(
|
|
["NonExistent"], mock_scene
|
|
)
|
|
await non_existent_handler.context_id_item_from_path(
|
|
"character.description",
|
|
["NonExistent", "description"],
|
|
"character.description:NonExistent.description",
|
|
mock_scene,
|
|
)
|
|
|
|
# Test invalid context type
|
|
none_value = await handler.context_id_item_from_path(
|
|
"invalid.type",
|
|
["TestCharacter", "description"],
|
|
"invalid.type:TestCharacter.description",
|
|
mock_scene,
|
|
)
|
|
assert none_value is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_character_context_id_from_path(mock_scene):
|
|
"""Test CharacterContext.context_id_from_path method."""
|
|
|
|
# Test description context ID
|
|
handler = CharacterContext.instance_from_path(["TestCharacter"], mock_scene)
|
|
desc_context_id = await handler.context_id_from_path(
|
|
"character.description",
|
|
["TestCharacter", "description"],
|
|
"character.description:TestCharacter.description",
|
|
mock_scene,
|
|
)
|
|
assert desc_context_id is not None
|
|
assert isinstance(desc_context_id, CharacterDescriptionContextID)
|
|
assert desc_context_id.character == "TestCharacter"
|
|
|
|
# Test attribute context ID
|
|
strength_compressed = compress_name("strength")
|
|
attr_path_str = f"character.attribute:TestCharacter.{strength_compressed}"
|
|
attr_context_id = await handler.context_id_from_path(
|
|
"character.attribute",
|
|
["TestCharacter", strength_compressed],
|
|
attr_path_str,
|
|
mock_scene,
|
|
)
|
|
assert attr_context_id is not None
|
|
assert isinstance(attr_context_id, CharacterAttributeContextID)
|
|
assert attr_context_id.character == "TestCharacter"
|
|
assert attr_context_id.attribute == "strength"
|
|
|
|
# Test detail context ID
|
|
appearance_compressed = compress_name("appearance")
|
|
detail_path_str = f"character.detail:TestCharacter.{appearance_compressed}"
|
|
detail_context_id = await handler.context_id_from_path(
|
|
"character.detail",
|
|
["TestCharacter", appearance_compressed],
|
|
detail_path_str,
|
|
mock_scene,
|
|
)
|
|
assert detail_context_id is not None
|
|
assert isinstance(detail_context_id, CharacterDetailContextID)
|
|
assert detail_context_id.character == "TestCharacter"
|
|
assert detail_context_id.detail == "appearance"
|
|
|
|
# Test non-existent character should raise ContextIDHandlerError
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'NonExistent' not found in scene"
|
|
):
|
|
non_existent_handler = CharacterContext.instance_from_path(
|
|
["NonExistent"], mock_scene
|
|
)
|
|
await non_existent_handler.context_id_from_path(
|
|
"character.description",
|
|
["NonExistent", "description"],
|
|
"character.description:NonExistent.description",
|
|
mock_scene,
|
|
)
|
|
|
|
|
|
def test_context_id_handler_lookup(mock_scene):
|
|
"""Test context_id_handler_from_string function."""
|
|
|
|
# Test valid character context types
|
|
for context_type in [
|
|
"character.description",
|
|
"character.attribute",
|
|
"character.detail",
|
|
]:
|
|
context_id_str = f"{context_type}:TestCharacter.test"
|
|
handler = context_id_handler_from_string(context_id_str, mock_scene)
|
|
assert isinstance(handler, CharacterContext)
|
|
|
|
# Test invalid context type
|
|
with pytest.raises(ContextIDNoHandlerFound):
|
|
context_id_handler_from_string("nonexistent.type:test.path", mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_integration_context_id_item_flow(mock_scene):
|
|
"""Test the complete context ID value flow from string to value operations."""
|
|
|
|
# Test functions directly with mock_scene parameter
|
|
|
|
# Test description flow
|
|
desc_context_id_str = "character.description:TestCharacter.description"
|
|
|
|
# Test context_id_item_from_string
|
|
context_value = await context_id_item_from_string(desc_context_id_str, mock_scene)
|
|
assert context_value is not None
|
|
assert context_value.context_type == "description"
|
|
assert context_value.character.name == "TestCharacter"
|
|
assert context_value.value == "A brave warrior"
|
|
|
|
# Test context_id_from_string
|
|
context_id = await context_id_from_string(desc_context_id_str, mock_scene)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, CharacterDescriptionContextID)
|
|
assert context_id.character == "TestCharacter"
|
|
|
|
# Test attribute flow
|
|
strength_compressed = compress_name("strength")
|
|
attr_context_id_str = f"character.attribute:TestCharacter.{strength_compressed}"
|
|
|
|
context_value = await context_id_item_from_string(attr_context_id_str, mock_scene)
|
|
assert context_value is not None
|
|
assert context_value.context_type == "attribute"
|
|
assert context_value.name == "strength"
|
|
assert context_value.value == 15
|
|
|
|
context_id = await context_id_from_string(attr_context_id_str, mock_scene)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, CharacterAttributeContextID)
|
|
assert context_id.attribute == "strength"
|
|
|
|
# Test detail flow
|
|
appearance_compressed = compress_name("appearance")
|
|
detail_context_id_str = f"character.detail:TestCharacter.{appearance_compressed}"
|
|
|
|
context_value = await context_id_item_from_string(detail_context_id_str, mock_scene)
|
|
assert context_value is not None
|
|
assert context_value.context_type == "detail"
|
|
assert context_value.name == "appearance"
|
|
assert context_value.value == "Tall and muscular"
|
|
|
|
context_id = await context_id_from_string(detail_context_id_str, mock_scene)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, CharacterDetailContextID)
|
|
assert context_id.detail == "appearance"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_integration_context_id_item_flow_no_scene():
|
|
"""Test context ID value flow when no active scene is available."""
|
|
|
|
# Create empty scene with no characters
|
|
empty_scene = MockScene()
|
|
|
|
desc_context_id_str = "character.description:TestCharacter.description"
|
|
|
|
# Should raise ContextIDHandlerError when character not found
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'TestCharacter' not found in scene"
|
|
):
|
|
await context_id_item_from_string(desc_context_id_str, empty_scene)
|
|
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'TestCharacter' not found in scene"
|
|
):
|
|
await context_id_from_string(desc_context_id_str, empty_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_integration_context_id_item_flow_nonexistent_character(mock_scene):
|
|
"""Test context ID value flow with nonexistent character."""
|
|
|
|
# Test with nonexistent character should raise ContextIDHandlerError
|
|
desc_context_id_str = "character.description:NonExistent.description"
|
|
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'NonExistent' not found in scene"
|
|
):
|
|
await context_id_item_from_string(desc_context_id_str, mock_scene)
|
|
|
|
with pytest.raises(
|
|
ContextIDHandlerError, match="Character 'NonExistent' not found in scene"
|
|
):
|
|
await context_id_from_string(desc_context_id_str, mock_scene)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_integration_full_context_value_operations(mock_scene):
|
|
"""Test full integration of context value operations through the context ID system."""
|
|
|
|
# Memory agent registered as a real `MockMemoryAgent` via the
|
|
# module-scoped autouse fixture.
|
|
|
|
# Get context value for character description
|
|
desc_context_id_str = "character.description:TestCharacter.description"
|
|
context_value = await context_id_item_from_string(desc_context_id_str, mock_scene)
|
|
assert context_value is not None
|
|
|
|
# Test get operation
|
|
original_value = await context_value.get(mock_scene)
|
|
assert original_value == "A brave warrior"
|
|
|
|
# Test set operation
|
|
await context_value.set(mock_scene, "A legendary warrior")
|
|
updated_value = await context_value.get(mock_scene)
|
|
assert updated_value == "A legendary warrior"
|
|
|
|
# Verify character was actually updated
|
|
character = mock_scene.get_character("TestCharacter")
|
|
assert character.description == "A legendary warrior"
|
|
|
|
# Test attribute operations
|
|
strength_compressed = compress_name("strength")
|
|
attr_context_id_str = f"character.attribute:TestCharacter.{strength_compressed}"
|
|
attr_context_value = await context_id_item_from_string(
|
|
attr_context_id_str, mock_scene
|
|
)
|
|
assert attr_context_value is not None
|
|
|
|
original_strength = await attr_context_value.get(mock_scene)
|
|
assert original_strength == 15
|
|
|
|
await attr_context_value.set(mock_scene, 20)
|
|
updated_strength = await attr_context_value.get(mock_scene)
|
|
assert updated_strength == 20
|
|
assert character.base_attributes["strength"] == 20
|
|
|
|
# Test detail operations
|
|
appearance_compressed = compress_name("appearance")
|
|
detail_context_id_str = f"character.detail:TestCharacter.{appearance_compressed}"
|
|
detail_context_value = await context_id_item_from_string(
|
|
detail_context_id_str, mock_scene
|
|
)
|
|
assert detail_context_value is not None
|
|
|
|
original_appearance = await detail_context_value.get(mock_scene)
|
|
assert original_appearance == "Tall and muscular"
|
|
|
|
await detail_context_value.set(mock_scene, "Imposing and battle-scarred")
|
|
updated_appearance = await detail_context_value.get(mock_scene)
|
|
assert updated_appearance == "Imposing and battle-scarred"
|
|
assert character.details["appearance"] == "Imposing and battle-scarred"
|
|
|
|
|
|
def test_character_context_edge_cases(mock_scene):
|
|
"""Test edge cases for character context operations."""
|
|
|
|
# Test with empty character name should raise ContextIDHandlerError
|
|
with pytest.raises(ContextIDHandlerError, match="Character '' not found in scene"):
|
|
CharacterContext.instance_from_path([""], mock_scene)
|
|
|
|
# Test with character that has no attributes or details
|
|
empty_char = Character(name="EmptyChar", description="Empty character")
|
|
mock_scene.character_data[empty_char.name] = empty_char
|
|
|
|
handler = CharacterContext.instance_from_path(["EmptyChar"], mock_scene)
|
|
assert handler is not None
|
|
|
|
# Should return empty lists for attributes and details
|
|
attributes = list(handler.attributes)
|
|
details = list(handler.details)
|
|
assert len(attributes) == 0
|
|
assert len(details) == 0
|
|
|
|
# Description should still work
|
|
desc_item = handler.description
|
|
assert desc_item.value == "Empty character"
|
|
|
|
# get_attribute and get_detail should return None for non-existent items
|
|
assert handler.get_attribute("nonexistent") is None
|
|
assert handler.get_detail("nonexistent") is None
|
|
|
|
|
|
# Tests for Story Configuration Context IDs
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene_story_config():
|
|
"""Create a mock scene for testing story configuration context IDs."""
|
|
scene = MockScene()
|
|
scene.title = "Test Story"
|
|
scene.name = "test_story"
|
|
scene.description = "A test story description"
|
|
scene.context = "Adult themes, fantasy setting"
|
|
scene.intro = "This is the story introduction."
|
|
scene.intent_state.intent = "Overall story intention"
|
|
scene.intent_state.instructions = "Director instructions for managing the scene"
|
|
scene.perspectives.default = "Third person limited, past tense"
|
|
return scene
|
|
|
|
|
|
def test_director_instructions_context_id_creation():
|
|
"""Test DirectorInstructionsContextID creation."""
|
|
context_id = DirectorInstructionsContextID.make()
|
|
assert context_id.key == "director_instructions"
|
|
assert context_id.path == ["director_instructions"]
|
|
assert context_id.context_type == "story_configuration"
|
|
assert context_id.path_to_str == "story_configuration:director_instructions"
|
|
|
|
|
|
def test_director_instructions_context_id_properties():
|
|
"""Test DirectorInstructionsContextID properties."""
|
|
context_id = DirectorInstructionsContextID.make()
|
|
assert str(context_id) == "story_configuration:director_instructions"
|
|
assert context_id.id == "story_configuration:director_instructions"
|
|
assert "director" in context_id.context_type_label.lower()
|
|
|
|
|
|
def test_story_configuration_context_handler_registration():
|
|
"""Test that StoryConfigurationContext handler is properly registered."""
|
|
assert "story_configuration" in CONTEXT_ID_PATH_HANDLERS
|
|
assert CONTEXT_ID_PATH_HANDLERS["story_configuration"] == StoryConfigurationContext
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_director_instructions_context_item_get(mock_scene_story_config):
|
|
"""Test getting director instructions through context item."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["director_instructions"], mock_scene_story_config
|
|
)
|
|
assert handler is not None
|
|
|
|
item = await handler.context_id_item_from_path(
|
|
"story_configuration",
|
|
["director_instructions"],
|
|
"story_configuration:director_instructions",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
assert item is not None
|
|
assert item.context_type == "director_instructions"
|
|
assert item.name == "director_instructions"
|
|
assert item.value == "Director instructions for managing the scene"
|
|
|
|
# Test get method
|
|
value = await item.get(mock_scene_story_config)
|
|
assert value == "Director instructions for managing the scene"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_director_instructions_context_item_set(mock_scene_story_config):
|
|
"""Test setting director instructions through context item."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["director_instructions"], mock_scene_story_config
|
|
)
|
|
|
|
item = await handler.context_id_item_from_path(
|
|
"story_configuration",
|
|
["director_instructions"],
|
|
"story_configuration:director_instructions",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
# Test set method
|
|
new_instructions = "Updated director instructions"
|
|
await item.set(mock_scene_story_config, new_instructions)
|
|
assert mock_scene_story_config.intent_state.instructions == new_instructions
|
|
|
|
# Test setting to None
|
|
await item.set(mock_scene_story_config, None)
|
|
assert mock_scene_story_config.intent_state.instructions is None
|
|
|
|
# Test setting empty string converts to None
|
|
await item.set(mock_scene_story_config, "")
|
|
assert mock_scene_story_config.intent_state.instructions is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_director_instructions_context_id_from_path(mock_scene_story_config):
|
|
"""Test getting DirectorInstructionsContextID from path."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["director_instructions"], mock_scene_story_config
|
|
)
|
|
|
|
context_id = await handler.context_id_from_path(
|
|
"story_configuration",
|
|
["director_instructions"],
|
|
"story_configuration:director_instructions",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
assert context_id is not None
|
|
assert isinstance(context_id, DirectorInstructionsContextID)
|
|
assert context_id.path == ["director_instructions"]
|
|
|
|
|
|
def test_director_instructions_context_item_properties(mock_scene_story_config):
|
|
"""Test DirectorInstructionsContextItem properties."""
|
|
item = StoryConfigurationContextItem(
|
|
context_type="director_instructions",
|
|
name="director_instructions",
|
|
value="Test instructions",
|
|
)
|
|
|
|
# Test context_id property
|
|
context_id = item.context_id
|
|
assert isinstance(context_id, DirectorInstructionsContextID)
|
|
|
|
# Test human_id property
|
|
assert item.human_id == "Director Instructions"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_director_instructions_integration_flow(mock_scene_story_config):
|
|
"""Test complete integration flow for director instructions context ID."""
|
|
context_id_str = "story_configuration:director_instructions"
|
|
|
|
# Test context_id_item_from_string
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_story_config
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "director_instructions"
|
|
assert context_item.value == "Director instructions for managing the scene"
|
|
|
|
# Test context_id_from_string
|
|
context_id = await context_id_from_string(context_id_str, mock_scene_story_config)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, DirectorInstructionsContextID)
|
|
|
|
# Test get and set operations
|
|
original_value = await context_item.get(mock_scene_story_config)
|
|
assert original_value == "Director instructions for managing the scene"
|
|
|
|
new_value = "New instructions for the director"
|
|
await context_item.set(mock_scene_story_config, new_value)
|
|
|
|
updated_value = await context_item.get(mock_scene_story_config)
|
|
assert updated_value == new_value
|
|
assert mock_scene_story_config.intent_state.instructions == new_value
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_director_instructions_with_none_value(mock_scene_story_config):
|
|
"""Test director instructions context ID when instructions are None."""
|
|
mock_scene_story_config.intent_state.instructions = None
|
|
|
|
context_id_str = "story_configuration:director_instructions"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_story_config
|
|
)
|
|
|
|
assert context_item is not None
|
|
value = await context_item.get(mock_scene_story_config)
|
|
assert value is None
|
|
|
|
|
|
# Tests for Scene Perspective Context ID
|
|
|
|
|
|
def test_scene_perspective_context_id_creation():
|
|
"""Test ScenePerspectiveContextID creation."""
|
|
context_id = ScenePerspectiveContextID.make()
|
|
assert context_id.key == "perspective"
|
|
assert context_id.path == ["perspective"]
|
|
assert context_id.context_type == "story_configuration"
|
|
assert context_id.path_to_str == "story_configuration:perspective"
|
|
|
|
|
|
def test_scene_perspective_context_id_properties():
|
|
"""Test ScenePerspectiveContextID properties."""
|
|
context_id = ScenePerspectiveContextID.make()
|
|
assert str(context_id) == "story_configuration:perspective"
|
|
assert context_id.id == "story_configuration:perspective"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scene_perspective_context_item_get(mock_scene_story_config):
|
|
"""Test getting perspective through context item."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["perspective"], mock_scene_story_config
|
|
)
|
|
|
|
item = await handler.context_id_item_from_path(
|
|
"story_configuration",
|
|
["perspective"],
|
|
"story_configuration:perspective",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
assert item is not None
|
|
assert item.context_type == "perspective"
|
|
assert item.name == "perspective"
|
|
assert item.value == "Third person limited, past tense"
|
|
|
|
value = await item.get(mock_scene_story_config)
|
|
assert value == "Third person limited, past tense"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scene_perspective_context_item_set(mock_scene_story_config):
|
|
"""Test setting perspective through context item."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["perspective"], mock_scene_story_config
|
|
)
|
|
|
|
item = await handler.context_id_item_from_path(
|
|
"story_configuration",
|
|
["perspective"],
|
|
"story_configuration:perspective",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
new_value = "First person, present tense"
|
|
await item.set(mock_scene_story_config, new_value)
|
|
assert mock_scene_story_config.perspectives.default == new_value
|
|
|
|
# Setting to None should result in empty string
|
|
await item.set(mock_scene_story_config, None)
|
|
assert mock_scene_story_config.perspectives.default == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scene_perspective_context_id_from_path(mock_scene_story_config):
|
|
"""Test getting ScenePerspectiveContextID from path."""
|
|
handler = StoryConfigurationContext.instance_from_path(
|
|
["perspective"], mock_scene_story_config
|
|
)
|
|
|
|
context_id = await handler.context_id_from_path(
|
|
"story_configuration",
|
|
["perspective"],
|
|
"story_configuration:perspective",
|
|
mock_scene_story_config,
|
|
)
|
|
|
|
assert context_id is not None
|
|
assert isinstance(context_id, ScenePerspectiveContextID)
|
|
assert context_id.path == ["perspective"]
|
|
|
|
|
|
def test_scene_perspective_context_item_properties():
|
|
"""Test ScenePerspectiveContextID item properties."""
|
|
item = StoryConfigurationContextItem(
|
|
context_type="perspective",
|
|
name="perspective",
|
|
value="Third person omniscient",
|
|
)
|
|
|
|
context_id = item.context_id
|
|
assert isinstance(context_id, ScenePerspectiveContextID)
|
|
assert item.human_id == "Narrative Perspective"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scene_perspective_integration_flow(mock_scene_story_config):
|
|
"""Test complete integration flow for perspective context ID."""
|
|
context_id_str = "story_configuration:perspective"
|
|
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_story_config
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "perspective"
|
|
assert context_item.value == "Third person limited, past tense"
|
|
|
|
context_id = await context_id_from_string(context_id_str, mock_scene_story_config)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, ScenePerspectiveContextID)
|
|
|
|
original_value = await context_item.get(mock_scene_story_config)
|
|
assert original_value == "Third person limited, past tense"
|
|
|
|
new_value = "First person, present tense, from Kaira's perspective"
|
|
await context_item.set(mock_scene_story_config, new_value)
|
|
|
|
updated_value = await context_item.get(mock_scene_story_config)
|
|
assert updated_value == new_value
|
|
assert mock_scene_story_config.perspectives.default == new_value
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scene_perspective_with_empty_value(mock_scene_story_config):
|
|
"""Test perspective context ID when perspective is empty."""
|
|
mock_scene_story_config.perspectives.default = ""
|
|
|
|
context_id_str = "story_configuration:perspective"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_story_config
|
|
)
|
|
|
|
assert context_item is not None
|
|
value = await context_item.get(mock_scene_story_config)
|
|
assert value == ""
|
|
|
|
|
|
# Tests for character names containing dots (e.g., "M.A.R.V.I.N.")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene_dotted_names():
|
|
"""Create a mock scene with characters whose names contain dots."""
|
|
scene = MockScene()
|
|
|
|
char = Character(
|
|
name="M.A.R.V.I.N.",
|
|
description="A paranoid android",
|
|
acting_instructions="Speak in a depressed, monotone manner",
|
|
base_attributes={"intelligence": 50, "happiness": 0},
|
|
details={"appearance": "A humanoid robot with a large head"},
|
|
example_dialogue=["I think you ought to know I'm feeling very depressed."],
|
|
)
|
|
scene.character_data[char.name] = char
|
|
|
|
# Also add a normal character to ensure no regressions
|
|
char2 = Character(
|
|
name="Arthur",
|
|
description="A bewildered earthling",
|
|
base_attributes={"luck": 3},
|
|
details={"appearance": "Wearing a dressing gown"},
|
|
)
|
|
scene.character_data[char2.name] = char2
|
|
|
|
return scene
|
|
|
|
|
|
def test_dotted_name_instance_from_path(mock_scene_dotted_names):
|
|
"""Test that CharacterContext.instance_from_path resolves dotted character names."""
|
|
# "M.A.R.V.I.N.".split(".") produces ["M", "A", "R", "V", "I", "N", ""]
|
|
path = "M.A.R.V.I.N.".split(".")
|
|
handler = CharacterContext.instance_from_path(path, mock_scene_dotted_names)
|
|
assert handler.character.name == "M.A.R.V.I.N."
|
|
|
|
|
|
def test_dotted_name_normal_character_still_works(mock_scene_dotted_names):
|
|
"""Test that normal character names still work after the fix."""
|
|
handler = CharacterContext.instance_from_path(["Arthur"], mock_scene_dotted_names)
|
|
assert handler.character.name == "Arthur"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_handler_from_string(mock_scene_dotted_names):
|
|
"""Test context_id_handler_from_string with dotted character name."""
|
|
handler = context_id_handler_from_string(
|
|
"character.description:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert isinstance(handler, CharacterContext)
|
|
assert handler.character.name == "M.A.R.V.I.N."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_description_from_string(mock_scene_dotted_names):
|
|
"""Test resolving description context ID for a dotted character name."""
|
|
context_item = await context_id_item_from_string(
|
|
"character.description:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "description"
|
|
assert context_item.character.name == "M.A.R.V.I.N."
|
|
value = await context_item.get(mock_scene_dotted_names)
|
|
assert value == "A paranoid android"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_acting_instructions_from_string(mock_scene_dotted_names):
|
|
"""Test resolving acting_instructions context ID for a dotted character name."""
|
|
context_item = await context_id_item_from_string(
|
|
"character.acting_instructions:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "acting_instructions"
|
|
assert context_item.character.name == "M.A.R.V.I.N."
|
|
value = await context_item.get(mock_scene_dotted_names)
|
|
assert value == "Speak in a depressed, monotone manner"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_attribute_from_string(mock_scene_dotted_names):
|
|
"""Test resolving attribute context ID for a dotted character name."""
|
|
intelligence_hash = compress_name("intelligence")
|
|
context_id_str = f"character.attribute:M.A.R.V.I.N..{intelligence_hash}"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "attribute"
|
|
assert context_item.name == "intelligence"
|
|
assert context_item.value == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_detail_from_string(mock_scene_dotted_names):
|
|
"""Test resolving detail context ID for a dotted character name."""
|
|
appearance_hash = compress_name("appearance")
|
|
context_id_str = f"character.detail:M.A.R.V.I.N..{appearance_hash}"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "detail"
|
|
assert context_item.name == "appearance"
|
|
assert context_item.value == "A humanoid robot with a large head"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_example_dialogue_from_string(mock_scene_dotted_names):
|
|
"""Test resolving example_dialogue context ID for a dotted character name."""
|
|
# Access the full list
|
|
context_item = await context_id_item_from_string(
|
|
"character.example_dialogue:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.context_type == "example_dialogue"
|
|
assert context_item.character.name == "M.A.R.V.I.N."
|
|
|
|
# Access specific index
|
|
context_item = await context_id_item_from_string(
|
|
"character.example_dialogue:M.A.R.V.I.N..0", mock_scene_dotted_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.name == "0"
|
|
value = await context_item.get(mock_scene_dotted_names)
|
|
assert value == "I think you ought to know I'm feeling very depressed."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_context_id_from_string(mock_scene_dotted_names):
|
|
"""Test context_id_from_string produces correct context ID for dotted names."""
|
|
context_id = await context_id_from_string(
|
|
"character.description:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert context_id is not None
|
|
assert isinstance(context_id, CharacterDescriptionContextID)
|
|
assert context_id.character == "M.A.R.V.I.N."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_nonexistent_still_errors(mock_scene_dotted_names):
|
|
"""Test that non-existent dotted names still raise errors."""
|
|
with pytest.raises(ContextIDHandlerError):
|
|
context_id_handler_from_string(
|
|
"character.description:X.Y.Z.", mock_scene_dotted_names
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dotted_name_full_set_operation(mock_scene_dotted_names):
|
|
"""Test full get/set cycle for a dotted character name."""
|
|
# Memory agent registered as a real `MockMemoryAgent` via the
|
|
# module-scoped autouse fixture.
|
|
context_item = await context_id_item_from_string(
|
|
"character.description:M.A.R.V.I.N.", mock_scene_dotted_names
|
|
)
|
|
assert await context_item.get(mock_scene_dotted_names) == "A paranoid android"
|
|
await context_item.set(mock_scene_dotted_names, "An even more depressed android")
|
|
assert (
|
|
mock_scene_dotted_names.get_character("M.A.R.V.I.N.").description
|
|
== "An even more depressed android"
|
|
)
|
|
|
|
|
|
# Tests for overlapping character names with dots
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scene_overlapping_names():
|
|
"""Create a scene where one dotted name is a prefix of another."""
|
|
scene = MockScene()
|
|
|
|
# "M.A" is a prefix of "M.A.R.V.I.N."
|
|
short = Character(
|
|
name="M.A",
|
|
description="A short-named entity",
|
|
base_attributes={"speed": 5},
|
|
)
|
|
scene.character_data[short.name] = short
|
|
|
|
long = Character(
|
|
name="M.A.R.V.I.N.",
|
|
description="A paranoid android",
|
|
base_attributes={"intelligence": 50},
|
|
)
|
|
scene.character_data[long.name] = long
|
|
|
|
return scene
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overlapping_names_longest_match_wins(mock_scene_overlapping_names):
|
|
"""Test that the longest matching character name is selected."""
|
|
# Should match "M.A.R.V.I.N.", not "M.A"
|
|
context_item = await context_id_item_from_string(
|
|
"character.description:M.A.R.V.I.N.", mock_scene_overlapping_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.character.name == "M.A.R.V.I.N."
|
|
value = await context_item.get(mock_scene_overlapping_names)
|
|
assert value == "A paranoid android"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overlapping_names_short_name_still_resolves(
|
|
mock_scene_overlapping_names,
|
|
):
|
|
"""Test that the shorter name still resolves correctly."""
|
|
context_item = await context_id_item_from_string(
|
|
"character.description:M.A", mock_scene_overlapping_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.character.name == "M.A"
|
|
value = await context_item.get(mock_scene_overlapping_names)
|
|
assert value == "A short-named entity"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overlapping_names_attribute_on_long_name(mock_scene_overlapping_names):
|
|
"""Test attribute lookup resolves to the correct character when names overlap."""
|
|
intelligence_hash = compress_name("intelligence")
|
|
context_id_str = f"character.attribute:M.A.R.V.I.N..{intelligence_hash}"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_overlapping_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.character.name == "M.A.R.V.I.N."
|
|
assert context_item.name == "intelligence"
|
|
assert context_item.value == 50
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_overlapping_names_attribute_on_short_name(mock_scene_overlapping_names):
|
|
"""Test attribute lookup resolves to the shorter character when appropriate."""
|
|
speed_hash = compress_name("speed")
|
|
context_id_str = f"character.attribute:M.A.{speed_hash}"
|
|
context_item = await context_id_item_from_string(
|
|
context_id_str, mock_scene_overlapping_names
|
|
)
|
|
assert context_item is not None
|
|
assert context_item.character.name == "M.A"
|
|
assert context_item.name == "speed"
|
|
assert context_item.value == 5
|