mirror of
https://github.com/vegu-ai/talemate.git
synced 2026-05-18 05:05:39 +02:00
0.37.0 - **Director Planning** — Multi-step todo lists in director chat plus a Generate long progress action for multi-beat scene arcs. - **Auto Narration** — Unified auto-narration replacing the old Narrate after Dialogue toggle, with a chance slider and weighted action mix. - **LLM Prompt Templates Manager** — Dedicated UI tab for viewing, creating, editing, and deleting prompt templates. - **Character Folders** — Collapsible folders in the World Editor character list, synced across linked scenes. - **OpenAI Compatible TTS** — Connect any number of OpenAI-compatible TTS servers in parallel. - **KoboldCpp TTS Auto-Setup** — KoboldCpp clients with a TTS model loaded register themselves as a TTS backend. - **Model Testing Harness** — Bundled scene that runs basic capability tests against any connected LLM. Plus 27 improvements and 28 bug fixes
686 lines
25 KiB
Python
686 lines
25 KiB
Python
"""Unit tests for talemate.scene_message message dataclasses and helpers."""
|
|
|
|
import pytest
|
|
|
|
from talemate.scene_message import (
|
|
CharacterMessage,
|
|
ContextInvestigationMessage,
|
|
DIRECTOR_INPUT_PREFIX,
|
|
DIRECTOR_INPUT_PREFIX_YIELD,
|
|
DirectorMessage,
|
|
Flags,
|
|
MESSAGES,
|
|
NarratorMessage,
|
|
ReinforcementMessage,
|
|
SceneMessage,
|
|
TimePassageMessage,
|
|
get_message_id,
|
|
reset_message_id,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures / helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_message_id():
|
|
"""Each test starts with a clean message id counter."""
|
|
reset_message_id()
|
|
yield
|
|
reset_message_id()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level constants & helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDirectorInputPrefixes:
|
|
def test_yield_prefix_is_two_hashes(self):
|
|
# The yield variant must be checked first since it is a superset of the
|
|
# single-character prefix.
|
|
assert DIRECTOR_INPUT_PREFIX_YIELD == "##"
|
|
assert DIRECTOR_INPUT_PREFIX == "#"
|
|
assert DIRECTOR_INPUT_PREFIX_YIELD.startswith(DIRECTOR_INPUT_PREFIX)
|
|
|
|
|
|
class TestMessageIdHelpers:
|
|
def test_get_message_id_increments(self):
|
|
first = get_message_id()
|
|
second = get_message_id()
|
|
third = get_message_id()
|
|
assert (first, second, third) == (1, 2, 3)
|
|
|
|
def test_reset_message_id_returns_counter_to_zero(self):
|
|
get_message_id()
|
|
get_message_id()
|
|
reset_message_id()
|
|
assert get_message_id() == 1
|
|
|
|
def test_default_factory_uses_global_counter(self):
|
|
# SceneMessage default factory pulls the next id from the helper
|
|
m1 = SceneMessage(message="hello")
|
|
m2 = SceneMessage(message="world")
|
|
assert m2.id == m1.id + 1
|
|
|
|
|
|
class TestMessagesRegistry:
|
|
def test_known_types_route_to_correct_class(self):
|
|
assert MESSAGES["scene"] is SceneMessage
|
|
assert MESSAGES["character"] is CharacterMessage
|
|
assert MESSAGES["narrator"] is NarratorMessage
|
|
assert MESSAGES["director"] is DirectorMessage
|
|
assert MESSAGES["time"] is TimePassageMessage
|
|
assert MESSAGES["reinforcement"] is ReinforcementMessage
|
|
assert MESSAGES["context_investigation"] is ContextInvestigationMessage
|
|
|
|
def test_typ_attr_matches_registry_key(self):
|
|
for key, cls in MESSAGES.items():
|
|
assert cls.typ == key, f"{cls.__name__}.typ != registry key {key!r}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Flags
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFlags:
|
|
def test_none_flag_is_zero(self):
|
|
assert int(Flags.NONE) == 0
|
|
|
|
def test_hidden_flag_set_via_hide(self):
|
|
msg = SceneMessage(message="hi")
|
|
assert msg.hidden == Flags.NONE
|
|
msg.hide()
|
|
assert msg.flags & Flags.HIDDEN
|
|
assert bool(msg.hidden)
|
|
|
|
def test_unhide_clears_hidden_flag(self):
|
|
msg = SceneMessage(message="hi", flags=Flags.HIDDEN)
|
|
assert msg.hidden
|
|
msg.unhide()
|
|
assert not msg.hidden
|
|
assert msg.flags == Flags.NONE
|
|
|
|
def test_hide_is_idempotent(self):
|
|
msg = SceneMessage(message="hi")
|
|
msg.hide()
|
|
msg.hide()
|
|
# Setting the same bit twice keeps the value identical.
|
|
assert msg.flags == Flags.HIDDEN
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SceneMessage base class
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSceneMessageBase:
|
|
def test_str_returns_message(self):
|
|
m = SceneMessage(message="hello world")
|
|
assert str(m) == "hello world"
|
|
|
|
def test_int_returns_id(self):
|
|
m = SceneMessage(message="x")
|
|
assert int(m) == m.id
|
|
|
|
def test_len_returns_message_length(self):
|
|
m = SceneMessage(message="hello")
|
|
assert len(m) == 5
|
|
|
|
def test_iter_yields_characters(self):
|
|
m = SceneMessage(message="ab")
|
|
assert list(iter(m)) == ["a", "b"]
|
|
|
|
def test_split_delegates_to_message(self):
|
|
m = SceneMessage(message="a:b:c")
|
|
assert m.split(":") == ["a", "b", "c"]
|
|
|
|
def test_startswith_endswith_delegate_to_message(self):
|
|
m = SceneMessage(message="hello world")
|
|
assert m.startswith("hello")
|
|
assert m.endswith("world")
|
|
assert not m.startswith("world")
|
|
|
|
def test_contains_message_in_other(self):
|
|
m = SceneMessage(message="cat")
|
|
# __contains__ is overridden to test self.message in other (reversed
|
|
# semantics from the typical container usage).
|
|
assert "the cat sat".__contains__ # sanity
|
|
assert "the cat sat".__contains__ is not None
|
|
# The dataclass override returns True when the message is contained
|
|
# within `other`.
|
|
assert m.__contains__("the cat sat") is True
|
|
assert m.__contains__("dog only") is False
|
|
|
|
def test_dict_returns_serialisable_payload(self):
|
|
m = SceneMessage(message="hi", source="ai", flags=Flags.HIDDEN)
|
|
d = m.__dict__()
|
|
assert d["message"] == "hi"
|
|
assert d["typ"] == "scene"
|
|
assert d["source"] == "ai"
|
|
assert d["flags"] == int(Flags.HIDDEN)
|
|
assert d["rev"] == 0
|
|
assert "id" in d
|
|
assert "meta" not in d # omitted when None
|
|
|
|
def test_dict_includes_meta_when_set(self):
|
|
m = SceneMessage(message="hi", meta={"agent": "narrator"})
|
|
d = m.__dict__()
|
|
assert d["meta"] == {"agent": "narrator"}
|
|
|
|
def test_raw_returns_message_as_string(self):
|
|
m = SceneMessage(message="hello")
|
|
assert m.raw == "hello"
|
|
|
|
def test_secondary_source_falls_back_to_source(self):
|
|
m = SceneMessage(message="hi", source="ai")
|
|
assert m.secondary_source == "ai"
|
|
|
|
def test_set_source_populates_meta(self):
|
|
m = SceneMessage(message="hi")
|
|
m.set_source("narrator", "narrate_scene", topic="weather")
|
|
assert m.meta == {
|
|
"agent": "narrator",
|
|
"function": "narrate_scene",
|
|
"arguments": {"topic": "weather"},
|
|
}
|
|
assert m.source_agent == "narrator"
|
|
assert m.source_function == "narrate_scene"
|
|
assert m.source_arguments == {"topic": "weather"}
|
|
|
|
def test_set_meta_merges_into_existing_meta(self):
|
|
m = SceneMessage(message="hi")
|
|
m.set_meta(a=1)
|
|
m.set_meta(b=2)
|
|
assert m.meta == {"a": 1, "b": 2}
|
|
|
|
def test_source_helpers_default_when_meta_missing(self):
|
|
m = SceneMessage(message="hi")
|
|
assert m.source_agent is None
|
|
assert m.source_function is None
|
|
assert m.source_arguments == {}
|
|
|
|
def test_meta_hash_changes_with_meta(self):
|
|
m = SceneMessage(message="hi")
|
|
h1 = m.meta_hash
|
|
m.set_meta(x=1)
|
|
h2 = m.meta_hash
|
|
assert h1 != h2
|
|
|
|
def test_fingerprint_is_stable_for_same_message(self):
|
|
a = SceneMessage(message="same")
|
|
b = SceneMessage(message="same")
|
|
assert a.fingerprint == b.fingerprint
|
|
assert len(a.fingerprint) <= 16
|
|
|
|
def test_fingerprint_differs_for_different_messages(self):
|
|
a = SceneMessage(message="alpha")
|
|
b = SceneMessage(message="beta")
|
|
assert a.fingerprint != b.fingerprint
|
|
|
|
def test_as_format_movie_script_appends_newline(self):
|
|
m = SceneMessage(message="hello\n\n")
|
|
# rstrips trailing newlines then adds exactly one.
|
|
assert m.as_format("movie_script") == "hello\n"
|
|
assert m.as_format("ai_aware") == "hello\n"
|
|
|
|
def test_as_format_narrative_strips_whitespace(self):
|
|
m = SceneMessage(message=" hello \n")
|
|
assert m.as_format("narrative") == "hello"
|
|
|
|
def test_as_format_unknown_returns_raw_message(self):
|
|
m = SceneMessage(message="raw message")
|
|
assert m.as_format("nonsense") == "raw message"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CharacterMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCharacterMessage:
|
|
def test_character_name_extracts_prefix(self):
|
|
m = CharacterMessage(message="Alice: Hello there!")
|
|
assert m.character_name == "Alice"
|
|
assert m.secondary_source == "Alice"
|
|
|
|
def test_without_name_returns_dialogue_part(self):
|
|
m = CharacterMessage(message="Alice: Hello there!")
|
|
assert m.without_name == " Hello there!"
|
|
|
|
def test_raw_strips_quotes_and_asterisks(self):
|
|
m = CharacterMessage(message='Alice: "Hello" *waves*')
|
|
# The raw property removes asterisks and quotes from the dialogue.
|
|
assert m.raw == "Hello waves"
|
|
|
|
def test_as_movie_script_uppercases_name(self):
|
|
m = CharacterMessage(message="Alice: Hello")
|
|
assert m.as_movie_script == "\nALICE\nHello\nEND-OF-LINE\n"
|
|
|
|
def test_as_movie_script_handles_missing_colon_gracefully(self):
|
|
m = CharacterMessage(message="just text without a colon")
|
|
# When the message does not contain a colon, character_name returns
|
|
# the entire message and the script falls back to using it whole.
|
|
result = m.as_movie_script
|
|
assert "END-OF-LINE" in result
|
|
assert "JUST TEXT WITHOUT A COLON" in result
|
|
|
|
def test_as_format_movie_script_returns_movie_script(self):
|
|
m = CharacterMessage(message="Alice: Hello")
|
|
assert m.as_format("movie_script") == m.as_movie_script
|
|
assert m.as_format("ai_aware") == m.as_movie_script
|
|
|
|
def test_as_format_narrative_returns_dialogue_only(self):
|
|
m = CharacterMessage(message="Alice: Hello there")
|
|
assert m.as_format("narrative") == "Hello there"
|
|
|
|
def test_as_format_default_returns_full_message(self):
|
|
m = CharacterMessage(message="Alice: Hello")
|
|
assert m.as_format("chat") == "Alice: Hello"
|
|
|
|
def test_dict_includes_optional_fields_when_set(self):
|
|
m = CharacterMessage(
|
|
message="Alice: hi",
|
|
from_choice="choice-1",
|
|
asset_id="asset-123",
|
|
asset_type="avatar",
|
|
)
|
|
d = m.__dict__()
|
|
assert d["from_choice"] == "choice-1"
|
|
assert d["asset_id"] == "asset-123"
|
|
assert d["asset_type"] == "avatar"
|
|
|
|
def test_dict_omits_unset_optional_fields(self):
|
|
m = CharacterMessage(message="Alice: hi")
|
|
d = m.__dict__()
|
|
assert "from_choice" not in d
|
|
assert "asset_id" not in d
|
|
assert "asset_type" not in d
|
|
|
|
def test_default_source_is_ai(self):
|
|
m = CharacterMessage(message="Alice: hi")
|
|
assert m.source == "ai"
|
|
assert m.typ == "character"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# NarratorMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestNarratorMessage:
|
|
def test_default_typ_is_narrator(self):
|
|
m = NarratorMessage(message="The sun rises.")
|
|
assert m.typ == "narrator"
|
|
assert m.source == "ai"
|
|
|
|
def test_source_to_meta_paraphrase(self):
|
|
m = NarratorMessage(message="x", source="paraphrase:original text")
|
|
meta = m.source_to_meta()
|
|
assert meta == {
|
|
"agent": "narrator",
|
|
"function": "paraphrase",
|
|
"arguments": {"narration": "original text"},
|
|
}
|
|
|
|
def test_source_to_meta_narrate_character_entry(self):
|
|
m = NarratorMessage(message="x", source="narrate_character_entry:Bob")
|
|
meta = m.source_to_meta()
|
|
assert meta["function"] == "narrate_character_entry"
|
|
assert meta["arguments"] == {"character": "Bob"}
|
|
|
|
def test_source_to_meta_narrate_query(self):
|
|
m = NarratorMessage(message="x", source="narrate_query:What time is it?")
|
|
meta = m.source_to_meta()
|
|
assert meta["function"] == "narrate_query"
|
|
assert meta["arguments"] == {"query": "What time is it?"}
|
|
|
|
def test_source_to_meta_narrate_time_passage(self):
|
|
m = NarratorMessage(
|
|
message="x", source="narrate_time_passage:1h:1 hour later:they slept"
|
|
)
|
|
meta = m.source_to_meta()
|
|
assert meta["function"] == "narrate_time_passage"
|
|
assert meta["arguments"] == {
|
|
"duration": "1h",
|
|
"time_passed": "1 hour later",
|
|
"narrative": "they slept",
|
|
}
|
|
|
|
def test_source_to_meta_progress_story(self):
|
|
m = NarratorMessage(message="x", source="progress_story:advance plot")
|
|
meta = m.source_to_meta()
|
|
assert meta["function"] == "progress_story"
|
|
assert meta["arguments"] == {"narrative_direction": "advance plot"}
|
|
|
|
def test_source_to_meta_unknown_action_returns_empty_arguments(self):
|
|
m = NarratorMessage(message="x", source="unknown_action:foo")
|
|
meta = m.source_to_meta()
|
|
assert meta["function"] == "unknown_action"
|
|
assert meta["arguments"] == {}
|
|
|
|
def test_migrate_source_to_meta_populates_meta(self):
|
|
m = NarratorMessage(
|
|
message="The sun rises.", source="paraphrase:earlier paraphrase"
|
|
)
|
|
assert m.meta is None
|
|
m.migrate_source_to_meta()
|
|
assert m.meta is not None
|
|
assert m.meta["agent"] == "narrator"
|
|
assert m.meta["function"] == "paraphrase"
|
|
|
|
def test_migrate_source_to_meta_skips_when_meta_already_set(self):
|
|
existing = {"agent": "narrator", "function": "preset", "arguments": {}}
|
|
m = NarratorMessage(
|
|
message="x", source="paraphrase:would clobber", meta=dict(existing)
|
|
)
|
|
m.migrate_source_to_meta()
|
|
assert m.meta == existing
|
|
|
|
def test_migrate_source_to_meta_handles_malformed_source(self):
|
|
# narrate_time_passage requires three colon-separated parts; missing
|
|
# parts should be caught by the migration helper and the meta should
|
|
# remain unset rather than raising.
|
|
m = NarratorMessage(message="x", source="narrate_time_passage:only one part")
|
|
m.migrate_source_to_meta()
|
|
assert m.meta is None
|
|
|
|
def test_dict_includes_asset_fields(self):
|
|
m = NarratorMessage(
|
|
message="text", asset_id="img-1", asset_type="scene_illustration"
|
|
)
|
|
d = m.__dict__()
|
|
assert d["asset_id"] == "img-1"
|
|
assert d["asset_type"] == "scene_illustration"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DirectorMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDirectorMessage:
|
|
def test_character_name_pulled_from_meta(self):
|
|
m = DirectorMessage(
|
|
message="be brave",
|
|
meta={"agent": "director", "function": "x", "character": "Alice"},
|
|
)
|
|
assert m.character_name == "Alice"
|
|
|
|
def test_character_name_none_when_meta_missing(self):
|
|
m = DirectorMessage(message="be brave")
|
|
assert m.character_name is None
|
|
|
|
def test_instructions_returns_message(self):
|
|
m = DirectorMessage(message="instructions go here")
|
|
assert m.instructions == "instructions go here"
|
|
|
|
def test_as_inner_monologue_replaces_pronouns(self):
|
|
m = DirectorMessage(
|
|
message="You should ready your sword and trust yourself",
|
|
meta={
|
|
"agent": "director",
|
|
"function": "actor_instruction",
|
|
"character": "Alice",
|
|
},
|
|
)
|
|
result = m.as_inner_monologue
|
|
assert "Alice thinks: I should" in result
|
|
# word boundary replacement
|
|
assert " i should " in result
|
|
assert "myself" in result
|
|
assert "my sword" in result
|
|
assert "yourself" not in result
|
|
assert "your" not in result
|
|
|
|
def test_as_inner_monologue_without_character_returns_lowercase_instructions(self):
|
|
m = DirectorMessage(message="You Stand Tall")
|
|
# No character name -> just lowercases without prefix
|
|
assert m.as_inner_monologue == "you stand tall"
|
|
|
|
def test_as_story_progression_includes_character_and_instructions(self):
|
|
m = DirectorMessage(
|
|
message="charge ahead",
|
|
meta={
|
|
"agent": "director",
|
|
"function": "actor_instruction",
|
|
"character": "Bob",
|
|
},
|
|
)
|
|
assert m.as_story_progression == "Bob's next action: charge ahead"
|
|
|
|
def test_str_uses_chat_format(self):
|
|
m = DirectorMessage(
|
|
message="charge",
|
|
meta={
|
|
"agent": "director",
|
|
"function": "actor_instruction",
|
|
"character": "Bob",
|
|
},
|
|
)
|
|
# Default format is chat -> "# {as_story_progression}"
|
|
assert str(m) == "# Bob's next action: charge"
|
|
|
|
def test_as_format_internal_monologue_chat(self):
|
|
m = DirectorMessage(
|
|
message="trust yourself",
|
|
meta={
|
|
"agent": "director",
|
|
"function": "actor_instruction",
|
|
"character": "Bob",
|
|
},
|
|
)
|
|
result = m.as_format("chat", mode="internal_monologue")
|
|
assert result.startswith("# ")
|
|
assert "Bob thinks" in result
|
|
|
|
def test_as_format_movie_script_default_mode(self):
|
|
m = DirectorMessage(
|
|
message="advance",
|
|
meta={
|
|
"agent": "director",
|
|
"function": "actor_instruction",
|
|
"character": "Bob",
|
|
},
|
|
)
|
|
result = m.as_format("movie_script")
|
|
assert result == "\n(Bob's next action: advance)\n"
|
|
|
|
def test_as_format_returns_empty_for_blank_message(self):
|
|
m = DirectorMessage(message=" ")
|
|
assert m.as_format("chat") == ""
|
|
assert m.as_format("movie_script") == ""
|
|
|
|
def test_migrate_legacy_director_instructs_message(self):
|
|
m = DirectorMessage(message="Director instructs Alice: be brave")
|
|
m.migrate_message_to_meta()
|
|
assert m.message == "be brave"
|
|
assert m.source == "player"
|
|
assert m.meta["agent"] == "director"
|
|
assert m.meta["function"] == "actor_instruction"
|
|
# The legacy migration stores the character under arguments rather
|
|
# than at the top of meta, so character_name (which reads from
|
|
# meta["character"]) does not pick it up. Verify both behaviours:
|
|
assert m.meta["arguments"]["character"] == "Alice"
|
|
assert m.character_name is None
|
|
|
|
def test_character_name_reads_top_level_meta_key(self):
|
|
# character_name pulls directly from meta["character"], not from the
|
|
# nested arguments dict that set_source populates.
|
|
m = DirectorMessage(
|
|
message="x", meta={"agent": "director", "function": "y", "character": "Eve"}
|
|
)
|
|
assert m.character_name == "Eve"
|
|
|
|
def test_migrate_backfills_subtype_for_user_direction(self):
|
|
m = DirectorMessage(message="proceed", action="user_direction")
|
|
assert m.subtype is None
|
|
m.migrate_message_to_meta()
|
|
assert m.subtype == "user_direction"
|
|
|
|
def test_dict_includes_action_and_subtype_when_set(self):
|
|
m = DirectorMessage(
|
|
message="x", action="user_direction", subtype="user_direction"
|
|
)
|
|
d = m.__dict__()
|
|
assert d["action"] == "user_direction"
|
|
assert d["subtype"] == "user_direction"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TimePassageMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTimePassageMessage:
|
|
def test_default_attrs(self):
|
|
m = TimePassageMessage(message="A day later")
|
|
assert m.typ == "time"
|
|
assert m.source == "manual"
|
|
assert m.ts == "PT0S"
|
|
|
|
def test_dict_includes_ts(self):
|
|
m = TimePassageMessage(message="A day later", ts="P1D")
|
|
d = m.__dict__()
|
|
assert d["ts"] == "P1D"
|
|
assert d["typ"] == "time"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ReinforcementMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestReinforcementMessage:
|
|
def test_character_name_and_question_from_meta(self):
|
|
m = ReinforcementMessage(
|
|
message="They are loyal",
|
|
meta={
|
|
"agent": "world_state",
|
|
"function": "update_reinforcement",
|
|
"arguments": {"character": "Alice", "question": "are they loyal?"},
|
|
},
|
|
)
|
|
assert m.character_name == "Alice"
|
|
assert m.question == "are they loyal?"
|
|
|
|
def test_character_name_default_when_meta_missing(self):
|
|
m = ReinforcementMessage(message="x")
|
|
assert m.character_name == "character"
|
|
assert m.question == "question"
|
|
|
|
def test_str_formats_internal_note(self):
|
|
m = ReinforcementMessage(
|
|
message="They are loyal",
|
|
meta={
|
|
"agent": "world_state",
|
|
"function": "update_reinforcement",
|
|
"arguments": {"character": "Alice", "question": "are they loyal?"},
|
|
},
|
|
)
|
|
s = str(m)
|
|
assert s.startswith("# Internal note for Alice - are they loyal?\n")
|
|
assert s.endswith("They are loyal")
|
|
|
|
def test_as_format_narrative_wraps_in_parens(self):
|
|
m = ReinforcementMessage(
|
|
message="They are loyal",
|
|
meta={
|
|
"agent": "world_state",
|
|
"function": "update_reinforcement",
|
|
"arguments": {"character": "Alice", "question": "are they loyal?"},
|
|
},
|
|
)
|
|
# narrative format strips leading "# " and wraps in newlines/parens
|
|
result = m.as_format("narrative")
|
|
assert result.startswith("\n(")
|
|
assert result.endswith(")\n")
|
|
assert "Internal note for Alice" in result
|
|
|
|
def test_as_format_default_just_pads_message(self):
|
|
m = ReinforcementMessage(message="hello")
|
|
assert m.as_format("chat") == "\nhello\n"
|
|
|
|
def test_source_to_meta_populates_meta(self):
|
|
m = ReinforcementMessage(
|
|
message="They are loyal", source="are they loyal?:Alice"
|
|
)
|
|
m.source_to_meta()
|
|
assert m.meta == {
|
|
"agent": "world_state",
|
|
"function": "update_reinforcement",
|
|
"arguments": {"character": "Alice", "question": "are they loyal?"},
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ContextInvestigationMessage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestContextInvestigationMessage:
|
|
def test_title_for_visual_character_subtype(self):
|
|
m = ContextInvestigationMessage(
|
|
message="Alice wears a red cloak",
|
|
sub_type="visual-character",
|
|
meta={
|
|
"agent": "x",
|
|
"function": "y",
|
|
"arguments": {"character": "Alice"},
|
|
},
|
|
)
|
|
assert m.title == "Visual description of Alice in the current moment"
|
|
|
|
def test_title_for_visual_scene_subtype(self):
|
|
m = ContextInvestigationMessage(message="A field", sub_type="visual-scene")
|
|
assert m.title == "Visual description of the current moment"
|
|
|
|
def test_title_for_query_subtype(self):
|
|
m = ContextInvestigationMessage(
|
|
message="response",
|
|
sub_type="query",
|
|
meta={
|
|
"agent": "x",
|
|
"function": "y",
|
|
"arguments": {"query": "Why is the sky blue?"},
|
|
},
|
|
)
|
|
assert m.title == "Query: Why is the sky blue?"
|
|
|
|
def test_title_default_when_unknown_subtype(self):
|
|
m = ContextInvestigationMessage(message="x", sub_type="other")
|
|
assert m.title == "Internal note"
|
|
|
|
def test_str_combines_title_and_message(self):
|
|
m = ContextInvestigationMessage(message="content", sub_type="visual-scene")
|
|
assert str(m) == "# Visual description of the current moment: content"
|
|
|
|
def test_as_format_narrative_strips_asterisks(self):
|
|
m = ContextInvestigationMessage(message="con*tent*", sub_type="visual-scene")
|
|
result = m.as_format("narrative")
|
|
assert "*" not in result
|
|
assert "Visual description of the current moment" in result
|
|
|
|
def test_as_format_default_pads_with_newlines(self):
|
|
m = ContextInvestigationMessage(message="content")
|
|
assert m.as_format("chat") == "\ncontent\n"
|
|
|
|
def test_dict_includes_sub_type_always(self):
|
|
m = ContextInvestigationMessage(message="x", sub_type="visual-scene")
|
|
d = m.__dict__()
|
|
assert d["sub_type"] == "visual-scene"
|
|
|
|
def test_dict_includes_assets_when_set(self):
|
|
m = ContextInvestigationMessage(
|
|
message="x",
|
|
sub_type="visual-scene",
|
|
asset_id="img-1",
|
|
asset_type="scene_illustration",
|
|
)
|
|
d = m.__dict__()
|
|
assert d["asset_id"] == "img-1"
|
|
assert d["asset_type"] == "scene_illustration"
|