fix: validate horizon_days before delete + wrapper forwarding tests (NQ-86-001, NQ-86-002)
This commit is contained in:
@@ -1193,6 +1193,79 @@ def test_regenerate_for_child_respects_horizon_days(tmp_path) -> None:
|
||||
_with_db(tmp_path, "regenerate-child-horizon.db")(_body)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [-1, 0, True, False, "5", 1.5])
|
||||
def test_regenerate_for_definition_rejects_invalid_horizon_before_delete(
|
||||
tmp_path, bad
|
||||
) -> None:
|
||||
"""A malformed horizon fails BEFORE any future-instance delete."""
|
||||
async def _body(database):
|
||||
children = ChildrenDao(database)
|
||||
child = await children.create("Ada", NOW)
|
||||
|
||||
today = datetime.date.today()
|
||||
start_iso = today.isoformat()
|
||||
horizon_end = today + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||
|
||||
created = await create_quest_definition(
|
||||
database,
|
||||
"Daily chore",
|
||||
ScheduleRule(rule_type=RuleType.DAILY, start_date=start_iso),
|
||||
[child.id],
|
||||
[("morning", "09:00")],
|
||||
)
|
||||
dao = QuestInstancesDao(database)
|
||||
await materialize(database, start_iso, horizon_end.isoformat())
|
||||
|
||||
with pytest.raises(ValueError, match="horizon_days"):
|
||||
await regenerate_for_definition(
|
||||
database, created.definition.id, horizon_days=bad
|
||||
)
|
||||
|
||||
# The refused call deleted nothing: the seeded horizon survives.
|
||||
records = await dao.list_by_date_range(
|
||||
child.id, start_iso, horizon_end.isoformat()
|
||||
)
|
||||
assert len(records) == DEFAULT_HORIZON_DAYS + 1
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "regenerate-def-horizon-invalid.db")(_body)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [-1, 0, True, False, "5", 1.5])
|
||||
def test_regenerate_for_child_rejects_invalid_horizon_before_delete(
|
||||
tmp_path, bad
|
||||
) -> None:
|
||||
"""A malformed child horizon fails BEFORE any future-instance delete."""
|
||||
async def _body(database):
|
||||
children = ChildrenDao(database)
|
||||
child = await children.create("Ada", NOW)
|
||||
|
||||
today = datetime.date.today()
|
||||
start_iso = today.isoformat()
|
||||
horizon_end = today + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||
|
||||
await create_quest_definition(
|
||||
database,
|
||||
"Daily chore",
|
||||
ScheduleRule(rule_type=RuleType.DAILY, start_date=start_iso),
|
||||
[child.id],
|
||||
[("morning", "09:00")],
|
||||
)
|
||||
dao = QuestInstancesDao(database)
|
||||
await materialize(database, start_iso, horizon_end.isoformat())
|
||||
|
||||
with pytest.raises(ValueError, match="horizon_days"):
|
||||
await regenerate_for_child(database, child.id, horizon_days=bad)
|
||||
|
||||
records = await dao.list_by_date_range(
|
||||
child.id, start_iso, horizon_end.isoformat()
|
||||
)
|
||||
assert len(records) == DEFAULT_HORIZON_DAYS + 1
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "regenerate-child-horizon-invalid.db")(_body)
|
||||
|
||||
|
||||
#: Fixed end-to-end anchor: 2026-06-01 is a Monday AND the 1st of its month,
|
||||
#: so the two-week custody cycle is week-aligned and the yearly rule's
|
||||
#: "1st of the anchor month" is GUARANTEED to fire inside the 28-day window
|
||||
|
||||
@@ -8,6 +8,7 @@ import re
|
||||
import pytest
|
||||
|
||||
from custom_components.nestquest.dao_children import ChildrenDao
|
||||
from custom_components.nestquest.const import DEFAULT_HORIZON_DAYS
|
||||
from custom_components.nestquest.dao_instances import (
|
||||
CompletionEventsDao,
|
||||
QuestInstancesDao,
|
||||
@@ -1938,3 +1939,71 @@ def test_full_lifecycle_preserves_completed_and_history(tmp_path) -> None:
|
||||
return reactivated
|
||||
|
||||
_with_db(tmp_path, "full-lifecycle.db")(_body)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode",
|
||||
["edit", "assign", "unassign", "set_active"],
|
||||
)
|
||||
def test_wrappers_forward_horizon_days(tmp_path, mode) -> None:
|
||||
"""The four change wrappers forward a non-default horizon_days.
|
||||
|
||||
Each wrapper threads ``horizon_days`` into the regeneration it triggers;
|
||||
passing 5 here must bound the regenerated window to today..today+5 —
|
||||
proving the forwarding argument is actually honored, not dropped.
|
||||
"""
|
||||
async def _body(database, children, rules, definitions, child):
|
||||
created = await create_quest_definition(
|
||||
database,
|
||||
"Chore",
|
||||
ScheduleRule(rule_type=RuleType.DAILY, start_date=_today_iso()),
|
||||
[child.id],
|
||||
["morning"],
|
||||
)
|
||||
definition_id = created.definition.id
|
||||
instances = QuestInstancesDao(database)
|
||||
|
||||
if mode == "edit":
|
||||
await edit_quest_definition(
|
||||
database,
|
||||
definition_id,
|
||||
windows=[("morning", "10:30")],
|
||||
horizon_days=5,
|
||||
)
|
||||
tracked = child
|
||||
elif mode == "assign":
|
||||
bo = await children.create("Bo", NOW)
|
||||
await assign_child(database, definition_id, bo.id, horizon_days=5)
|
||||
tracked = bo
|
||||
elif mode == "unassign":
|
||||
# The remaining assignee (``child``) must be regenerated over
|
||||
# the 5-day window after ``bo`` is removed.
|
||||
bo = await children.create("Bo", NOW)
|
||||
await assign_child(database, definition_id, bo.id)
|
||||
await unassign_child(
|
||||
database, definition_id, bo.id, horizon_days=5
|
||||
)
|
||||
tracked = child
|
||||
else: # set_active
|
||||
await set_quest_definition_active(
|
||||
database, definition_id, True, horizon_days=5
|
||||
)
|
||||
tracked = child
|
||||
|
||||
today = datetime.date.today()
|
||||
end = (today + datetime.timedelta(days=5)).isoformat()
|
||||
records = await instances.list_by_date_range(
|
||||
tracked.id, today.isoformat(), end
|
||||
)
|
||||
assert len(records) == 6
|
||||
|
||||
beyond = (today + datetime.timedelta(days=6)).isoformat()
|
||||
beyond_end = (
|
||||
today + datetime.timedelta(days=DEFAULT_HORIZON_DAYS)
|
||||
).isoformat()
|
||||
assert await instances.list_by_date_range(
|
||||
tracked.id, beyond, beyond_end
|
||||
) == []
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, f"wrapper-horizon-{mode}.db")(_body)
|
||||
|
||||
Reference in New Issue
Block a user