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
711 lines
25 KiB
Python
711 lines
25 KiB
Python
"""
|
|
Tests for time passage operations:
|
|
|
|
- insert_time_passage / delete_time_passage: archive-index-based (WSM view)
|
|
- insert_time_passage_after_message / delete_time_passage_by_id / update_time_passage_by_id:
|
|
message-id-based (live scene view)
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from talemate.history import (
|
|
insert_time_passage,
|
|
delete_time_passage,
|
|
insert_time_passage_after_message,
|
|
delete_time_passage_by_id,
|
|
update_time_passage_by_id,
|
|
)
|
|
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` /
|
|
`message_index` semantics are caught here.
|
|
"""
|
|
scene = Scene()
|
|
scene.ts = ts
|
|
scene.history = history
|
|
scene.archived_history = archived_history
|
|
scene.layered_history = layered_history or []
|
|
return scene
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: basic insertion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInsertTimePassageBasic:
|
|
"""Verify basic insertion, index shifting, and timestamp recalculation."""
|
|
|
|
def test_insert_before_middle_entry(self):
|
|
"""Insert a time passage before the second summarized entry."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_msg("C"), # 2
|
|
_msg("D"), # 3
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2-3", "start": 2, "end": 3, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
tp = insert_time_passage(scene, archive_index=1, amount=2, unit="hours")
|
|
|
|
# TimePassageMessage inserted at original start=2
|
|
assert isinstance(scene.history[2], TimePassageMessage)
|
|
assert tp.ts == "PT2H"
|
|
|
|
# First entry: start=0 < insertion_index=2, end=1 < 2 => unchanged
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
|
|
# Second entry: start=2 >= 2 => 3, end=3 >= 2 => 4
|
|
assert scene.archived_history[1]["start"] == 3
|
|
assert scene.archived_history[1]["end"] == 4
|
|
|
|
# fix_time should set timestamp on second entry
|
|
assert scene.archived_history[1]["ts"] == "PT2H"
|
|
|
|
def test_insert_before_first_entry(self):
|
|
"""Insert before the very first summarized entry (start=0)."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_msg("C"), # 2
|
|
_msg("D"), # 3
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2-3", "start": 2, "end": 3, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
insert_time_passage(scene, archive_index=0, amount=30, unit="minutes")
|
|
|
|
# Inserted at index 0
|
|
assert isinstance(scene.history[0], TimePassageMessage)
|
|
assert len(scene.history) == 5
|
|
|
|
# Both entries bumped
|
|
assert scene.archived_history[0]["start"] == 1
|
|
assert scene.archived_history[0]["end"] == 2
|
|
assert scene.archived_history[1]["start"] == 3
|
|
assert scene.archived_history[1]["end"] == 4
|
|
|
|
# Time passage at index 0, both entries end > 0 => both get PT30M
|
|
assert scene.archived_history[0]["ts"] == "PT30M"
|
|
assert scene.archived_history[1]["ts"] == "PT30M"
|
|
|
|
def test_insert_before_last_entry(self):
|
|
"""Insert before the last entry only shifts that entry's indices."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_msg("C"), # 2
|
|
_msg("D"), # 3
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2-3", "start": 2, "end": 3, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
insert_time_passage(scene, archive_index=1, amount=1, unit="days")
|
|
|
|
# First entry unchanged
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
|
|
# Second entry shifted
|
|
assert scene.archived_history[1]["start"] == 3
|
|
assert scene.archived_history[1]["end"] == 4
|
|
|
|
def test_history_length_increases(self):
|
|
"""Scene history should have one more element after insertion."""
|
|
scene = make_scene(
|
|
history=[_msg("A"), _msg("B")],
|
|
archived_history=[
|
|
{"text": "Sum", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
insert_time_passage(scene, archive_index=0, amount=1, unit="hours")
|
|
|
|
assert len(scene.history) == 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: validation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInsertTimePassageValidation:
|
|
"""Verify error handling for invalid inputs."""
|
|
|
|
def test_static_entry_raises(self):
|
|
"""Cannot insert time passage before a static (manual) entry."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[
|
|
{"text": "Static", "ts": "PT0S", "id": "s1"},
|
|
],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not a summarized entry"):
|
|
insert_time_passage(scene, archive_index=0, amount=1, unit="hours")
|
|
|
|
def test_negative_index_raises(self):
|
|
"""Negative archive_index should raise IndexError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[
|
|
{"text": "Sum", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
with pytest.raises(IndexError):
|
|
insert_time_passage(scene, archive_index=-1, amount=1, unit="hours")
|
|
|
|
def test_out_of_bounds_index_raises(self):
|
|
"""archive_index beyond list length should raise IndexError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[
|
|
{"text": "Sum", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
with pytest.raises(IndexError):
|
|
insert_time_passage(scene, archive_index=5, amount=1, unit="hours")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: interaction with existing time passages
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInsertTimePassageWithExisting:
|
|
"""Verify correct behaviour when time passages already exist."""
|
|
|
|
def test_insert_alongside_existing_time_passage(self):
|
|
"""
|
|
When scene.history already has TimePassageMessages, inserting a new one
|
|
should produce correct cumulative timestamps.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_time("PT1H"), # 2 — existing 1h passage
|
|
_msg("C"), # 3
|
|
_msg("D"), # 4
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 3-4", "start": 3, "end": 4, "ts": "PT1H", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
# Insert 2h before the second entry (start=3)
|
|
insert_time_passage(scene, archive_index=1, amount=2, unit="hours")
|
|
|
|
# scene.history should now be:
|
|
# [msg, msg, time(1h), time(2h), msg, msg]
|
|
assert isinstance(scene.history[2], TimePassageMessage)
|
|
assert scene.history[2].ts == "PT1H" # original
|
|
assert isinstance(scene.history[3], TimePassageMessage)
|
|
assert scene.history[3].ts == "PT2H" # newly inserted
|
|
|
|
# Indices: existing time passage at idx 2 is < insertion_index=3, unaffected
|
|
# Second entry: start 3->4, end 4->5
|
|
assert scene.archived_history[1]["start"] == 4
|
|
assert scene.archived_history[1]["end"] == 5
|
|
|
|
# Cumulative: PT1H + PT2H = PT3H
|
|
assert scene.archived_history[1]["ts"] == "PT3H"
|
|
|
|
def test_insert_before_entry_that_precedes_existing_passage(self):
|
|
"""
|
|
Insert before the first entry when a time passage exists after it.
|
|
The new passage should come first in the cumulative chain.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_time("PT3H"), # 2
|
|
_msg("C"), # 3
|
|
_msg("D"), # 4
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 3-4", "start": 3, "end": 4, "ts": "PT3H", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
# Insert 30min before first entry
|
|
insert_time_passage(scene, archive_index=0, amount=30, unit="minutes")
|
|
|
|
# New passage at index 0, everything else shifted
|
|
assert isinstance(scene.history[0], TimePassageMessage)
|
|
assert scene.history[0].ts == "PT30M"
|
|
|
|
# First entry: start 0->1, end 1->2
|
|
assert scene.archived_history[0]["start"] == 1
|
|
assert scene.archived_history[0]["end"] == 2
|
|
|
|
# Second entry: start 3->4, end 4->5
|
|
assert scene.archived_history[1]["start"] == 4
|
|
assert scene.archived_history[1]["end"] == 5
|
|
|
|
# Cumulative: PT30M (idx 0) + PT3H (idx 3) = PT3H30M
|
|
assert scene.archived_history[1]["ts"] == "PT3H30M"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: layered history is not corrupted
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInsertTimePassageLayeredHistory:
|
|
"""Verify layered_history indices are untouched but timestamps update."""
|
|
|
|
def test_layered_history_indices_unchanged(self):
|
|
"""
|
|
Layered history references archived_history indices, not scene.history.
|
|
Inserting into scene.history should not change layered start/end.
|
|
"""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_msg("C"), # 2
|
|
_msg("D"), # 3
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2-3", "start": 2, "end": 3, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
layered_history=[
|
|
[
|
|
{
|
|
"text": "L0 covers both",
|
|
"start": 0,
|
|
"end": 1,
|
|
"ts": "STALE",
|
|
"ts_start": "STALE",
|
|
"ts_end": "STALE",
|
|
"id": "l0a",
|
|
},
|
|
],
|
|
],
|
|
)
|
|
|
|
insert_time_passage(scene, archive_index=1, amount=2, unit="hours")
|
|
|
|
# Layered history indices must remain the same
|
|
assert scene.layered_history[0][0]["start"] == 0
|
|
assert scene.layered_history[0][0]["end"] == 1
|
|
|
|
# But timestamps should be updated via fix_time
|
|
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"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: delete_time_passage
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDeleteTimePassageBasic:
|
|
"""Verify deletion, index shifting, and timestamp recalculation."""
|
|
|
|
def test_delete_middle_time_passage(self):
|
|
"""Delete a time passage between two summarized entries."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_time("PT2H"), # 2
|
|
_msg("C"), # 3
|
|
_msg("D"), # 4
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 3-4", "start": 3, "end": 4, "ts": "PT2H", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
delete_time_passage(scene, history_index=2)
|
|
|
|
# Time passage removed
|
|
assert len(scene.history) == 4
|
|
assert not isinstance(scene.history[2], TimePassageMessage)
|
|
|
|
# First entry: start=0, end=1 — unchanged (both < 2)
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
|
|
# Second entry: start=3->2, end=4->3 (both > 2)
|
|
assert scene.archived_history[1]["start"] == 2
|
|
assert scene.archived_history[1]["end"] == 3
|
|
|
|
# No time passages left — fix_time returns early without updating
|
|
# individual entry timestamps, but scene.ts is set to starting_time
|
|
assert scene.ts == "PT0S"
|
|
|
|
def test_delete_first_time_passage(self):
|
|
"""Delete a time passage at index 0."""
|
|
scene = make_scene(
|
|
history=[
|
|
_time("PT1H"), # 0
|
|
_msg("A"), # 1
|
|
_msg("B"), # 2
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 1-2", "start": 1, "end": 2, "ts": "PT1H", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
delete_time_passage(scene, history_index=0)
|
|
|
|
assert len(scene.history) == 2
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
|
|
def test_delete_preserves_other_time_passages(self):
|
|
"""Delete one of multiple time passages; remaining should still work."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_time("PT1H"), # 1
|
|
_msg("B"), # 2
|
|
_time("PT2H"), # 3
|
|
_msg("C"), # 4
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "PT1H", "id": "a2"},
|
|
{"text": "Sum 4", "start": 4, "end": 4, "ts": "PT3H", "id": "a3"},
|
|
],
|
|
)
|
|
|
|
# Delete the first time passage (index 1)
|
|
delete_time_passage(scene, history_index=1)
|
|
|
|
# History: [msg, msg, time(2h), msg]
|
|
assert len(scene.history) == 4
|
|
assert isinstance(scene.history[2], TimePassageMessage)
|
|
assert scene.history[2].ts == "PT2H"
|
|
|
|
# Indices shifted: entries after idx 1 decremented
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 0
|
|
assert scene.archived_history[1]["start"] == 1
|
|
assert scene.archived_history[1]["end"] == 1
|
|
assert scene.archived_history[2]["start"] == 3
|
|
assert scene.archived_history[2]["end"] == 3
|
|
|
|
# Only PT2H remains, affects third entry
|
|
assert scene.archived_history[2]["ts"] == "PT2H"
|
|
|
|
|
|
class TestDeleteTimePassageValidation:
|
|
"""Verify error handling for invalid deletion inputs."""
|
|
|
|
def test_not_a_time_passage_raises(self):
|
|
"""Cannot delete a non-TimePassageMessage."""
|
|
scene = make_scene(
|
|
history=[_msg("A"), _msg("B")],
|
|
archived_history=[
|
|
{"text": "Sum", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not a TimePassageMessage"):
|
|
delete_time_passage(scene, history_index=0)
|
|
|
|
def test_out_of_bounds_raises(self):
|
|
"""Out-of-bounds history_index should raise IndexError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(IndexError):
|
|
delete_time_passage(scene, history_index=5)
|
|
|
|
def test_negative_index_raises(self):
|
|
"""Negative history_index should raise IndexError."""
|
|
scene = make_scene(
|
|
history=[_time("PT1H"), _msg("A")],
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(IndexError):
|
|
delete_time_passage(scene, history_index=-1)
|
|
|
|
|
|
class TestDeleteTimePassageRoundTrip:
|
|
"""Verify insert followed by delete restores original state."""
|
|
|
|
def test_insert_then_delete_restores_indices(self):
|
|
"""Inserting and then deleting should restore original indices."""
|
|
scene = make_scene(
|
|
history=[
|
|
_msg("A"), # 0
|
|
_msg("B"), # 1
|
|
_msg("C"), # 2
|
|
_msg("D"), # 3
|
|
],
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2-3", "start": 2, "end": 3, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
# Insert a 2h passage before second entry
|
|
insert_time_passage(scene, archive_index=1, amount=2, unit="hours")
|
|
|
|
# Now the time passage is at index 2
|
|
assert isinstance(scene.history[2], TimePassageMessage)
|
|
assert scene.archived_history[1]["start"] == 3
|
|
assert scene.archived_history[1]["end"] == 4
|
|
|
|
# Delete it
|
|
delete_time_passage(scene, history_index=2)
|
|
|
|
# Indices should be back to original
|
|
assert len(scene.history) == 4
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
assert scene.archived_history[1]["start"] == 2
|
|
assert scene.archived_history[1]["end"] == 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: insert_time_passage_after_message (message-id-based)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInsertTimePassageAfterMessage:
|
|
"""Verify inserting a time passage after a message identified by id."""
|
|
|
|
def test_insert_after_first_message(self):
|
|
"""Insert time passage after the first message."""
|
|
msgs = [_msg("A"), _msg("B"), _msg("C")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
tp = insert_time_passage_after_message(
|
|
scene, msgs[0].id, amount=1, unit="hours"
|
|
)
|
|
|
|
# Inserted at index 1 (after msg A at 0)
|
|
assert scene.history[1] is tp
|
|
assert isinstance(tp, TimePassageMessage)
|
|
assert tp.ts == "PT1H"
|
|
assert len(scene.history) == 4
|
|
|
|
# Indices shifted: entries with start/end >= 1 get bumped
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 2 # was 1, >= 1, bumped
|
|
assert scene.archived_history[1]["start"] == 3 # was 2, bumped
|
|
assert scene.archived_history[1]["end"] == 3 # was 2, bumped
|
|
|
|
def test_insert_after_last_message(self):
|
|
"""Insert time passage after the last message in history."""
|
|
msgs = [_msg("A"), _msg("B")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[
|
|
{"text": "Sum 0-1", "start": 0, "end": 1, "ts": "PT0S", "id": "a1"},
|
|
],
|
|
)
|
|
|
|
tp = insert_time_passage_after_message(scene, msgs[1].id, amount=3, unit="days")
|
|
|
|
# Inserted at index 2 (after end of list)
|
|
assert len(scene.history) == 3
|
|
assert scene.history[2] is tp
|
|
assert tp.ts == "P3D"
|
|
|
|
# Archived entries: start=0, end=1, both < 2 => unchanged
|
|
assert scene.archived_history[0]["start"] == 0
|
|
assert scene.archived_history[0]["end"] == 1
|
|
|
|
def test_invalid_message_id_raises(self):
|
|
"""Non-existent message id raises ValueError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
insert_time_passage_after_message(
|
|
scene, message_id=99999, amount=1, unit="hours"
|
|
)
|
|
|
|
def test_timestamps_updated(self):
|
|
"""fix_time is called and timestamps reflect the new passage."""
|
|
msgs = [_msg("A"), _msg("B"), _msg("C")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 1-2", "start": 1, "end": 2, "ts": "PT0S", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
insert_time_passage_after_message(scene, msgs[0].id, amount=2, unit="hours")
|
|
|
|
# Time passage at index 1, second entry now starts at 2
|
|
assert scene.archived_history[1]["ts"] == "PT2H"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: delete_time_passage_by_id (message-id-based)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDeleteTimePassageById:
|
|
"""Verify deleting a time passage by its message id."""
|
|
|
|
def test_delete_by_id(self):
|
|
"""Delete a time passage using its message id."""
|
|
tp = _time("PT2H")
|
|
msgs = [_msg("A"), tp, _msg("B")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "PT2H", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
delete_time_passage_by_id(scene, tp.id)
|
|
|
|
assert len(scene.history) == 2
|
|
assert not any(isinstance(m, TimePassageMessage) for m in scene.history)
|
|
# Second entry shifted: start=2->1, end=2->1
|
|
assert scene.archived_history[1]["start"] == 1
|
|
assert scene.archived_history[1]["end"] == 1
|
|
|
|
def test_invalid_message_id_raises(self):
|
|
"""Non-existent message id raises ValueError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
delete_time_passage_by_id(scene, message_id=99999)
|
|
|
|
def test_non_time_passage_raises(self):
|
|
"""Trying to delete a non-TimePassageMessage by id raises ValueError."""
|
|
msgs = [_msg("A")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not a TimePassageMessage"):
|
|
delete_time_passage_by_id(scene, msgs[0].id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests: update_time_passage_by_id (message-id-based)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestUpdateTimePassageById:
|
|
"""Verify updating a time passage's duration by its message id."""
|
|
|
|
def test_update_duration(self):
|
|
"""Update a time passage from 2h to 30 minutes."""
|
|
tp = _time("PT2H")
|
|
msgs = [_msg("A"), tp, _msg("B")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[
|
|
{"text": "Sum 0", "start": 0, "end": 0, "ts": "PT0S", "id": "a1"},
|
|
{"text": "Sum 2", "start": 2, "end": 2, "ts": "PT2H", "id": "a2"},
|
|
],
|
|
)
|
|
|
|
update_time_passage_by_id(scene, tp.id, amount=30, unit="minutes")
|
|
|
|
assert tp.ts == "PT30M"
|
|
assert "30 minutes" in tp.message.lower()
|
|
# fix_time recalculates: second entry should now be PT30M
|
|
assert scene.archived_history[1]["ts"] == "PT30M"
|
|
|
|
def test_update_preserves_position(self):
|
|
"""Updating does not change the position or history length."""
|
|
tp = _time("PT1H")
|
|
msgs = [_msg("A"), tp, _msg("B")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[],
|
|
)
|
|
|
|
update_time_passage_by_id(scene, tp.id, amount=3, unit="days")
|
|
|
|
assert len(scene.history) == 3
|
|
assert scene.history[1] is tp
|
|
assert tp.ts == "P3D"
|
|
|
|
def test_invalid_message_id_raises(self):
|
|
"""Non-existent message id raises ValueError."""
|
|
scene = make_scene(
|
|
history=[_msg("A")],
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not found"):
|
|
update_time_passage_by_id(scene, message_id=99999, amount=1, unit="hours")
|
|
|
|
def test_non_time_passage_raises(self):
|
|
"""Trying to update a non-TimePassageMessage raises ValueError."""
|
|
msgs = [_msg("A")]
|
|
scene = make_scene(
|
|
history=msgs,
|
|
archived_history=[],
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="not a TimePassageMessage"):
|
|
update_time_passage_by_id(scene, msgs[0].id, amount=1, unit="hours")
|