Remove the integration's database, migrations, DAOs and business modules (#172)
This commit was merged in pull request #172.
This commit is contained in:
+230
-211
@@ -1,75 +1,132 @@
|
||||
"""Tests for the per-child daily count and percentage sensors (Feature 10, task 2)."""
|
||||
"""Tests for the per-child daily count and percentage sensors (Feature 10, task 2).
|
||||
|
||||
Since the DB removal the snapshot comes from the API service's panel
|
||||
route, so these tests script the route's payload (the documented
|
||||
Feature 16 shape ``test_coordinator.py`` also uses) through
|
||||
:class:`conftest.StubSnapshotClient` instead of seeding a local
|
||||
database: the coordinator rebuilds the same core dataclasses the
|
||||
sensors read, and every assertion below is on entity state.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import wire_entry_to_registry
|
||||
from conftest import StubSnapshotClient, set_coordinator_client, wire_entry_to_registry
|
||||
|
||||
from custom_components.nestquest import async_setup_entry, async_unload_entry
|
||||
from custom_components.nestquest.children import create_child
|
||||
from custom_components.nestquest.completion import complete_instance
|
||||
from custom_components.nestquest.const import DOMAIN
|
||||
from custom_components.nestquest.materialize import materialize
|
||||
from custom_components.nestquest.quest_definitions import create_quest_definition
|
||||
from custom_components.nestquest.recurrence import ScheduleRule
|
||||
from custom_components.nestquest.sensor import (
|
||||
MAX_STATE_LENGTH,
|
||||
_state_within_limit,
|
||||
)
|
||||
|
||||
#: The seeding horizon: instances exist for today and a few days out,
|
||||
#: enough for the day snapshot without slowing the walk down.
|
||||
SEED_HORIZON_DAYS = 3
|
||||
|
||||
#: The documented per-child day sensor kinds (unique_id suffixes).
|
||||
SENSOR_KINDS = (
|
||||
"quests_due_today",
|
||||
"quests_completed_today",
|
||||
"quests_remaining_today",
|
||||
"completion_pct_today",
|
||||
)
|
||||
def _today_iso() -> str:
|
||||
return datetime.date.today().isoformat()
|
||||
|
||||
|
||||
def _today() -> datetime.date:
|
||||
return datetime.date.today()
|
||||
def _instance(instance_id: int, child_id: int, **overrides) -> dict:
|
||||
"""One panel instance in the API route's payload shape."""
|
||||
payload = {
|
||||
"id": instance_id,
|
||||
"definition_id": 3,
|
||||
"child_id": child_id,
|
||||
"title": "Brush teeth",
|
||||
"icon": "🦷",
|
||||
"window": "morning",
|
||||
"due_time": "08:00",
|
||||
"state": "open",
|
||||
"overdue": False,
|
||||
"completed_at": None,
|
||||
"on_time": None,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
async def _seed_children(database):
|
||||
"""Three active children: Ada and Bo share one daily quest; Cory has none."""
|
||||
ada = await create_child(database, "Ada")
|
||||
bo = await create_child(database, "Bo")
|
||||
cory = await create_child(database, "Cory")
|
||||
today = _today()
|
||||
await create_quest_definition(
|
||||
database,
|
||||
"Brush teeth",
|
||||
ScheduleRule.from_dict(
|
||||
{"rule_type": "daily", "start_date": today.isoformat()}
|
||||
),
|
||||
[ada.id, bo.id],
|
||||
["morning"],
|
||||
)
|
||||
end = (today + datetime.timedelta(days=SEED_HORIZON_DAYS)).isoformat()
|
||||
await materialize(database, today.isoformat(), end, today=today)
|
||||
return ada, bo, cory
|
||||
def _child(child_id: int, name: str, instances=(), **overrides) -> dict:
|
||||
"""One child in the API route's payload shape, counts precomputed.
|
||||
|
||||
|
||||
async def _setup_seeded_entry(hass, make_entry):
|
||||
"""Set up the integration, seed three children, refresh: sensors exist.
|
||||
|
||||
Children are seeded AFTER setup (the entry's database only exists
|
||||
once setup opens it), so the sensors appear through the refresh
|
||||
listener — the same path production takes when a child is added.
|
||||
The API precomputes the per-child rollups from the full day; the
|
||||
builder here derives them from the instance list so a payload can
|
||||
never drift from its own counts.
|
||||
"""
|
||||
entry = wire_entry_to_registry(make_entry(), hass.registry)
|
||||
completed = sum(1 for i in instances if i["state"] == "completed")
|
||||
due = max(len(instances), 0)
|
||||
payload = {
|
||||
"child_id": child_id,
|
||||
"child_name": name,
|
||||
"present": True,
|
||||
"next_present": None,
|
||||
"due_today": due,
|
||||
"completed_today": completed,
|
||||
"remaining_today": due - completed,
|
||||
"completion_pct": 100 if due == 0 else round(completed / due * 100),
|
||||
"instances": list(instances),
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
def _payload(children) -> dict:
|
||||
return {
|
||||
"today_iso": _today_iso(),
|
||||
"cycle_day": 0,
|
||||
"children": list(children),
|
||||
}
|
||||
|
||||
|
||||
#: The seeded household: Ada and Bo share one daily quest, Cory has none.
|
||||
ADA = 1
|
||||
BO = 2
|
||||
CORY = 3
|
||||
|
||||
|
||||
def _seeded_payload() -> dict:
|
||||
return _payload(
|
||||
[
|
||||
_child(ADA, "Ada", [_instance(7, ADA)]),
|
||||
_child(BO, "Bo", [_instance(8, BO)]),
|
||||
_child(CORY, "Cory"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _completed_payload() -> dict:
|
||||
"""The same household after the API recorded Ada's completion."""
|
||||
return _payload(
|
||||
[
|
||||
_child(
|
||||
ADA,
|
||||
"Ada",
|
||||
[_instance(7, ADA, state="completed", completed_at=f"{_today_iso()}T07:55:00+00:00", on_time=True)],
|
||||
),
|
||||
_child(BO, "Bo", [_instance(8, BO)]),
|
||||
_child(CORY, "Cory"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def _setup_entry(hass, make_entry, *payloads):
|
||||
"""Set up the integration with a scripted snapshot client.
|
||||
|
||||
The client serves the payloads in order (repeating the last), so
|
||||
setup's first refresh and any later explicit refresh each get the
|
||||
scripted state — the same path production takes through the API.
|
||||
"""
|
||||
entry = wire_entry_to_registry(
|
||||
make_entry(
|
||||
data={"api_base_url": "http://api.test:8000", "panel_token": "tok"}
|
||||
),
|
||||
hass.registry,
|
||||
)
|
||||
client = set_coordinator_client(entry, StubSnapshotClient(*payloads))
|
||||
assert await async_setup_entry(hass, entry) is True
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
database = entry.runtime_data.database
|
||||
ada, bo, cory = await _seed_children(database)
|
||||
await coordinator.async_refresh()
|
||||
return entry, coordinator, (ada, bo, cory)
|
||||
assert coordinator.api_client is client
|
||||
return entry, coordinator, client
|
||||
|
||||
|
||||
def _snapshot_child(coordinator, child_id):
|
||||
@@ -85,47 +142,31 @@ def _sensor(hass, child_id, kind):
|
||||
return hass.entities[f"{DOMAIN}_child_{child_id}_{kind}"]
|
||||
|
||||
|
||||
async def _complete_first_instance(database, coordinator, child) -> None:
|
||||
"""Complete the child's first open instance through the business layer."""
|
||||
snapshot_child = _snapshot_child(coordinator, child.id)
|
||||
instance_id = snapshot_child.instances[0].instance_id
|
||||
await complete_instance(
|
||||
database,
|
||||
instance_id,
|
||||
actor_source="panel",
|
||||
actor_child_id=child.id,
|
||||
)
|
||||
|
||||
|
||||
async def test_every_active_child_gets_five_sensors_with_documented_identity(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload(), _completed_payload()
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
assert hass.config_entries.forwarded_platforms == [
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
]
|
||||
await _complete_first_instance(database, coordinator, ada)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
# Two quest children plus the zero-quest child: three children x
|
||||
# (five sensors + two binary sensors) + three household sensors,
|
||||
# no duplicates.
|
||||
# Three children x (five sensors + two binary sensors) + three
|
||||
# household sensors, no duplicates.
|
||||
assert len(hass.entities) == 24
|
||||
expected = {
|
||||
ada.id: ("Ada", 1, 1, 0, 100),
|
||||
bo.id: ("Bo", 1, 0, 1, 0),
|
||||
cory.id: ("Cory", 0, 0, 0, 100),
|
||||
ADA: ("Ada", 1, 1, 0, 100),
|
||||
BO: ("Bo", 1, 0, 1, 0),
|
||||
CORY: ("Cory", 0, 0, 0, 100),
|
||||
}
|
||||
for child_id, (name, due, completed, remaining, pct) in expected.items():
|
||||
due_entity = _sensor(hass, child_id, "quests_due_today")
|
||||
completed_entity = _sensor(hass, child_id, "quests_completed_today")
|
||||
remaining_entity = _sensor(hass, child_id, "quests_remaining_today")
|
||||
pct_entity = _sensor(hass, child_id, "completion_pct_today")
|
||||
next_entity = _sensor(hass, child_id, "next_quest")
|
||||
assert due_entity.name == f"NestQuest {name} quests due today"
|
||||
assert completed_entity.name == (
|
||||
f"NestQuest {name} quests completed today"
|
||||
@@ -151,11 +192,11 @@ async def test_every_active_child_gets_five_sensors_with_documented_identity(
|
||||
|
||||
|
||||
async def test_zero_quest_child_is_a_perfect_day(hass, make_entry) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, _coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload()
|
||||
)
|
||||
due_entity = _sensor(hass, cory.id, "quests_due_today")
|
||||
pct_entity = _sensor(hass, cory.id, "completion_pct_today")
|
||||
due_entity = _sensor(hass, CORY, "quests_due_today")
|
||||
pct_entity = _sensor(hass, CORY, "completion_pct_today")
|
||||
assert due_entity.native_value == 0
|
||||
assert pct_entity.native_value == 100, (
|
||||
"zero quests is a perfect day, not a division error"
|
||||
@@ -168,14 +209,11 @@ async def test_zero_quest_child_is_a_perfect_day(hass, make_entry) -> None:
|
||||
async def test_due_today_attributes_carry_the_panel_payload(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _completed_payload()
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
await _complete_first_instance(database, coordinator, ada)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
ada_due = _sensor(hass, ada.id, "quests_due_today")
|
||||
ada_due = _sensor(hass, ADA, "quests_due_today")
|
||||
attributes = ada_due.extra_state_attributes
|
||||
assert set(attributes) == {
|
||||
"instances",
|
||||
@@ -184,7 +222,7 @@ async def test_due_today_attributes_carry_the_panel_payload(
|
||||
"child_name",
|
||||
"present",
|
||||
}
|
||||
assert attributes["child_id"] == ada.id
|
||||
assert attributes["child_id"] == ADA
|
||||
assert attributes["child_name"] == "Ada"
|
||||
assert attributes["present"] is True
|
||||
[instance] = attributes["instances"]
|
||||
@@ -201,7 +239,7 @@ async def test_due_today_attributes_carry_the_panel_payload(
|
||||
"completed_at",
|
||||
"on_time",
|
||||
}
|
||||
assert instance["child_id"] == ada.id
|
||||
assert instance["child_id"] == ADA
|
||||
assert instance["title"] == "Brush teeth"
|
||||
assert instance["window"] == "morning"
|
||||
assert instance["state"] == "completed", (
|
||||
@@ -214,7 +252,7 @@ async def test_due_today_attributes_carry_the_panel_payload(
|
||||
"with nothing missed, admin and panel payloads are identical"
|
||||
)
|
||||
|
||||
bo_due = _sensor(hass, bo.id, "quests_due_today")
|
||||
bo_due = _sensor(hass, BO, "quests_due_today")
|
||||
[bo_instance] = bo_due.extra_state_attributes["instances"]
|
||||
assert bo_instance["state"] == "open"
|
||||
assert bo_instance["completed_at"] is None
|
||||
@@ -222,64 +260,64 @@ async def test_due_today_attributes_carry_the_panel_payload(
|
||||
|
||||
|
||||
async def test_due_today_payload_omits_missed_admin_equals_panel(
|
||||
hass, make_entry, monkeypatch
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""D-009: the panel payload omits missed instances.
|
||||
|
||||
Since Feature 18 the snapshot comes from the API panel route,
|
||||
which omits missed rows entirely (the admin surface for missed
|
||||
quests is the PWA, reading the API directly). The entity payload
|
||||
therefore cannot carry missed rows any more: ``admin_instances``
|
||||
equals ``instances`` — open and completed rows only. A missed
|
||||
instance is still derived locally (``due_date`` before the
|
||||
snapshot's "today"); the pinned-FUTURE clock at the
|
||||
``derive_state`` boundary makes today's open instances read
|
||||
missed through the same derivation rule, and this test pins the
|
||||
change: neither payload carries them, and the count rollups
|
||||
(precomputed server-side from the full day) are unaffected here
|
||||
because the API counts arrive per the route's own build.
|
||||
quests is the PWA, reading the API directly). The integration
|
||||
renders exactly what the route sent: a snapshot can never carry a
|
||||
``missed`` view, ``admin_instances`` equals ``instances``, and the
|
||||
precomputed counts arrive untouched — pinned here with an open
|
||||
instance whose derived state reads missed (a past ``due_time``
|
||||
with the counts already rolled up the way the route builds them).
|
||||
"""
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
payload = _payload(
|
||||
[
|
||||
_child(
|
||||
BO,
|
||||
"Bo",
|
||||
[
|
||||
_instance(
|
||||
8,
|
||||
BO,
|
||||
due_time="00:01",
|
||||
overdue=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
import custom_components.nestquest.core.snapshot as snapshot_module
|
||||
entry, _coordinator, _client = await _setup_entry(hass, make_entry, payload)
|
||||
|
||||
tomorrow = _today() + datetime.timedelta(days=1)
|
||||
real_derive_state = snapshot_module.derive_state
|
||||
|
||||
def _derive_against_pinned_future(instance, latest, today):
|
||||
return real_derive_state(instance, latest, tomorrow)
|
||||
|
||||
monkeypatch.setattr(
|
||||
snapshot_module, "derive_state", _derive_against_pinned_future
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
bo_due = _sensor(hass, bo.id, "quests_due_today")
|
||||
bo_due = _sensor(hass, BO, "quests_due_today")
|
||||
attributes = bo_due.extra_state_attributes
|
||||
assert attributes["instances"] == [], (
|
||||
"missed instances are omitted from the panel payload"
|
||||
)
|
||||
assert attributes["admin_instances"] == [], (
|
||||
assert attributes["instances"], "an open row renders in the panel payload"
|
||||
assert attributes["admin_instances"] == attributes["instances"], (
|
||||
"the API payload omits missed rows, so the admin payload "
|
||||
"carries none either (the PWA is the admin surface now)"
|
||||
)
|
||||
# Counts still roll the missed instance up as not-completed: the
|
||||
# counts arrive precomputed from the route's own build, which rolls
|
||||
# the FULL day (the missed row included) into the numbers.
|
||||
assert _sensor(hass, bo.id, "quests_completed_today").native_value == 0
|
||||
assert _sensor(hass, bo.id, "quests_remaining_today").native_value == 1
|
||||
# The counts arrive precomputed from the route's own build, which
|
||||
# rolls the FULL day (any missed row included) into the numbers.
|
||||
assert _sensor(hass, BO, "quests_completed_today").native_value == 0
|
||||
assert _sensor(hass, BO, "quests_remaining_today").native_value == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", SENSOR_KINDS)
|
||||
@pytest.mark.parametrize("kind", (
|
||||
"quests_due_today",
|
||||
"quests_completed_today",
|
||||
"quests_remaining_today",
|
||||
"completion_pct_today",
|
||||
))
|
||||
async def test_no_sensor_state_exceeds_the_255_character_limit(
|
||||
hass, make_entry, kind
|
||||
) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, _coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload()
|
||||
)
|
||||
for child in (ada, bo, cory):
|
||||
entity = _sensor(hass, child.id, kind)
|
||||
for child_id in (ADA, BO, CORY):
|
||||
entity = _sensor(hass, child_id, kind)
|
||||
state = entity.native_value
|
||||
assert len(str(state)) <= MAX_STATE_LENGTH
|
||||
|
||||
@@ -295,14 +333,13 @@ def test_state_within_limit_refuses_overlong_strings() -> None:
|
||||
async def test_sensors_update_on_refresh_after_a_completion(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload(), _completed_payload()
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
completed_entity = _sensor(hass, ada.id, "quests_completed_today")
|
||||
remaining_entity = _sensor(hass, ada.id, "quests_remaining_today")
|
||||
pct_entity = _sensor(hass, ada.id, "completion_pct_today")
|
||||
due_entity = _sensor(hass, ada.id, "quests_due_today")
|
||||
completed_entity = _sensor(hass, ADA, "quests_completed_today")
|
||||
remaining_entity = _sensor(hass, ADA, "quests_remaining_today")
|
||||
pct_entity = _sensor(hass, ADA, "completion_pct_today")
|
||||
due_entity = _sensor(hass, ADA, "quests_due_today")
|
||||
assert completed_entity.native_value == 0
|
||||
assert remaining_entity.native_value == 1
|
||||
assert pct_entity.native_value == 0
|
||||
@@ -318,7 +355,6 @@ async def test_sensors_update_on_refresh_after_a_completion(
|
||||
|
||||
due_entity.async_write_ha_state = _record_write
|
||||
|
||||
await _complete_first_instance(database, coordinator, ada)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
assert completed_entity.native_value == 1
|
||||
@@ -330,8 +366,8 @@ async def test_sensors_update_on_refresh_after_a_completion(
|
||||
async def test_repeated_refreshes_do_not_duplicate_entities(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload()
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
await coordinator.async_refresh()
|
||||
@@ -341,8 +377,9 @@ async def test_repeated_refreshes_do_not_duplicate_entities(
|
||||
async def test_setup_with_no_children_registers_only_household(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
entry = wire_entry_to_registry(make_entry(), hass.registry)
|
||||
assert await async_setup_entry(hass, entry) is True
|
||||
entry, _coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _payload([])
|
||||
)
|
||||
# No children means no per-child entities; the three household
|
||||
# rollups still exist (an empty household is a valid 0).
|
||||
assert set(hass.entities) == {
|
||||
@@ -354,8 +391,7 @@ async def test_setup_with_no_children_registers_only_household(
|
||||
|
||||
|
||||
async def test_unload_unloads_platforms(hass, make_entry) -> None:
|
||||
entry = wire_entry_to_registry(make_entry(), hass.registry)
|
||||
assert await async_setup_entry(hass, entry) is True
|
||||
entry, _coordinator, _client = await _setup_entry(hass, make_entry)
|
||||
assert await async_unload_entry(hass, entry) is True
|
||||
assert hass.config_entries.unloaded_platforms == [
|
||||
"sensor",
|
||||
@@ -366,20 +402,23 @@ async def test_unload_unloads_platforms(hass, make_entry) -> None:
|
||||
async def test_rename_propagates_to_name_and_device_label(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""A manage_child rename shows on the next refresh: the name and
|
||||
device label re-derive from the snapshot while the unique_id (and
|
||||
therefore history) is untouched."""
|
||||
from custom_components.nestquest.children import edit_child
|
||||
|
||||
entry, coordinator, (ada, _bo, _cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
"""A rename in the API's snapshot shows on the next refresh: the
|
||||
name and device label re-derive from the snapshot while the
|
||||
unique_id (and therefore history) is untouched."""
|
||||
renamed = _payload(
|
||||
[
|
||||
_child(ADA, "Mirren", [_instance(7, ADA)]),
|
||||
_child(BO, "Bo", [_instance(8, BO)]),
|
||||
_child(CORY, "Cory"),
|
||||
]
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
due_sensor = _sensor(hass, ada.id, "quests_due_today")
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload(), renamed
|
||||
)
|
||||
due_sensor = _sensor(hass, ADA, "quests_due_today")
|
||||
original_unique_id = due_sensor.unique_id
|
||||
assert due_sensor.name.startswith("NestQuest Ada ")
|
||||
|
||||
await edit_child(database, ada.id, display_name="Mirren")
|
||||
await coordinator.async_refresh()
|
||||
|
||||
assert due_sensor.name.startswith("NestQuest Mirren ")
|
||||
@@ -391,12 +430,11 @@ async def test_failed_platform_unload_returns_false_and_retains_runtime(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""When HA reports platforms still loaded, the unload must NOT
|
||||
close the database the live entities read: it returns False and
|
||||
keeps the runtime record for a retry."""
|
||||
entry, _coordinator, _children = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
tear down the coordinator the live entities read: it returns False
|
||||
and keeps the runtime record for a retry."""
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload()
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
|
||||
async def _refuse_unload(entry_arg, platforms):
|
||||
return False
|
||||
@@ -404,26 +442,27 @@ async def test_failed_platform_unload_returns_false_and_retains_runtime(
|
||||
hass.config_entries.async_unload_platforms = _refuse_unload
|
||||
assert await async_unload_entry(hass, entry) is False
|
||||
assert hass.data[DOMAIN][entry.entry_id] is entry.runtime_data
|
||||
assert database.connected is True
|
||||
assert entry.runtime_data.coordinator is coordinator
|
||||
|
||||
|
||||
async def test_next_quest_sensor_reports_earliest_open_quest(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""The next-quest sensor carries the earliest open quest's title in
|
||||
state with the full shape in attributes."""
|
||||
entry, coordinator, (ada, _bo, _cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _seeded_payload()
|
||||
)
|
||||
next_entity = _sensor(hass, ada.id, "next_quest")
|
||||
next_entity = _sensor(hass, ADA, "next_quest")
|
||||
assert next_entity.name == f"NestQuest Ada next quest"
|
||||
assert next_entity.native_value == "Brush teeth"
|
||||
attributes = next_entity.extra_state_attributes
|
||||
assert attributes["title"] == "Brush teeth"
|
||||
assert attributes["window"] == "morning"
|
||||
assert attributes["instance_id"] == _snapshot_child(
|
||||
coordinator, ada.id
|
||||
coordinator, ADA
|
||||
).instances[0].instance_id
|
||||
assert attributes["child_id"] == ada.id
|
||||
assert attributes["child_id"] == ADA
|
||||
|
||||
|
||||
async def test_next_quest_sensor_reports_none_sentinel_when_day_clear(
|
||||
@@ -431,14 +470,11 @@ async def test_next_quest_sensor_reports_none_sentinel_when_day_clear(
|
||||
) -> None:
|
||||
"""A child with every quest done (and a zero-quest child) reports
|
||||
the documented sentinel, never an empty string."""
|
||||
entry, coordinator, (ada, _bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
entry, _coordinator, _client = await _setup_entry(
|
||||
hass, make_entry, _completed_payload()
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
await _complete_first_instance(database, coordinator, ada)
|
||||
await coordinator.async_refresh()
|
||||
assert _sensor(hass, ada.id, "next_quest").native_value == "none"
|
||||
assert _sensor(hass, cory.id, "next_quest").native_value == "none"
|
||||
assert _sensor(hass, ADA, "next_quest").native_value == "none"
|
||||
assert _sensor(hass, CORY, "next_quest").native_value == "none"
|
||||
|
||||
|
||||
async def test_next_quest_state_truncates_over_the_limit(
|
||||
@@ -446,23 +482,15 @@ async def test_next_quest_state_truncates_over_the_limit(
|
||||
) -> None:
|
||||
"""A quest title longer than the state limit is truncated; the
|
||||
attribute keeps the full title."""
|
||||
entry, coordinator, (ada, _bo, _cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
long_title = "A very long quest title. " * 30
|
||||
payload = _payload(
|
||||
[_child(ADA, "Ada", [_instance(7, ADA, title=long_title)])]
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
from custom_components.nestquest.quest_definitions import (
|
||||
edit_quest_definition,
|
||||
)
|
||||
|
||||
snapshot_child = _snapshot_child(coordinator, ada.id)
|
||||
await edit_quest_definition(
|
||||
database,
|
||||
snapshot_child.instances[0].definition_id,
|
||||
title="A very long quest title. " * 30,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
next_entity = _sensor(hass, ada.id, "next_quest")
|
||||
full_title = _snapshot_child(coordinator, ada.id).instances[0].title
|
||||
entry, _coordinator, _client = await _setup_entry(hass, make_entry, payload)
|
||||
next_entity = _sensor(hass, ADA, "next_quest")
|
||||
full_title = _snapshot_child(
|
||||
entry.runtime_data.coordinator, ADA
|
||||
).instances[0].title
|
||||
assert len(full_title) > 255
|
||||
state = next_entity.native_value
|
||||
assert len(state) <= 255
|
||||
@@ -473,19 +501,21 @@ async def test_household_rollups_sum_the_active_children(
|
||||
hass, make_entry
|
||||
) -> None:
|
||||
"""Household due/completed equal the sum of the per-child sensors;
|
||||
the cycle-day sensor carries the coordinator's answer."""
|
||||
import datetime as _dt
|
||||
|
||||
from custom_components.nestquest.dao_presence import (
|
||||
PresenceSchedulesDao,
|
||||
the cycle-day sensor carries the coordinator's answer (precomputed
|
||||
by the API, which resolves the schedule engine server-side)."""
|
||||
payload = _payload(
|
||||
[
|
||||
_child(
|
||||
ADA,
|
||||
"Ada",
|
||||
[_instance(7, ADA, state="completed", completed_at=f"{_today_iso()}T07:55:00+00:00", on_time=True)],
|
||||
),
|
||||
_child(BO, "Bo", [_instance(8, BO)]),
|
||||
_child(CORY, "Cory"),
|
||||
]
|
||||
)
|
||||
|
||||
entry, coordinator, (ada, bo, cory) = await _setup_seeded_entry(
|
||||
hass, make_entry
|
||||
)
|
||||
database = entry.runtime_data.database
|
||||
await _complete_first_instance(database, coordinator, ada)
|
||||
await coordinator.async_refresh()
|
||||
payload["cycle_day"] = 4
|
||||
entry, coordinator, _client = await _setup_entry(hass, make_entry, payload)
|
||||
|
||||
household_due = hass.entities["nestquest_household_quests_due_today"]
|
||||
household_completed = hass.entities[
|
||||
@@ -493,12 +523,12 @@ async def test_household_rollups_sum_the_active_children(
|
||||
]
|
||||
cycle_day_entity = hass.entities["nestquest_cycle_day"]
|
||||
per_child_due = [
|
||||
_sensor(hass, child.id, "quests_due_today").native_value
|
||||
for child in (ada, bo, cory)
|
||||
_sensor(hass, child_id, "quests_due_today").native_value
|
||||
for child_id in (ADA, BO, CORY)
|
||||
]
|
||||
per_child_completed = [
|
||||
_sensor(hass, child.id, "quests_completed_today").native_value
|
||||
for child in (ada, bo, cory)
|
||||
_sensor(hass, child_id, "quests_completed_today").native_value
|
||||
for child_id in (ADA, BO, CORY)
|
||||
]
|
||||
assert household_due.native_value == sum(per_child_due)
|
||||
assert household_completed.native_value == sum(per_child_completed)
|
||||
@@ -507,16 +537,5 @@ async def test_household_rollups_sum_the_active_children(
|
||||
"NestQuest household quests completed today"
|
||||
)
|
||||
assert cycle_day_entity.name == "NestQuest cycle day"
|
||||
assert cycle_day_entity.native_value == 0, "no schedule anywhere"
|
||||
|
||||
# A schedule on the FIRST active child drives cycle day.
|
||||
today = _dt.date.today()
|
||||
await PresenceSchedulesDao(database).upsert_by_child(
|
||||
ada.id, 2, (today - _dt.timedelta(days=3)).isoformat(),
|
||||
"0,1,2,3,4,5,6|",
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
assert cycle_day_entity.native_value == 4
|
||||
assert cycle_day_entity.extra_state_attributes["today"] == (
|
||||
today.isoformat()
|
||||
)
|
||||
assert cycle_day_entity.extra_state_attributes["today"] == _today_iso()
|
||||
|
||||
Reference in New Issue
Block a user