Files
talemate/tests/test_context_id.py
veguAI 37e59719cc Fix: director-set story introduction now updates the scene main view immediately (#191) (#192)
* fix: emit scene history and status on all intro write paths so the scene view updates immediately (#191)

* review: trim unreachable episode path from changelog entry, fix stale test docstring and fold duplicate status assertion
2026-08-06 02:52:38 +03:00

1656 lines
59 KiB
Python

import pytest
from unittest.mock import AsyncMock, MagicMock
import talemate.instance as instance
from conftest import MockMemoryAgent, MockScene, bootstrap_scene
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 Story Introduction Context ID
@pytest.mark.asyncio
async def test_story_introduction_context_item_set(mock_scene_story_config):
"""Test setting the story introduction through context item."""
# `context_id_item_from_path` reads through `scene.get_intro`, which needs
# the editor agent.
bootstrap_scene(mock_scene_story_config)
handler = StoryConfigurationContext.instance_from_path(
["introduction"], mock_scene_story_config
)
item = await handler.context_id_item_from_path(
"story_configuration",
["introduction"],
"story_configuration:introduction",
mock_scene_story_config,
)
mock_scene_story_config.emit_status = MagicMock()
mock_scene_story_config.emit_history = AsyncMock()
new_intro = "A brand new introduction."
await item.set(mock_scene_story_config, new_intro)
assert mock_scene_story_config.intro == new_intro
# The main view only renders the intro from an emit_history pass, the
# World Editor off scene status.
mock_scene_story_config.emit_status.assert_called_once()
mock_scene_story_config.emit_history.assert_awaited_once()
@pytest.mark.asyncio
async def test_story_configuration_set_only_emits_history_for_introduction(
mock_scene_story_config,
):
"""Other story configuration values must not trigger a history re-emit."""
handler = StoryConfigurationContext.instance_from_path(
["description"], mock_scene_story_config
)
item = await handler.context_id_item_from_path(
"story_configuration",
["description"],
"story_configuration:description",
mock_scene_story_config,
)
mock_scene_story_config.emit_status = MagicMock()
mock_scene_story_config.emit_history = AsyncMock()
await item.set(mock_scene_story_config, "A different description")
assert mock_scene_story_config.description == "A different description"
mock_scene_story_config.emit_status.assert_not_called()
mock_scene_story_config.emit_history.assert_not_awaited()
# 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