Files
talemate/tests/test_isodate.py
veguAI 42a8863e65 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

251 lines
8.2 KiB
Python

import pytest
from talemate.util import (
iso8601_add,
iso8601_diff,
iso8601_diff_to_human,
iso8601_duration_to_human,
parse_duration_to_isodate_duration,
duration_to_timedelta,
amount_unit_to_iso8601_duration,
)
from talemate.util.time import iso8601_duration_to_amount_unit
def test_isodate_utils():
date1 = "P11MT15M"
date2 = "PT1S"
duration1 = parse_duration_to_isodate_duration(date1)
assert duration1.months == 11
assert duration1.tdelta.seconds == 900
duration2 = parse_duration_to_isodate_duration(date2)
assert duration2.seconds == 1
timedelta1 = duration_to_timedelta(duration1)
assert timedelta1.seconds == 900
assert timedelta1.days == 11 * 30, timedelta1.days
timedelta2 = duration_to_timedelta(duration2)
assert timedelta2.seconds == 1
parsed = parse_duration_to_isodate_duration("P11MT14M59S")
assert iso8601_diff(date1, date2) == parsed, parsed
assert (
iso8601_duration_to_human(date1, flatten=False)
== "11 Months and 15 Minutes ago"
), iso8601_duration_to_human(date1, flatten=False)
assert iso8601_duration_to_human(date2, flatten=False) == "1 Second ago", (
iso8601_duration_to_human(date2, flatten=False)
)
assert (
iso8601_duration_to_human(iso8601_diff(date1, date2), flatten=False)
== "11 Months, 14 Minutes and 59 Seconds ago"
), iso8601_duration_to_human(iso8601_diff(date1, date2), flatten=False)
@pytest.mark.parametrize(
"dates, expected",
[
(["PT1S", "P3M", "P6M", "P8M"], "P17MT1S"),
],
)
def test_adding_isodates(dates: list[str], expected: str):
date = dates[0]
for i in range(1, len(dates)):
date = iso8601_add(date, dates[i])
assert date == expected, date
@pytest.mark.parametrize(
"a, b, expected",
[
# Basic year/month cases
("P1Y", "P11M", "1 Month and 5 Days ago"),
("P12M", "P11M", "1 Month ago"),
("P2Y", "P1Y", "1 Year ago"),
("P25M", "P1Y", "1 Year, 2 Weeks and 6 Days ago"),
# Mixed time components
("P34DT2H30M", "PT0S", "1 Month, 4 Days, 2 Hours and 30 Minutes ago"),
("P1YT24H", "P1Y", "1 Day ago"),
("P1MT60S", "P1M", "1 Minute ago"),
("P400D", "P1Y", "1 Month and 5 Days ago"),
# Edge cases
("PT1S", "PT0S", "1 Second ago"),
("PT1M", "PT0S", "1 Minute ago"),
("PT1H", "PT0S", "1 Hour ago"),
("P1D", "PT0S", "1 Day ago"),
("P1W", "PT0S", "1 Week ago"),
("P1M", "PT0S", "1 Month ago"),
("P1Y", "PT0S", "1 Year ago"),
# Complex mixed durations
(
"P1Y2M3DT4H5M6S",
"PT0S",
"1 Year, 2 Months, 3 Days, 4 Hours, 5 Minutes and 6 Seconds ago",
),
("P1Y1M1DT1H1M1S", "P1Y", "1 Month, 1 Day, 1 Hour, 1 Minute and 1 Second ago"),
("P2Y15M", "P1Y", "2 Years, 2 Months, 3 Weeks and 4 Days ago"),
# Time-only durations
("PT24H", "PT0S", "1 Day ago"),
("PT25H", "PT1H", "1 Day ago"),
("PT90M", "PT30M", "1 Hour ago"),
("PT3600S", "PT0S", "1 Hour ago"),
# Inverse order (should give same absolute difference)
("P1M", "P2M", "1 Month ago"),
("PT0S", "P1Y", "1 Year ago"),
# Zero difference
("P1Y", "P1Y", "Recently"),
("P1M", "P1M", "Recently"),
("PT0S", "PT0S", "Recently"),
# long durations
("P0D", "P998Y23M30D", "999 Years, 11 Months, 3 Weeks and 4 Days ago"),
("P0D", "P12M364640D", "1000 Years ago"),
],
)
def test_iso8601_diff_to_human_unflattened(a, b, expected):
assert iso8601_diff_to_human(a, b, flatten=False) == expected, (
iso8601_diff_to_human(a, b, flatten=False)
)
@pytest.mark.parametrize(
"a, b, expected",
[
# Basic duration flattening tests
("P1Y2M3DT4H5M6S", "PT0S", "1 Year and 2 Months ago"),
("P2Y7M", "PT0S", "2 Years and 7 Months ago"),
("P18M", "PT0S", "1 Year and 6 Months ago"),
("P6M15D", "PT0S", "6 Months ago"),
("P45D", "PT0S", "1 Month and 15 Days ago"),
("P25D", "PT0S", "25 Days ago"),
("P2DT12H", "PT0S", "2 Days and 12 Hours ago"),
("PT20H", "PT0S", "20 Hours ago"),
("P1DT30M", "PT0S", "1 Day and 1 Hour ago"),
("P2DT45M", "PT0S", "2 Days and 1 Hour ago"),
("P15DT8H", "PT0S", "15 Days ago"),
("P35DT12H30M", "PT0S", "1 Month and 5 Days ago"),
("P12M364640D", "P0D", "1000 Years ago"),
],
)
def test_iso8601_diff_to_human_flattened(a, b, expected):
assert iso8601_duration_to_human(iso8601_diff(a, b), flatten=True) == expected, (
f"Failed for {a} vs {b}: Got {iso8601_duration_to_human(iso8601_diff(a, b), flatten=True)}"
)
@pytest.mark.parametrize(
"amount, unit, expected",
[
# Minutes
(5, "minutes", "PT5M"),
(1, "minute", "PT1M"),
# Hours
(3, "hours", "PT3H"),
# Days
(2, "days", "P2D"),
# Weeks
(2, "weeks", "P2W"),
# Months (handled specially in the date section)
(7, "months", "P7M"),
# Years
(4, "years", "P4Y"),
# Negative amount should be converted to positive duration
(-5, "hours", "PT5H"),
# 1000 years
(1000, "years", "P1000Y"),
],
)
def test_amount_unit_to_iso8601_duration_valid(amount: int, unit: str, expected: str):
"""Ensure valid (amount, unit) pairs are converted to the correct ISO-8601 duration."""
assert amount_unit_to_iso8601_duration(amount, unit) == expected
@pytest.mark.parametrize(
"amount, unit",
[
(1, "invalid"),
(0, "centuries"),
],
)
def test_amount_unit_to_iso8601_duration_invalid(amount: int, unit: str):
"""Ensure invalid units raise ValueError."""
with pytest.raises(ValueError):
amount_unit_to_iso8601_duration(amount, unit)
@pytest.mark.parametrize(
"iso_duration, expected_amount, expected_unit",
[
# Single-unit durations
("PT5M", 5, "minutes"),
("PT1M", 1, "minutes"),
("PT3H", 3, "hours"),
("PT1H", 1, "hours"),
("P2D", 2, "days"),
("P1D", 1, "days"),
("P7M", 7, "months"),
("P1M", 1, "months"),
("P4Y", 4, "years"),
("P1Y", 1, "years"),
# Weeks convert to days
("P2W", 14, "days"),
("P1W", 7, "days"),
# Zero duration
("PT0S", 0, "minutes"),
("PT0M", 0, "minutes"),
("P0D", 0, "minutes"),
# Mixed time-only: days + hours → hours
("P1DT2H", 26, "hours"),
# Mixed time-only: hours + minutes → minutes
("PT1H30M", 90, "minutes"),
# Mixed time-only: days + minutes → minutes
("P1DT30M", 1470, "minutes"),
# Days that are exact multiples of 24h
("PT48H", 2, "days"),
("PT24H", 1, "days"),
# Years + months → months
("P1Y6M", 18, "months"),
("P2Y3M", 27, "months"),
# Months + sub-month components → months (lossy)
("P1MT2H", 1, "months"),
# Large values
("P1000Y", 1000, "years"),
],
)
def test_iso8601_duration_to_amount_unit(iso_duration, expected_amount, expected_unit):
"""Ensure ISO-8601 durations are converted to the correct (amount, unit) pair."""
amount, unit = iso8601_duration_to_amount_unit(iso_duration)
assert amount == expected_amount, (
f"For {iso_duration}: expected amount {expected_amount}, got {amount}"
)
assert unit == expected_unit, (
f"For {iso_duration}: expected unit {expected_unit}, got {unit}"
)
@pytest.mark.parametrize(
"amount, unit",
[
(5, "minutes"),
(3, "hours"),
(2, "days"),
(7, "months"),
(4, "years"),
],
)
def test_iso8601_duration_roundtrip(amount, unit):
"""Verify that amount_unit_to_iso8601_duration and iso8601_duration_to_amount_unit
are inverses for single-unit durations."""
iso = amount_unit_to_iso8601_duration(amount, unit)
rt_amount, rt_unit = iso8601_duration_to_amount_unit(iso)
assert rt_amount == amount, (
f"Roundtrip failed for ({amount}, {unit}): got amount {rt_amount}"
)
assert rt_unit == unit, (
f"Roundtrip failed for ({amount}, {unit}): got unit {rt_unit}"
)