Files
talemate/tests/server/test_scene_message.py
veguAI c12a82930e 0.38.0 (#272)
- **Per-Scene Agent Overrides** — Override agent configuration for a single scene without touching your global config.
- **Message Revision History** — Browse and continue from previous regenerations / revisions.
- **Scene Perspective Overrides** — Set distinct narrative perspectives for the player, NPCs, and narrator.
- **Durable World State Snapshot** — The world state snapshot now persists and updates entities across refreshes.
- **World State Highlights** — Characters, objects and places marked by the current world state snapshot are now clickable in the recent message(s) for additional detail.

Plus 30 improvements and 17 bug fixes
2026-07-02 09:28:45 +03:00

266 lines
8.9 KiB
Python

"""
Unit tests for the scene_message websocket plugin.
The plugin exposes three actions that map to three distinct scene-level
operations:
- ``edit`` → ``Scene.edit_message`` (replace active version's text)
- ``append_version`` → ``Scene.append_message_version`` (grow the stack)
- ``swap_revision`` → ``Scene.set_message_active_version`` (move pointer)
The editor agent's ``cleanup_character_message`` branch in ``handle_edit``
is bypassed by stubbing the agent to ``enabled=False``; that branch is
exercised in its own agent tests and isn't the contract under test here.
"""
import pytest
from talemate.scene_message import CharacterMessage, reset_message_id
from talemate.server.scene_message import SceneMessagePlugin
from talemate.tale_mate import Scene
class _MockWebsocketHandler:
"""Minimal handler stand-in: exposes ``scene`` and a queue_put list."""
def __init__(self, scene):
self._scene = scene
self.messages: list[dict] = []
@property
def scene(self):
return self._scene
def queue_put(self, data):
self.messages.append(data)
class _DisabledEditor:
"""Stub editor agent; the plugin checks ``enabled`` and short-circuits."""
enabled = False
@pytest.fixture(autouse=True)
def _reset_message_ids():
reset_message_id()
yield
reset_message_id()
@pytest.fixture
def scene_with_message():
"""Real Scene carrying one CharacterMessage. Tests target its id."""
scene = Scene()
msg = CharacterMessage(message="Alice: Hello.")
scene.history.append(msg)
return scene, msg
@pytest.fixture
def plugin(scene_with_message, monkeypatch):
"""SceneMessagePlugin wired to a real Scene with editor cleanup disabled.
Scene method calls are captured via spies so tests can assert which
of the three scene-level operations was dispatched and with what
arguments — no emit bus runs.
"""
scene, _ = scene_with_message
handler = _MockWebsocketHandler(scene=scene)
plug = SceneMessagePlugin(handler)
monkeypatch.setattr(
"talemate.server.scene_message.instance.get_agent",
lambda name: _DisabledEditor(),
)
calls: dict[str, list[dict]] = {
"edit": [],
"append_version": [],
"swap_revision": [],
"delete": [],
}
# Linear log capturing inter-call ordering across spies (e.g. mutate
# then autosave). Each entry is a (name, payload) tuple.
order: list[tuple[str, object]] = []
def _spy_edit(message_id, text):
entry = {"message_id": message_id, "text": text}
calls["edit"].append(entry)
order.append(("edit", entry))
def _spy_append(message_id, text, source, reason=None):
entry = {
"message_id": message_id,
"text": text,
"source": source,
"reason": reason,
}
calls["append_version"].append(entry)
order.append(("append_version", entry))
def _spy_swap(message_id, index):
entry = {"message_id": message_id, "index": index}
calls["swap_revision"].append(entry)
order.append(("swap_revision", entry))
def _spy_delete(message_id):
calls["delete"].append(message_id)
order.append(("delete", message_id))
async def _spy_auto_save():
order.append(("auto_save", None))
monkeypatch.setattr(scene, "edit_message", _spy_edit)
monkeypatch.setattr(scene, "append_message_version", _spy_append)
monkeypatch.setattr(scene, "set_message_active_version", _spy_swap)
monkeypatch.setattr(scene, "delete_message", _spy_delete)
monkeypatch.setattr(scene, "attempt_auto_save", _spy_auto_save)
plug._spy_calls = calls
plug._spy_order = order
return plug
class TestSceneMessagePluginHandleEdit:
@pytest.mark.asyncio
async def test_plain_edit_replaces_active_version(self, plugin, scene_with_message):
"""``handle_edit`` dispatches to ``Scene.edit_message`` — the
active version's text is rewritten in place, no new stack
entry is created."""
_, msg = scene_with_message
await plugin.handle_edit({"id": msg.id, "text": "Alice: Hi."})
assert plugin._spy_calls["edit"] == [
{"message_id": msg.id, "text": "Alice: Hi."}
]
assert plugin._spy_calls["append_version"] == []
assert plugin._spy_calls["swap_revision"] == []
@pytest.mark.asyncio
async def test_edit_drops_extra_payload_fields(self, plugin, scene_with_message):
"""``EditPayload`` only declares ``id`` and ``text``; any other
fields a client tries to inject (legacy ``reason`` /
``mutation_source``, hypothetical ``source``, etc.) are dropped
by pydantic before reaching ``Scene.edit_message``."""
_, msg = scene_with_message
await plugin.handle_edit(
{
"id": msg.id,
"text": "Alice: Hi.",
"source": "custom",
"reason": "should not propagate",
}
)
assert plugin._spy_calls["edit"] == [
{"message_id": msg.id, "text": "Alice: Hi."}
]
class TestSceneMessagePluginHandleAppendVersion:
@pytest.mark.asyncio
async def test_append_forwards_source_and_reason(self, plugin, scene_with_message):
"""``handle_append_version`` dispatches to
``Scene.append_message_version`` with the wire-supplied
``source`` and optional ``reason``."""
_, msg = scene_with_message
await plugin.handle_append_version(
{
"id": msg.id,
"text": "Alice: Hi. How are you?",
"source": "continue",
}
)
assert plugin._spy_calls["append_version"] == [
{
"message_id": msg.id,
"text": "Alice: Hi. How are you?",
"source": "continue",
"reason": None,
}
]
@pytest.mark.asyncio
async def test_append_defaults_source_to_custom(self, plugin, scene_with_message):
"""Omitting ``source`` defaults to ``"custom"`` — the catch-all
bucket for client-driven mutations that don't fit the closed
revision/regenerate/continue set."""
_, msg = scene_with_message
await plugin.handle_append_version(
{"id": msg.id, "text": "Alice: rewritten", "reason": "manual override"}
)
assert plugin._spy_calls["append_version"] == [
{
"message_id": msg.id,
"text": "Alice: rewritten",
"source": "custom",
"reason": "manual override",
}
]
class TestSceneMessagePluginHandleSwapRevision:
@pytest.mark.asyncio
async def test_swap_forwards_index(self, plugin, scene_with_message):
"""``handle_swap_revision`` takes an index and forwards it to
``Scene.set_message_active_version`` — the canonical text follows
the pointer rather than being set explicitly by the client."""
_, msg = scene_with_message
await plugin.handle_swap_revision({"id": msg.id, "index": 2})
assert plugin._spy_calls["swap_revision"] == [
{"message_id": msg.id, "index": 2}
]
class TestSceneMessagePluginHandleDelete:
@pytest.mark.asyncio
async def test_delete_forwards_id(self, plugin, scene_with_message):
_, msg = scene_with_message
await plugin.handle_delete({"id": msg.id})
assert plugin._spy_calls["delete"] == [msg.id]
class TestSceneMessagePluginAutosavesAfterMutation:
"""All four mutation handlers must autosave AFTER their mutation so the
change reaches the on-disk changelog — forks taken from the changelog
UI read from disk, not from scene memory.
"""
@pytest.mark.asyncio
async def test_delete(self, plugin, scene_with_message):
_, msg = scene_with_message
await plugin.handle_delete({"id": msg.id})
names = [name for name, _ in plugin._spy_order]
assert names == ["delete", "auto_save"]
@pytest.mark.asyncio
async def test_edit(self, plugin, scene_with_message):
_, msg = scene_with_message
await plugin.handle_edit({"id": msg.id, "text": "Alice: Hi."})
names = [name for name, _ in plugin._spy_order]
assert names == ["edit", "auto_save"]
@pytest.mark.asyncio
async def test_append_version(self, plugin, scene_with_message):
_, msg = scene_with_message
await plugin.handle_append_version(
{"id": msg.id, "text": "Alice: more.", "source": "continue"}
)
names = [name for name, _ in plugin._spy_order]
assert names == ["append_version", "auto_save"]
@pytest.mark.asyncio
async def test_swap_revision(self, plugin, scene_with_message):
_, msg = scene_with_message
await plugin.handle_swap_revision({"id": msg.id, "index": 2})
names = [name for name, _ in plugin._spy_order]
assert names == ["swap_revision", "auto_save"]