Files
talemate/tests/test_fix_time.py
veguAI f5d41c04c8 0.37.0 (#267)
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
2026-05-12 21:01:51 +03:00

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"