Files
talemate/tests/test_template_uid_tracking.py

211 lines
6.7 KiB
Python
Raw Normal View History

0.36.0 (#255) Major Features - API key encryption at rest using Fernet (OS keyring with file fallback) - Prompt Manager: unified UI with template groups, priority ordering, override tracking, response extractors - Scene context history review panel with token budgets and best-fit mode - Multiple concurrent director chats with auto-generated titles - Granular scene state reset dialog - Time passage insert/edit/delete in scene view - Image analysis via OpenAI-compatible and Talemate Client backends - Volatile context placement after scene history for improved prompt caching Improvements - Configurable narrator generation length per narration type - AI Aware conversation mode - Summarizer: custom instructions, writing style inclusion, short line filtering - Anthropic: adaptive thinking support, updated model list (opus-4-5/4-6, haiku-4-5) - Google: gemini-3.1 support - World editor: generate from topic, quick create state reinforcement, reorganized menus - Node editor: promote scene modules to global - Frontend: version mismatch detection, hideable bracket content, required scene name - TTS: improved pause handling, audio tag support for vocal markers (ElevenLabs v3) - Writing style template for AI-generated instructions - Added Kimi.jinja2 LLM prompt template - Option to disable character names in stopping strings - Client response length enforcement options - Graduated token count sliders - Increased summarizer token threshold max Bugfixes - Fix bracket/paren/brace terminators stripped from message ends - Fix colon in conversation causing content loss - Fix "Use as reference" navigating to blank page - Fix avatar regeneration and manual regenerate - Fix conversation agent ignoring generation length - Fix duplicate length instructions with reasoning enabled - Fix trailing newline on message edits - Fix summarize dialogue sending too much context with layered history - Fix layered history inspection and construction issues - Fix empty response handling in summarization - Fix context ID dot notation with dotted character names - Fix recursive retry in focal agent - Fix leading whitespace causing duplicate prepared responses - Fix summarization not stripping ANALYSIS OF lines - Fix template group selection/removal in prompt manager - Fix multiline text in parentheses/brackets parser - Fix determine_character_name resolution - Fix character activate/deactivate desyncing creative menu - Fix character image generation missing context - Fix LMStudio client not sending token limits - Fix Recent Scene images on newer Chromium - Fix sequential reinforcement messages cut off at first linebreak - Fix reinforcement removal not clearing state - Fixes #252, #256, #258 Deprecations - Removed context investigations (replaced by AI-assisted RAG mixin) - Removed deprecated prompt templates (fix-continuity-errors, fix-exposition, etc.) - Removed conversation/edit.jinja2, auto break repetition, CLI reset layered history --------- Co-authored-by: theDTV2 <47825738+theDTV2@users.noreply.github.com>
2026-03-15 12:00:57 +02:00
"""
Unit tests for template_uid tracking in PromptData.
Tests that when a Prompt is sent via Prompt.send(), the template_uid is correctly
passed through to the PromptData emitted via the prompt_sent signal.
"""
import pytest
from unittest.mock import AsyncMock
from talemate.client.base import PromptData
from talemate.prompts.base import Prompt, active_template_uid
class TestPromptDataTemplateUid:
"""Tests for the template_uid field in PromptData."""
def test_prompt_data_has_template_uid_field(self):
"""Verify PromptData model includes template_uid field."""
data = PromptData(
kind="test",
prompt="test prompt",
response="test response",
prompt_tokens=10,
response_tokens=5,
client_name="test-client",
client_type="test",
time=1.0,
template_uid="narrator.narrate-scene",
)
assert data.template_uid == "narrator.narrate-scene"
def test_prompt_data_template_uid_defaults_to_none(self):
"""Verify template_uid defaults to None when not provided."""
data = PromptData(
kind="test",
prompt="test prompt",
response="test response",
prompt_tokens=10,
response_tokens=5,
client_name="test-client",
client_type="test",
time=1.0,
)
assert data.template_uid is None
def test_prompt_data_serializes_template_uid(self):
"""Verify template_uid is included in model_dump() output."""
data = PromptData(
kind="test",
prompt="test prompt",
response="test response",
prompt_tokens=10,
response_tokens=5,
client_name="test-client",
client_type="test",
time=1.0,
template_uid="director.guide-scene",
)
dumped = data.model_dump()
assert "template_uid" in dumped
assert dumped["template_uid"] == "director.guide-scene"
class TestActiveTemplateUidContext:
"""Tests for the active_template_uid context variable."""
def test_active_template_uid_defaults_to_none(self):
"""Verify active_template_uid defaults to None."""
assert active_template_uid.get() is None
def test_active_template_uid_can_be_set_and_reset(self):
"""Verify context variable can be set and properly reset."""
assert active_template_uid.get() is None
token = active_template_uid.set("narrator.test")
assert active_template_uid.get() == "narrator.test"
active_template_uid.reset(token)
assert active_template_uid.get() is None
def test_active_template_uid_nested_contexts(self):
"""Verify nested context variable handling."""
assert active_template_uid.get() is None
token1 = active_template_uid.set("outer.template")
assert active_template_uid.get() == "outer.template"
token2 = active_template_uid.set("inner.template")
assert active_template_uid.get() == "inner.template"
active_template_uid.reset(token2)
assert active_template_uid.get() == "outer.template"
active_template_uid.reset(token1)
assert active_template_uid.get() is None
class TestPromptSendSetsTemplateUid:
"""Tests that Prompt.send() properly sets the active_template_uid context."""
@pytest.fixture
def mock_client(self):
"""Create a mock LLM client."""
client = AsyncMock()
client.send_prompt = AsyncMock(return_value="test response")
client.can_be_coerced = True
client.data_format = "json"
return client
@pytest.mark.asyncio
async def test_prompt_send_sets_context_variable(self, mock_client):
"""Verify Prompt.send() sets active_template_uid during client call."""
captured_uid = None
async def capture_uid(*args, **kwargs):
nonlocal captured_uid
captured_uid = active_template_uid.get()
return "test response"
mock_client.send_prompt = capture_uid
prompt = Prompt(
uid="narrator.test-template",
agent_type="narrator",
name="test-template",
template="Test prompt content",
)
await prompt.send(mock_client, kind="test")
assert captured_uid == "narrator.test-template"
@pytest.mark.asyncio
async def test_prompt_send_resets_context_after_completion(self, mock_client):
"""Verify context is reset after Prompt.send() completes."""
assert active_template_uid.get() is None
prompt = Prompt(
uid="narrator.test",
agent_type="narrator",
name="test",
template="Test prompt",
)
await prompt.send(mock_client, kind="test")
# Context should be reset after send completes
assert active_template_uid.get() is None
@pytest.mark.asyncio
async def test_prompt_send_resets_context_on_exception(self, mock_client):
"""Verify context is reset even if client.send_prompt raises."""
mock_client.send_prompt = AsyncMock(side_effect=Exception("Test error"))
prompt = Prompt(
uid="narrator.test",
agent_type="narrator",
name="test",
template="Test prompt",
)
with pytest.raises(Exception, match="Test error"):
await prompt.send(mock_client, kind="test")
# Context should still be reset
assert active_template_uid.get() is None
@pytest.mark.asyncio
async def test_prompt_send_with_empty_uid(self, mock_client):
"""Verify empty uid is handled gracefully."""
captured_uid = None
async def capture_uid(*args, **kwargs):
nonlocal captured_uid
captured_uid = active_template_uid.get()
return "test response"
mock_client.send_prompt = capture_uid
prompt = Prompt(
uid="",
agent_type="",
name="",
template="Test prompt",
)
await prompt.send(mock_client, kind="test")
# Empty string should be converted to None
assert captured_uid is None
@pytest.mark.asyncio
async def test_prompt_from_text_has_no_uid(self, mock_client):
"""Verify Prompt.from_text() results in None template_uid."""
captured_uid = None
async def capture_uid(*args, **kwargs):
nonlocal captured_uid
captured_uid = active_template_uid.get()
return "test response"
mock_client.send_prompt = capture_uid
prompt = Prompt.from_text("Some raw prompt text")
await prompt.send(mock_client, kind="test")
# from_text creates a prompt with empty uid
assert captured_uid is None