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
503 lines
18 KiB
Python
503 lines
18 KiB
Python
"""
|
|
Tests for Scene.fix_time / Scene._fix_time.
|
|
|
|
fix_time recalculates timestamps across archived_history (and layered_history)
|
|
based on TimePassageMessage entries in scene.history.
|
|
"""
|
|
|
|
from talemate.scene_message import CharacterMessage, TimePassageMessage
|
|
from talemate.tale_mate import Scene
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _msg(text: str) -> CharacterMessage:
|
|
return CharacterMessage(message=text, source="ai")
|
|
|
|
|
|
def _time(ts: str) -> TimePassageMessage:
|
|
return TimePassageMessage(ts=ts, message=f"{ts} later")
|
|
|
|
|
|
def make_scene(
|
|
history: list,
|
|
archived_history: list,
|
|
layered_history: list | None = None,
|
|
ts: str = "PT0S",
|
|
):
|
|
"""Build a real `Scene` populated with the requested timeline state.
|
|
|
|
Uses the production `Scene` class so changes to its `_fix_time`
|
|
semantics surface here.
|
|
"""
|
|
scene = Scene()
|
|
scene.ts = ts
|
|
scene.history = history
|
|
scene.archived_history = archived_history
|
|
scene.layered_history = layered_history or []
|
|
return scene
|
|
|
|
|
|
def fix_time(scene: Scene):
|
|
"""Call Scene._fix_time directly to skip the try/except wrapper in fix_time."""
|
|
Scene._fix_time(scene)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: fix_time with archived_history only (current behaviour)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFixTimeArchivedHistory:
|
|
"""Verify fix_time correctly recalculates archived_history timestamps."""
|
|
|
|
def test_single_time_jump(self):
|
|
"""One time passage should set ts on subsequent archived entries."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("Hello"), # 0
|
|
_msg("Hi"), # 1
|
|
_time("PT2H"), # 2 — 2 hours
|
|
_msg("After time"), # 3
|
|
_msg("More"), # 4
|
|
],
|
|
archived_history=[
|
|
# static entry (no start/end) — should be left alone
|
|
{"text": "Background", "ts": "PT0S"},
|
|
# summary of messages 0-1 (before time jump at idx 2)
|
|
{"text": "Summary A", "start": 0, "end": 1, "ts": "WRONG"},
|
|
# summary of messages 3-4 (after time jump at idx 2)
|
|
{"text": "Summary B", "start": 3, "end": 4, "ts": "WRONG"},
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# Static entry unchanged
|
|
assert scene.archived_history[0]["ts"] == "PT0S"
|
|
# Entry ending at idx 1: no time jump before idx 1, so inherits starting_time
|
|
assert scene.archived_history[1]["ts"] == "PT0S"
|
|
# Entry ending at idx 4: time jump at idx 2 (PT2H) is before idx 4
|
|
assert scene.archived_history[2]["ts"] == "PT2H"
|
|
# scene.ts should be the last cumulative time
|
|
assert scene.ts == "PT2H"
|
|
|
|
def test_multiple_time_jumps_cumulative(self):
|
|
"""Multiple time passages should accumulate correctly."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT2H"), # 1 — 2 hours
|
|
_msg("B"), # 2
|
|
_time("PT1H"), # 3 — +1 hour = 3 hours total
|
|
_msg("C"), # 4
|
|
_time("P1D"), # 5 — +1 day = 1 day 3 hours total
|
|
_msg("D"), # 6
|
|
],
|
|
archived_history=[
|
|
{"text": "Before jumps", "start": 0, "end": 0, "ts": "WRONG"},
|
|
{"text": "After 2h", "start": 2, "end": 2, "ts": "WRONG"},
|
|
{"text": "After 3h", "start": 4, "end": 4, "ts": "WRONG"},
|
|
{"text": "After 1d3h", "start": 6, "end": 6, "ts": "WRONG"},
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# end=0: no jump before idx 0
|
|
assert scene.archived_history[0]["ts"] == "PT0S"
|
|
# end=2: jump at idx 1 (PT2H) is before idx 2
|
|
assert scene.archived_history[1]["ts"] == "PT2H"
|
|
# end=4: jumps at idx 1+3 cumulative = PT3H, both before idx 4
|
|
assert scene.archived_history[2]["ts"] == "PT3H"
|
|
# end=6: all jumps cumulative = P1DT3H
|
|
assert scene.archived_history[3]["ts"] == "P1DT3H"
|
|
assert scene.ts == "P1DT3H"
|
|
|
|
def test_static_entries_used_as_starting_time(self):
|
|
"""Static entries with ts should set the starting baseline."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT1H"), # 1 — +1h from baseline
|
|
_msg("B"), # 2
|
|
],
|
|
archived_history=[
|
|
# static entry sets baseline to P1D
|
|
{"text": "Background", "ts": "P1D"},
|
|
{"text": "After 1h", "start": 0, "end": 2, "ts": "WRONG"},
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# Baseline is P1D from static entry, +1h = P1DT1H
|
|
assert scene.archived_history[1]["ts"] == "P1DT1H"
|
|
assert scene.ts == "P1DT1H"
|
|
|
|
def test_no_time_jumps(self):
|
|
"""No TimePassageMessages should leave ts at starting_time."""
|
|
scene = make_scene(
|
|
history=[_msg("A"), _msg("B")],
|
|
archived_history=[
|
|
{"text": "Summary", "start": 0, "end": 1, "ts": "PT5H"},
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# No time jumps, so ts stays at starting_time (PT0S default)
|
|
assert scene.ts == "PT0S"
|
|
|
|
def test_zero_duration_time_passage(self):
|
|
"""A zero-duration time passage should not change timestamps."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT0S"), # 1 — zero
|
|
_msg("B"), # 2
|
|
_time("PT3H"), # 3 — 3 hours
|
|
_msg("C"), # 4
|
|
],
|
|
archived_history=[
|
|
{"text": "Before", "start": 0, "end": 0, "ts": "WRONG"},
|
|
{"text": "Middle", "start": 2, "end": 2, "ts": "WRONG"},
|
|
{"text": "After", "start": 4, "end": 4, "ts": "WRONG"},
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
assert scene.archived_history[0]["ts"] == "PT0S"
|
|
# PT0S added = still PT0S (but as isodate normalises it may vary)
|
|
assert scene.archived_history[1]["ts"] in ("PT0S", "P0D")
|
|
assert scene.archived_history[2]["ts"] == "PT3H"
|
|
assert scene.ts == "PT3H"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: fix_time does NOT update layered_history (current gap)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFixTimeLayeredHistoryBasic:
|
|
"""Verify that fix_time updates both archived_history and layered_history."""
|
|
|
|
def test_layered_history_updated(self):
|
|
"""
|
|
After fix_time, both archived_history and layered_history
|
|
timestamps should be updated from scene.history time passages.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_time("PT2H"), # 2
|
|
_msg("C"), # 3
|
|
_msg("D"), # 4
|
|
_time("P1D"), # 5
|
|
_msg("E"), # 6
|
|
_msg("F"), # 7
|
|
],
|
|
archived_history=[
|
|
{
|
|
"text": "Summary 0-1",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"id": "a1",
|
|
},
|
|
{
|
|
"text": "Summary 3-4",
|
|
"start": 3,
|
|
"end": 4,
|
|
"ts": "STALE",
|
|
"id": "a2",
|
|
},
|
|
{
|
|
"text": "Summary 6-7",
|
|
"start": 6,
|
|
"end": 7,
|
|
"ts": "STALE",
|
|
"id": "a3",
|
|
},
|
|
],
|
|
layered_history=[
|
|
# Layer 0: references archived_history indices
|
|
[
|
|
{
|
|
"text": "Layer0 entry A",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0a",
|
|
},
|
|
{
|
|
"text": "Layer0 entry B",
|
|
"start": 2,
|
|
"end": 2,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0b",
|
|
},
|
|
],
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# Archived history IS fixed
|
|
assert scene.archived_history[0]["ts"] == "PT0S"
|
|
assert scene.archived_history[1]["ts"] == "PT2H"
|
|
assert scene.archived_history[2]["ts"] == "P1DT2H"
|
|
|
|
# Layered history IS fixed — timestamps derived from archived_history
|
|
# Layer 0 entry [0]: covers archived[0..1]
|
|
# ts = archived[0]["ts"] = PT0S
|
|
# ts_end = archived[1]["ts"] = PT2H
|
|
assert scene.layered_history[0][0]["ts"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_start"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_end"] == "PT2H"
|
|
# Layer 0 entry [1]: covers archived[2..2]
|
|
assert scene.layered_history[0][1]["ts"] == "P1DT2H"
|
|
assert scene.layered_history[0][1]["ts_start"] == "P1DT2H"
|
|
assert scene.layered_history[0][1]["ts_end"] == "P1DT2H"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: fix_time SHOULD update layered_history (expected after fix)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFixTimeLayeredHistory:
|
|
"""Verify fix_time correctly updates layered_history timestamps."""
|
|
|
|
def test_single_layer_timestamps_fixed(self):
|
|
"""
|
|
Layer 0 of layered_history references archived_history via start/end.
|
|
After fix_time, its ts/ts_start/ts_end should reflect the updated
|
|
archived_history timestamps.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_time("PT2H"), # 2
|
|
_msg("C"), # 3
|
|
_msg("D"), # 4
|
|
_time("P1D"), # 5
|
|
_msg("E"), # 6
|
|
_msg("F"), # 7
|
|
],
|
|
archived_history=[
|
|
{
|
|
"text": "Summary 0-1",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"id": "a1",
|
|
},
|
|
{
|
|
"text": "Summary 3-4",
|
|
"start": 3,
|
|
"end": 4,
|
|
"ts": "STALE",
|
|
"id": "a2",
|
|
},
|
|
{
|
|
"text": "Summary 6-7",
|
|
"start": 6,
|
|
"end": 7,
|
|
"ts": "STALE",
|
|
"id": "a3",
|
|
},
|
|
],
|
|
layered_history=[
|
|
# Layer 0: references archived_history[0..1] and archived_history[2]
|
|
[
|
|
{
|
|
"text": "Layer0 summary of archived 0-1",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0a",
|
|
},
|
|
{
|
|
"text": "Layer0 summary of archived 2",
|
|
"start": 2,
|
|
"end": 2,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0b",
|
|
},
|
|
],
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# Archived history is fixed:
|
|
# a1 end=1: no jump before idx 1 → PT0S
|
|
# a2 end=4: jump at idx 2 (PT2H) before idx 4 → PT2H
|
|
# a3 end=7: jumps cumulative P1DT2H before idx 7 → P1DT2H
|
|
assert scene.archived_history[0]["ts"] == "PT0S"
|
|
assert scene.archived_history[1]["ts"] == "PT2H"
|
|
assert scene.archived_history[2]["ts"] == "P1DT2H"
|
|
|
|
# Layer 0 entry [0]: covers archived[0..1]
|
|
# ts = archived[0]["ts"] = PT0S
|
|
# ts_start = archived[0]["ts"] = PT0S (archived has no ts_start)
|
|
# ts_end = archived[1]["ts"] = PT2H
|
|
assert scene.layered_history[0][0]["ts"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_start"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_end"] == "PT2H"
|
|
|
|
# Layer 0 entry [1]: covers archived[2..2]
|
|
# ts = archived[2]["ts"] = P1DT2H
|
|
# ts_start = P1DT2H
|
|
# ts_end = P1DT2H
|
|
assert scene.layered_history[0][1]["ts"] == "P1DT2H"
|
|
assert scene.layered_history[0][1]["ts_start"] == "P1DT2H"
|
|
assert scene.layered_history[0][1]["ts_end"] == "P1DT2H"
|
|
|
|
def test_multi_layer_timestamps_cascade(self):
|
|
"""
|
|
Layer 1 references layer 0, which references archived_history.
|
|
All should be updated after fix_time.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT1H"), # 1
|
|
_msg("B"), # 2
|
|
_time("PT1H"), # 3
|
|
_msg("C"), # 4
|
|
_time("PT1H"), # 5
|
|
_msg("D"), # 6
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "STALE", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "STALE", "id": "a2"},
|
|
{"text": "Sum 4", "start": 4, "end": 4, "ts": "STALE", "id": "a3"},
|
|
{"text": "Sum 6", "start": 6, "end": 6, "ts": "STALE", "id": "a4"},
|
|
],
|
|
layered_history=[
|
|
# Layer 0: references archived_history
|
|
[
|
|
{
|
|
"text": "L0 A",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0a",
|
|
},
|
|
{
|
|
"text": "L0 B",
|
|
"start": 2,
|
|
"end": 3,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0b",
|
|
},
|
|
],
|
|
# Layer 1: references layered_history[0]
|
|
[
|
|
{
|
|
"text": "L1 A",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l1a",
|
|
},
|
|
],
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
# Archived: jumps at idx 1,3,5 → cumulative PT1H, PT2H, PT3H
|
|
assert scene.archived_history[0]["ts"] == "PT0S" # end=0, no jump before
|
|
assert scene.archived_history[1]["ts"] == "PT1H" # end=2, jump at 1
|
|
assert scene.archived_history[2]["ts"] == "PT2H" # end=4, jumps at 1,3
|
|
assert scene.archived_history[3]["ts"] == "PT3H" # end=6, jumps at 1,3,5
|
|
|
|
# Layer 0 entry [0]: covers archived[0..1]
|
|
assert scene.layered_history[0][0]["ts"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_start"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_end"] == "PT1H"
|
|
|
|
# Layer 0 entry [1]: covers archived[2..3]
|
|
assert scene.layered_history[0][1]["ts"] == "PT2H"
|
|
assert scene.layered_history[0][1]["ts_start"] == "PT2H"
|
|
assert scene.layered_history[0][1]["ts_end"] == "PT3H"
|
|
|
|
# Layer 1 entry [0]: covers layered_history[0][0..1]
|
|
# ts_end = l0[1].ts_end = PT3H (l0[1] covers archived[2..3])
|
|
assert scene.layered_history[1][0]["ts"] == "PT0S"
|
|
assert scene.layered_history[1][0]["ts_start"] == "PT0S"
|
|
assert scene.layered_history[1][0]["ts_end"] == "PT3H"
|
|
|
|
def test_layered_entry_without_ts_start_ts_end(self):
|
|
"""
|
|
If a layered entry has no ts_start/ts_end fields, fix_time should
|
|
still set them based on the source layer.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT5H"), # 1
|
|
_msg("B"), # 2
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "STALE", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "STALE", "id": "a2"},
|
|
],
|
|
layered_history=[
|
|
[
|
|
{
|
|
"text": "L0 covers both",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"id": "l0a",
|
|
# no ts_start or ts_end
|
|
},
|
|
],
|
|
],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
assert scene.layered_history[0][0]["ts"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_start"] == "PT0S"
|
|
assert scene.layered_history[0][0]["ts_end"] == "PT5H"
|
|
|
|
def test_empty_layered_history(self):
|
|
"""fix_time should not crash when layered_history is empty."""
|
|
scene = make_scene(
|
|
history=[_msg("A"), _time("PT1H"), _msg("B")],
|
|
archived_history=[
|
|
{"text": "Sum", "start": 0, "end": 2, "ts": "STALE"},
|
|
],
|
|
layered_history=[],
|
|
)
|
|
|
|
fix_time(scene)
|
|
|
|
assert scene.archived_history[0]["ts"] == "PT1H"
|
|
assert scene.ts == "PT1H"
|