Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72043ae3cf | ||
|
|
20caf4c91b | ||
|
|
d277d5816e |
@@ -151,14 +151,6 @@ async def materialize(
|
|||||||
Assistant's configured time zone MUST pass that same date here so a
|
Assistant's configured time zone MUST pass that same date here so a
|
||||||
time zone behind the host around midnight is not rejected as "past".
|
time zone behind the host around midnight is not rejected as "past".
|
||||||
|
|
||||||
``start_date`` is clamped up to the resolved "today" (the same
|
|
||||||
``today`` anchor threaded into the no-past guard), so a caller that
|
|
||||||
passes a ``start_date`` already in the past never yields a
|
|
||||||
past-dated instance: the walk simply begins on today and skips every
|
|
||||||
earlier date. A range entirely in the past therefore produces zero
|
|
||||||
instances rather than an error, and the DAO's no-past guard remains
|
|
||||||
the authoritative backstop under the lock.
|
|
||||||
|
|
||||||
All inputs are read in ONE locked transaction; each tuple's write then
|
All inputs are read in ONE locked transaction; each tuple's write then
|
||||||
goes through the atomic :meth:`~.dao_instances.QuestInstancesDao.upsert_if_valid`,
|
goes through the atomic :meth:`~.dao_instances.QuestInstancesDao.upsert_if_valid`,
|
||||||
which re-validates the tuple and its current ``due_time`` in the same
|
which re-validates the tuple and its current ``due_time`` in the same
|
||||||
@@ -192,13 +184,6 @@ async def materialize(
|
|||||||
start = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
|
start = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||||
end = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
|
end = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
|
||||||
|
|
||||||
# Clamp a past start up to today (the HA-local anchor when the caller
|
|
||||||
# pinned one, else the host clock) so the walk can never emit a
|
|
||||||
# past-dated instance; the DAO's no-past guard remains the backstop.
|
|
||||||
# A range entirely in the past therefore yields zero instances.
|
|
||||||
today_date = _resolve_today(today)
|
|
||||||
start = max(start, today_date)
|
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
cursor = start
|
cursor = start
|
||||||
while cursor <= end:
|
while cursor <= end:
|
||||||
|
|||||||
@@ -631,14 +631,13 @@ def test_materialize_rejects_malformed_or_inverted_bounds(tmp_path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_materialize_accepts_ha_local_today_behind_host(tmp_path) -> None:
|
def test_materialize_accepts_ha_local_today_behind_host(tmp_path) -> None:
|
||||||
"""The clamp and no-past guard compare against a caller-supplied HA-local today.
|
"""The no-past guard compares against a caller-supplied HA-local today.
|
||||||
|
|
||||||
A household time zone behind the host clock around midnight resolves a
|
A household time zone behind the host clock around midnight resolves a
|
||||||
HA-local "today" one calendar day earlier than the host's
|
HA-local "today" one calendar day earlier than the host's
|
||||||
``date.today()``. The walk clamps its start to that anchor: without
|
``date.today()``. The host-clock guard would reject that date as past,
|
||||||
a pinned HA-local date the effective start is the host's today (one
|
so the resolved HA-local date must be threaded through ``materialize``
|
||||||
day after ha_today), so the now-past day is skipped rather than
|
and accepted instead of raising.
|
||||||
rejected, and only the host-today-onward days materialize.
|
|
||||||
"""
|
"""
|
||||||
async def _body(database):
|
async def _body(database):
|
||||||
children = ChildrenDao(database)
|
children = ChildrenDao(database)
|
||||||
@@ -654,13 +653,12 @@ def test_materialize_accepts_ha_local_today_behind_host(tmp_path) -> None:
|
|||||||
["morning"],
|
["morning"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Without a pinned HA-local today, the walk clamps its start to the
|
# Without the pinned HA-local today, the host-clock guard rejects
|
||||||
# host's today (one day after ha_today); the past day is dropped and
|
# the horizon start (ha_today is "yesterday" on the host).
|
||||||
# the 14 host-today-onward days materialize.
|
with pytest.raises(ValueError, match="never generated in the past"):
|
||||||
without_pin = await materialize(database, start_iso, end_iso)
|
await materialize(database, start_iso, end_iso)
|
||||||
assert without_pin == 14
|
|
||||||
|
|
||||||
# With today=ha_today, the full 15-day horizon materializes.
|
# With today=ha_today, the same range is accepted and materialized.
|
||||||
count = await materialize(database, start_iso, end_iso, today=ha_today)
|
count = await materialize(database, start_iso, end_iso, today=ha_today)
|
||||||
assert count == 15
|
assert count == 15
|
||||||
return count
|
return count
|
||||||
@@ -668,50 +666,6 @@ def test_materialize_accepts_ha_local_today_behind_host(tmp_path) -> None:
|
|||||||
_with_db(tmp_path, "materialize-ha-local-today.db")(_body)
|
_with_db(tmp_path, "materialize-ha-local-today.db")(_body)
|
||||||
|
|
||||||
|
|
||||||
def test_materialize_clamps_past_start_to_today(tmp_path) -> None:
|
|
||||||
"""A past start_date is clamped to today: no past-dated instance is created.
|
|
||||||
|
|
||||||
A caller passing a start one month in the past never yields a
|
|
||||||
past-dated instance: the walk clamps its effective start up to today
|
|
||||||
and materializes only dates on/after today, leaving the past untouched
|
|
||||||
rather than raising or corrupting state.
|
|
||||||
"""
|
|
||||||
async def _body(database):
|
|
||||||
children = ChildrenDao(database)
|
|
||||||
child = await children.create("Ada", NOW)
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
|
||||||
start_iso = (today - datetime.timedelta(days=30)).isoformat()
|
|
||||||
end_iso = (today + datetime.timedelta(days=14)).isoformat()
|
|
||||||
|
|
||||||
await create_quest_definition(
|
|
||||||
database,
|
|
||||||
"Daily chore",
|
|
||||||
ScheduleRule(rule_type=RuleType.DAILY, start_date=start_iso),
|
|
||||||
[child.id],
|
|
||||||
["morning"],
|
|
||||||
)
|
|
||||||
|
|
||||||
count = await materialize(database, start_iso, end_iso, today=today)
|
|
||||||
assert count == 15
|
|
||||||
|
|
||||||
dao = QuestInstancesDao(database)
|
|
||||||
records = await dao.list_by_date_range(child.id, start_iso, end_iso)
|
|
||||||
assert len(records) == 15
|
|
||||||
assert all(r.due_date >= today.isoformat() for r in records)
|
|
||||||
|
|
||||||
# The past month is empty: no past-dated instance was ever created.
|
|
||||||
past_records = await dao.list_by_date_range(
|
|
||||||
child.id,
|
|
||||||
start_iso,
|
|
||||||
(today - datetime.timedelta(days=1)).isoformat(),
|
|
||||||
)
|
|
||||||
assert past_records == []
|
|
||||||
return count
|
|
||||||
|
|
||||||
_with_db(tmp_path, "materialize-past-start.db")(_body)
|
|
||||||
|
|
||||||
|
|
||||||
def test_materialize_skips_inactive_definitions(tmp_path) -> None:
|
def test_materialize_skips_inactive_definitions(tmp_path) -> None:
|
||||||
async def _body(database):
|
async def _body(database):
|
||||||
from custom_components.nestquest.quest_definitions import (
|
from custom_components.nestquest.quest_definitions import (
|
||||||
|
|||||||
Reference in New Issue
Block a user