fix: validate horizon_days before delete + wrapper forwarding tests (NQ-86-001, NQ-86-002)

This commit is contained in:
2026-09-17 17:05:50 -04:00
parent 8ab8260b33
commit b274741b0c
3 changed files with 172 additions and 3 deletions
+73
View File
@@ -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
+69
View File
@@ -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)