152 lines
7.0 KiB
Python
152 lines
7.0 KiB
Python
"""Test suite for ``lore_engine_poc.time_model`` (slice 1, sub-slice 1.6).
|
|
|
|
These cases cover the AC 1.3 contract: ``time_in_window`` is the
|
|
load-bearing primitive; its behaviour must be locked down before
|
|
slice 2's consistency engine trusts it. The original 13 inline
|
|
self-tests are preserved verbatim; the additional cases cover
|
|
month/day precision, ``current`` token resolution, null-bound
|
|
semantics, and malformed input handling — all the cases called out
|
|
in the slice 1 plan.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from lore_engine_poc.time_model import (
|
|
CURRENT,
|
|
NULL_TOKENS,
|
|
is_descendant,
|
|
normalize,
|
|
time_in_window,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Original 13 cases from the slice 0 self-test block.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ORIGINAL_CASES = [
|
|
# (at, lo, hi, current, expected, description)
|
|
("3rd_age.year_345", "3rd_age.year_340", "3rd_age.year_352", None, True, "year inside year window"),
|
|
("3rd_age.year_352", "3rd_age.year_340", "3rd_age.year_352", None, False, "year at exclusive upper"),
|
|
("3rd_age.year_340", "3rd_age.year_340", "3rd_age.year_352", None, True, "year at inclusive lower"),
|
|
("3rd_age", "3rd_age.year_1", "3rd_age.year_999", None, True, "era ancestor of lo"),
|
|
("3rd_age.year_5", "3rd_age", "3rd_age.year_10", None, True, "at is descendant of lo"),
|
|
("3rd_age.age_of_iron.year_3", "3rd_age.age_of_iron.year_1", "3rd_age.age_of_iron.year_5", None, True, "sub-era window"),
|
|
("3rd_age.age_of_iron.year_6", "3rd_age.age_of_iron.year_1", "3rd_age.age_of_iron.year_5", None, False, "sub-era past upper"),
|
|
(None, "3rd_age.year_1", "3rd_age.year_10", None, False, "open at, bounded"),
|
|
("3rd_age.year_5", None, "3rd_age.year_10", None, True, "open lower"),
|
|
("3rd_age.year_50", "3rd_age.year_10", None, None, True, "open upper"),
|
|
("current", "3rd_age.year_300", "3rd_age.year_400", "3rd_age.year_345", True, "current inside"),
|
|
("current", "3rd_age.year_300", "3rd_age.year_400", "3rd_age.year_500", False, "current outside"),
|
|
("2nd_age.year_5", "3rd_age", "4th_age", None, False, "different era"),
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Additional cases (slice 1, AC 1.3). The plan calls for ≥30 total; we ship
|
|
# 13 + 22 = 35.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ADDITIONAL_CASES = [
|
|
# Month/day precision — slice 1 plan §"month/day precision".
|
|
("3rd_age.year_345.month_3", "3rd_age.year_345", "3rd_age.year_346", None, True, "month inside year window"),
|
|
("3rd_age.year_345.month_3.day_17", "3rd_age.year_345", "3rd_age.year_346", None, True, "day inside year window"),
|
|
("3rd_age.year_345.month_4.day_1", "3rd_age.year_345", "3rd_age.year_346", None, True, "next-month day inside year"),
|
|
("3rd_age.year_345.month_3.day_17", "3rd_age.year_345.month_3.day_1", "3rd_age.year_345.month_3.day_31", None, True, "day inside day-window"),
|
|
|
|
# Era-boundary cases.
|
|
("3rd_age.age_of_iron.year_1", "3rd_age.age_of_iron.year_1", "3rd_age.age_of_iron.year_5", None, True, "first year of sub-era inclusive"),
|
|
("3rd_age.age_of_iron.year_5", "3rd_age.age_of_iron.year_1", "3rd_age.age_of_iron.year_5", None, False, "last year of sub-era exclusive"),
|
|
|
|
# ``current`` token resolution.
|
|
("current", "current", "3rd_age.year_400", "3rd_age.year_345", True, "current as lower bound resolved"),
|
|
("3rd_age.year_500", "3rd_age.year_300", "current", "3rd_age.year_345", False, "fixed at past current bound"),
|
|
|
|
# Null-bound semantics.
|
|
(None, None, None, None, True, "all three null"),
|
|
("3rd_age.year_5", None, None, None, True, "at present no bounds"),
|
|
(None, "3rd_age.year_1", "3rd_age.year_10", None, False, "open at with bounded window"),
|
|
("3rd_age.year_5", "3rd_age.year_1", None, None, True, "open upper with at inside"),
|
|
("3rd_age.year_5", None, "3rd_age.year_10", None, True, "open lower with at inside"),
|
|
|
|
# Era-tree membership from the OTHER side — at is a coarse era,
|
|
# the window is fine-grained. Conservatively true: the coarse
|
|
# at overlaps the era the window defines.
|
|
("3rd_age", "3rd_age.year_300", "3rd_age.year_400", None, True, "coarse at inside fine window"),
|
|
("3rd_age", "2nd_age.year_300", "2nd_age.year_400", None, False, "coarse at in a different era than window"),
|
|
|
|
# Half-open window: a_from == at is inclusive.
|
|
("3rd_age.year_300", "3rd_age.year_300", "3rd_age.year_400", None, True, "at == lo inclusive"),
|
|
("3rd_age.year_400", "3rd_age.year_300", "3rd_age.year_400", None, False, "at == hi exclusive"),
|
|
|
|
# Wrong-format strings raise.
|
|
("not a time", "3rd_age.year_1", "3rd_age.year_10", None, "?", "malformed at is rejected"),
|
|
("3rd_age.year_5", "not a lo", "3rd_age.year_10", None, "?", "malformed lo is rejected"),
|
|
("3rd_age.year_5", "3rd_age.year_1", "not a hi", None, "?", "malformed hi is rejected"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"at, lo, hi, current, expected, desc",
|
|
ORIGINAL_CASES + ADDITIONAL_CASES,
|
|
ids=[c[5] for c in ORIGINAL_CASES + ADDITIONAL_CASES],
|
|
)
|
|
def test_time_in_window(at, lo, hi, current, expected, desc):
|
|
if expected == "?":
|
|
# Wrong-format strings must raise, not silently return.
|
|
with pytest.raises(Exception):
|
|
time_in_window(at, lo, hi, current_time=current)
|
|
return
|
|
assert time_in_window(at, lo, hi, current_time=current) is expected, desc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Direct unit tests on the helpers — these catch regressions in the
|
|
# orthogonal primitives (normalize, is_descendant) without going through
|
|
# time_in_window.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw, expected",
|
|
[
|
|
(None, None),
|
|
("", None),
|
|
("null", None),
|
|
("none", None),
|
|
(" 3rd_age.year_345 ", "3rd_age.year_345"),
|
|
("CURRENT", "current"),
|
|
("3rd_age.year_345", "3rd_age.year_345"),
|
|
],
|
|
)
|
|
def test_normalize(raw, expected):
|
|
assert normalize(raw) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"child, ancestor, expected",
|
|
[
|
|
("3rd_age.year_345", "3rd_age", True),
|
|
("3rd_age.age_of_iron", "3rd_age", True),
|
|
("3rd_age.age_of_iron.year_3", "3rd_age.age_of_iron", True),
|
|
("3rd_age.age_of_iron.year_3", "3rd_age", True),
|
|
("2nd_age.year_5", "3rd_age", False),
|
|
("3rd_age", "3rd_age", True),
|
|
(None, "3rd_age", False),
|
|
("3rd_age.year_5", None, False),
|
|
# The "vord" case: 3rd_age_vord is NOT a descendant of 3rd_age_v
|
|
# because the prefix match is "3rd_age_v" not "3rd_age."
|
|
("3rd_age_vord.year_5", "3rd_age", False),
|
|
],
|
|
)
|
|
def test_is_descendant(child, ancestor, expected):
|
|
assert is_descendant(child, ancestor) is expected
|
|
|
|
|
|
def test_current_token_requires_current_time():
|
|
"""AC 1.6: 'current' without current_time raises ValueError."""
|
|
with pytest.raises(ValueError):
|
|
time_in_window("current", "3rd_age.year_1", "3rd_age.year_10")
|