mirror of
https://github.com/vegu-ai/talemate.git
synced 2025-12-16 11:47:48 +01:00
1029 lines
36 KiB
Python
1029 lines
36 KiB
Python
|
|
import pytest
|
||
|
|
from unittest.mock import patch, AsyncMock
|
||
|
|
|
||
|
|
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.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
|
||
|
|
|
||
|
|
|
||
|
|
class MockScene:
|
||
|
|
"""Mock scene for testing context handlers."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.characters = {}
|
||
|
|
|
||
|
|
def get_character(self, name: str) -> Character:
|
||
|
|
return self.characters.get(name)
|
||
|
|
|
||
|
|
def add_character(self, character: Character):
|
||
|
|
self.characters[character.name] = character
|
||
|
|
|
||
|
|
|
||
|
|
@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.add_character(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.add_character(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
|
||
|
|
|
||
|
|
# Mock the memory agent since we don't have a real one in tests
|
||
|
|
mock_memory_agent = AsyncMock()
|
||
|
|
with patch("talemate.instance.get_agent", return_value=mock_memory_agent):
|
||
|
|
# 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."""
|
||
|
|
|
||
|
|
# Mock memory agent
|
||
|
|
mock_memory_agent = AsyncMock()
|
||
|
|
with patch("talemate.instance.get_agent", return_value=mock_memory_agent):
|
||
|
|
# 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.add_character(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
|