P2.1: Point test_presence.py ISO-week scan at core.presence (the real model since task 510c1f78); the integration's presence.py is now a re-export shim, so scanning it was vacuous. P2.2: Add direct core-builder unit tests to test_events.py — exact payload key sets for completed/uncompleted/day-complete events, was_on_time threading (True/False/None), event ordering (completed fires before day-complete on the bus), and the four day-complete outcomes (zero-quest day, non-today instance, partial day, fully-cleared today). Add a shim timezone test that sets hass.config.time_zone to a zone whose local date differs from UTC and asserts the day-complete rule uses that local date. P3.1: Split build_quest_completed_events into build_quest_completed_event (single event, no day-complete state) and build_child_day_complete_event (separate, returns the day-complete event or None). The shim fires nestquest_quest_completed BEFORE it reads the HA-local today and evaluates the day-complete rule, restoring the pre-extraction ordering so a failure in the day-complete DB reads or the ZoneInfo lookup cannot suppress the completed event. Payloads and the day-complete rule are identical. P3.2: test_startup_db.py glob -> rglob so core/ is scanned by the 'never unlinks or truncates DB files' guardrail (non-recursive glob skipped the extracted core package). P3.3: RELEASE_NOTES.md notes the children and admin_allowlist loggers moved to .core.children / .core.admin_allowlist (parent logger still catches them). P3.4: Refresh the stale test_core_isolation.py docstring to cover the whole core package (storage-layer task d0b691d8 + engines/business- layer task 510c1f78). uv run pytest -q = 1507 passed (baseline 1497 + 10 new).
121 lines
4.7 KiB
Python
121 lines
4.7 KiB
Python
"""Invariant: the bundled core package is self-contained.
|
|
|
|
Every module under ``custom_components/nestquest/core/`` must be free of
|
|
Home Assistant and must not climb out of the core package: no
|
|
``homeassistant`` import, no relative import with level > 1, no absolute
|
|
import of ``custom_components.nestquest.<something other than core>``,
|
|
and no bare ``import core`` / ``from core``. This guards the whole
|
|
core package — the storage-layer extraction (task d0b691d8) and the
|
|
engines/business-layer extraction (task 510c1f78) — from regressing
|
|
into a HA-coupled or integration-coupled bundle.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
CORE_PKG = Path(
|
|
__import__(
|
|
"custom_components.nestquest.core", fromlist=["__file__"]
|
|
).__file__
|
|
).parent
|
|
|
|
|
|
def _module_chain(base: str, name: str) -> str:
|
|
"""Combined dotted path for ``from <base> import <name>``."""
|
|
return f"{base}.{name}" if base else name
|
|
|
|
|
|
def _is_homeassistant(module: str) -> bool:
|
|
return module == "homeassistant" or module.startswith("homeassistant.")
|
|
|
|
|
|
def _climbs_out_of_core(module: str) -> bool:
|
|
"""An absolute path that reaches the integration package but not core."""
|
|
if module == "custom_components.nestquest":
|
|
return True
|
|
if module.startswith("custom_components.nestquest."):
|
|
rest = module[len("custom_components.nestquest."):]
|
|
head = rest.split(".", 1)[0]
|
|
return head != "core"
|
|
return False
|
|
|
|
|
|
def _is_bare_core(module: str) -> bool:
|
|
"""A bare ``core`` / ``core.*`` absolute reference (not the bundle)."""
|
|
return module == "core" or module.startswith("core.")
|
|
|
|
|
|
def _scan(path: Path) -> list[str]:
|
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
|
offenders: list[str] = []
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
mod = alias.name
|
|
if _is_homeassistant(mod):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} homeassistant import: import {mod}"
|
|
)
|
|
if _is_bare_core(mod):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} bare core import: import {mod}"
|
|
)
|
|
if _climbs_out_of_core(mod):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} climbs out of core: import {mod}"
|
|
)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.level and node.level > 1:
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} relative import climbs out "
|
|
f"of core (level {node.level})"
|
|
)
|
|
continue
|
|
if node.level:
|
|
# relative level 1 stays inside core; nothing to check.
|
|
continue
|
|
base = node.module or ""
|
|
if _is_homeassistant(base):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} homeassistant import: from {base}"
|
|
)
|
|
if _is_bare_core(base):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} bare core import: from {base}"
|
|
)
|
|
if _climbs_out_of_core(base):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} climbs out of core: from {base}"
|
|
)
|
|
for alias in node.names:
|
|
combined = _module_chain(base, alias.name)
|
|
if _is_homeassistant(combined):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} homeassistant import: "
|
|
f"from {base} import {alias.name}"
|
|
)
|
|
if _is_bare_core(combined):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} bare core import: "
|
|
f"from {base} import {alias.name}"
|
|
)
|
|
if _climbs_out_of_core(combined):
|
|
offenders.append(
|
|
f"{path.name}:{node.lineno} climbs out of core: "
|
|
f"from {base} import {alias.name}"
|
|
)
|
|
return offenders
|
|
|
|
|
|
def test_core_package_is_self_contained() -> None:
|
|
"""No core module imports homeassistant or climbs out of the core package."""
|
|
py_files = sorted(CORE_PKG.rglob("*.py"))
|
|
assert py_files, "core package not found under custom_components/nestquest/core"
|
|
all_offenders: list[str] = []
|
|
for py_file in py_files:
|
|
all_offenders.extend(_scan(py_file))
|
|
assert all_offenders == [], (
|
|
"core package isolation violated:\n" + "\n".join(all_offenders)
|
|
)
|