Extract the storage layer into core/ (bundled in the integration)
Extract the storage layer into core/ (bundled in the integration)
This commit was merged in pull request #142.
This commit is contained in:
@@ -829,6 +829,24 @@ def make_hass() -> tuple:
|
||||
return hass, registry
|
||||
|
||||
|
||||
def executor_for(hass):
|
||||
"""Return a dynamic executor callable backed by ``hass.async_add_executor_job``.
|
||||
|
||||
The wrapper resolves ``hass.async_add_executor_job`` on every call, so a
|
||||
test that swaps it after constructing the database (to gate or fail
|
||||
specific jobs) is observed without rebuilding the wrapper — mirroring
|
||||
the pre-extraction db.py, which read the attribute dynamically off
|
||||
``hass``. Use this for cancellation/gating tests that reassign
|
||||
``hass.async_add_executor_job``; plain call sites can pass
|
||||
``hass.async_add_executor_job`` directly.
|
||||
"""
|
||||
|
||||
async def _executor(fn, *args, **kwargs):
|
||||
return await hass.async_add_executor_job(fn, *args, **kwargs)
|
||||
|
||||
return _executor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def hass():
|
||||
"""Provide a standard-shaped hass fixture bound to the running test loop.
|
||||
|
||||
@@ -31,7 +31,7 @@ def _make_hass_mock():
|
||||
def _with_db(tmp_path, name):
|
||||
def _run_test(body):
|
||||
async def _main():
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / name)
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -33,7 +33,7 @@ def _make_hass_mock():
|
||||
def _with_db(tmp_path, name):
|
||||
def _run_test(body):
|
||||
async def _main():
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / name)
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -360,7 +360,7 @@ def test_create_child_concurrent_same_name_still_warns(tmp_path) -> None:
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "create-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -633,7 +633,7 @@ def test_edit_child_concurrent_opposite_edits_return_own_values(
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "edit-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -685,7 +685,7 @@ def test_set_child_active_concurrent_opposite_transitions_return_own(
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "active-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -61,7 +61,7 @@ D1 = _future_day(1)
|
||||
|
||||
|
||||
async def _prepare(path) -> tuple:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
children = ChildrenDao(database)
|
||||
@@ -630,7 +630,7 @@ def test_complete_instance_concurrent_second_call_is_noop(tmp_path) -> None:
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "complete-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -1060,7 +1060,7 @@ def test_uncomplete_instance_concurrent_second_call_is_noop(tmp_path) -> None:
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "uncomplete-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_flow_strings_match_translations(
|
||||
def test_no_hardcoded_domain_or_db_filename_in_new_modules() -> None:
|
||||
"""Ensure no module other than const.py hardcodes the domain or db filename."""
|
||||
pkg_dir = Path(config_flow.__file__).parent
|
||||
py_files = [f for f in pkg_dir.glob("*.py") if f.name not in ("const.py",)]
|
||||
py_files = [f for f in pkg_dir.rglob("*.py") if f.name not in ("const.py",)]
|
||||
|
||||
assert len(py_files) > 0, "No python modules found in package to check"
|
||||
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ def test_window_clock_ranges_cover_every_window() -> None:
|
||||
def test_no_hardcoded_domain_or_db_filename_in_modules() -> None:
|
||||
"""Ensure no module other than const.py hardcodes DOMAIN string or SQLITE_DB_FILENAME."""
|
||||
pkg_dir = Path(const.__file__).parent
|
||||
py_files = [f for f in pkg_dir.glob("*.py") if f.name != "const.py"]
|
||||
py_files = [f for f in pkg_dir.rglob("*.py") if f.name != "const.py"]
|
||||
|
||||
assert len(py_files) > 0, "No python modules found in package to check"
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""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
|
||||
storage-layer extraction (task d0b691d8) 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)
|
||||
)
|
||||
@@ -30,7 +30,7 @@ def _make_hass_mock():
|
||||
|
||||
|
||||
def _open_db(path) -> NestQuestDatabase:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
_run(database.open(path))
|
||||
_run(apply_migrations(database))
|
||||
return database
|
||||
@@ -319,7 +319,7 @@ def test_reorder_concurrent_calls_serialize(tmp_path) -> None:
|
||||
RuntimeError.
|
||||
"""
|
||||
async def _main() -> None:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / "reorder-concurrent.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -377,7 +377,7 @@ def test_admin_add_concurrent_calls_are_idempotent(tmp_path) -> None:
|
||||
'see no row' and one of them raise IntegrityError.
|
||||
"""
|
||||
async def _main() -> None:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / "admin-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -487,17 +487,17 @@ def test_children_and_admin_sql_lives_only_in_dao_module() -> None:
|
||||
scan_roots = [package, repo_root / "tests"]
|
||||
|
||||
allowed = {
|
||||
"custom_components/nestquest/dao_children.py", # the DAO itself
|
||||
"custom_components/nestquest/dao_rules.py", # validates child
|
||||
"custom_components/nestquest/core/dao_children.py", # the DAO itself
|
||||
"custom_components/nestquest/core/dao_rules.py", # validates child
|
||||
# activeness on assignment (FK to children, done-condition
|
||||
# 'validates that the assigned child is active')
|
||||
"custom_components/nestquest/dao_presence.py", # validates the
|
||||
"custom_components/nestquest/core/dao_presence.py", # validates the
|
||||
# schedule/override child exists before writing (same FK shape
|
||||
# as dao_rules: one existence SELECT per write)
|
||||
"custom_components/nestquest/dao_instances.py", # validates the
|
||||
"custom_components/nestquest/core/dao_instances.py", # validates the
|
||||
# instance/event child exists before writing (same FK shape)
|
||||
"custom_components/nestquest/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/migrations.py", # applies the DDL
|
||||
"custom_components/nestquest/core/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/core/migrations.py", # applies the DDL
|
||||
"tests/test_schema.py", # tests the DDL
|
||||
"tests/test_migrations.py", # tests migration application
|
||||
"tests/test_dao_children.py", # this file, scanned separately
|
||||
|
||||
@@ -71,7 +71,7 @@ D20 = _future_day(20)
|
||||
|
||||
|
||||
async def _prepare(path) -> tuple:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
children = ChildrenDao(database)
|
||||
@@ -352,7 +352,7 @@ def test_upsert_no_past_check_uses_execution_date_not_call_date(
|
||||
|
||||
monkeypatch = pytest.MonkeyPatch()
|
||||
monkeypatch.setattr(
|
||||
"custom_components.nestquest.dao_instances.datetime",
|
||||
"custom_components.nestquest.core.dao_instances.datetime",
|
||||
_ShiftingModule(),
|
||||
)
|
||||
try:
|
||||
@@ -362,7 +362,7 @@ def test_upsert_no_past_check_uses_execution_date_not_call_date(
|
||||
# the lock, the IN-LOCK re-check sees tomorrow and must
|
||||
# reject the insert — the stale call-date pre-check alone
|
||||
# would have let it through.
|
||||
import custom_components.nestquest.dao_instances as mod
|
||||
import custom_components.nestquest.core.dao_instances as mod
|
||||
|
||||
async def _lock_holder():
|
||||
# Hold the CONNECTION lock across a transaction until
|
||||
@@ -402,7 +402,7 @@ def test_upsert_no_past_check_uses_execution_date_not_call_date(
|
||||
return None
|
||||
|
||||
async def _main():
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / "upsert-rollover.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -1030,8 +1030,8 @@ def test_no_mutation_sql_for_completion_events_anywhere() -> None:
|
||||
# INSERT statements are stripped before scanning so only genuine
|
||||
# UPDATE/DELETE/non-DAO-INSERT paths hit the pattern.
|
||||
insert_exempt = {
|
||||
"custom_components/nestquest/dao_instances.py",
|
||||
"custom_components/nestquest/migrations.py", # the rebuild
|
||||
"custom_components/nestquest/core/dao_instances.py",
|
||||
"custom_components/nestquest/core/migrations.py", # the rebuild
|
||||
# migrations re-create the table via INSERT..SELECT (append-
|
||||
# order preserved); UPDATE/DELETE stays forbidden everywhere
|
||||
"tests/test_schema.py",
|
||||
@@ -1141,7 +1141,7 @@ def test_completion_events_dao_instance_has_no_mutation_capability(
|
||||
def test_append_concurrent_events_serialize(tmp_path) -> None:
|
||||
"""Concurrent appends for the same instance all land, in order."""
|
||||
async def _main():
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / "append-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -118,7 +118,7 @@ class World:
|
||||
|
||||
|
||||
async def _world(path) -> World:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
world = World(database)
|
||||
@@ -441,7 +441,7 @@ def test_completion_events_absolutely_no_mutation_path() -> None:
|
||||
)
|
||||
offenders = [
|
||||
py.name
|
||||
for py in sorted(package.glob("*.py"))
|
||||
for py in sorted(package.rglob("*.py"))
|
||||
if py.name != "__pycache__" and mutation.search(py.read_text())
|
||||
]
|
||||
assert offenders == []
|
||||
@@ -33,7 +33,7 @@ NOW = "2026-09-14T12:00:00+00:00"
|
||||
|
||||
|
||||
async def _prepare(path) -> tuple:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
schedules = PresenceSchedulesDao(database)
|
||||
@@ -196,7 +196,7 @@ def test_upsert_concurrent_same_child_serializes(tmp_path) -> None:
|
||||
return fn(*args)
|
||||
|
||||
hass.async_add_executor_job = _gated_executor
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "upsert-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -553,9 +553,9 @@ def test_presence_sql_lives_only_in_dao_module() -> None:
|
||||
scan_roots = [package, repo_root / "tests"]
|
||||
|
||||
allowed = {
|
||||
"custom_components/nestquest/dao_presence.py", # this DAO
|
||||
"custom_components/nestquest/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/migrations.py", # applies the DDL
|
||||
"custom_components/nestquest/core/dao_presence.py", # this DAO
|
||||
"custom_components/nestquest/core/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/core/migrations.py", # applies the DDL
|
||||
"tests/test_schema.py", # tests the DDL
|
||||
"tests/test_migrations.py", # tests migration application
|
||||
"tests/test_dao_presence.py", # this file, scanned separately
|
||||
@@ -766,7 +766,7 @@ def test_override_create_racing_conflicts_serialize(tmp_path) -> None:
|
||||
return fn(*args)
|
||||
|
||||
hass.async_add_executor_job = _gated_executor
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "override-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -42,7 +42,7 @@ NOW = "2026-09-14T12:00:00+00:00"
|
||||
|
||||
|
||||
async def _prepare(path) -> tuple:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
rules = ScheduleRulesDao(database)
|
||||
@@ -325,7 +325,7 @@ def test_rule_delete_concurrent_with_definition_create_is_safe(
|
||||
create's validation pauses mid-flight before the delete starts.
|
||||
"""
|
||||
async def _main(order: str):
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_path / f"rule-delete-race-{order}.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
@@ -1218,12 +1218,12 @@ def test_rules_and_definitions_sql_lives_only_in_dao_module() -> None:
|
||||
scan_roots = [package, repo_root / "tests"]
|
||||
|
||||
allowed = {
|
||||
"custom_components/nestquest/dao_rules.py", # this DAO
|
||||
"custom_components/nestquest/dao_instances.py", # validates the
|
||||
"custom_components/nestquest/core/dao_rules.py", # this DAO
|
||||
"custom_components/nestquest/core/dao_instances.py", # validates the
|
||||
# definition exists + fetches its assignee before generating an
|
||||
# instance (the instance must snapshot the definition's child)
|
||||
"custom_components/nestquest/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/migrations.py", # applies the DDL
|
||||
"custom_components/nestquest/core/schema.py", # declares the DDL
|
||||
"custom_components/nestquest/core/migrations.py", # applies the DDL
|
||||
"tests/test_schema.py", # tests the DDL
|
||||
"tests/test_migrations.py", # tests migration application
|
||||
"tests/test_dao_rules.py", # this file, scanned separately
|
||||
|
||||
+28
-26
@@ -11,9 +11,9 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import make_hass
|
||||
from conftest import executor_for, make_hass
|
||||
|
||||
from custom_components.nestquest import db as db_module
|
||||
import custom_components.nestquest.core.db as db_module
|
||||
from custom_components.nestquest.db import NestQuestDatabase
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def _make_hass_mock():
|
||||
|
||||
def _open_db(path) -> tuple[NestQuestDatabase, MagicMock]:
|
||||
hass = _make_hass_mock()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
_run(database.open(path))
|
||||
return database, hass
|
||||
|
||||
@@ -55,7 +55,7 @@ def test_open_enables_foreign_key_enforcement(tmp_path) -> None:
|
||||
|
||||
def test_open_delegates_connection_to_executor(tmp_path) -> None:
|
||||
hass = _make_hass_mock()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
_run(database.open(tmp_path / "exec.db"))
|
||||
first_call = hass.async_add_executor_job.call_args_list[0]
|
||||
assert callable(first_call.args[0])
|
||||
@@ -65,7 +65,7 @@ def test_open_delegates_connection_to_executor(tmp_path) -> None:
|
||||
def test_open_twice_returns_same_wrapper_without_second_connect(tmp_path) -> None:
|
||||
path = tmp_path / "once.db"
|
||||
hass = _make_hass_mock()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
first = _run(database.open(path))
|
||||
second = _run(database.open(path))
|
||||
assert first is database
|
||||
@@ -111,7 +111,7 @@ def test_every_method_routes_sqlite3_through_executor(tmp_path) -> None:
|
||||
async def test_every_wrapper_call_awaits_executor_job(tmp_path) -> None:
|
||||
"""The executor surface is awaited per wrapper call on a live loop."""
|
||||
hass = _make_hass_mock()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "loop.db")
|
||||
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
@@ -129,7 +129,7 @@ async def test_every_wrapper_call_awaits_executor_job(tmp_path) -> None:
|
||||
async def test_public_methods_return_only_plain_data(tmp_path) -> None:
|
||||
"""execute/execute_many return plain dataclasses, never sqlite3 objects."""
|
||||
hass = _make_hass_mock()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "plain.db")
|
||||
result = await database.execute(
|
||||
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)"
|
||||
@@ -156,7 +156,7 @@ async def test_public_methods_return_only_plain_data(tmp_path) -> None:
|
||||
async def test_no_sqlite3_call_on_event_loop_with_real_executor(tmp_path) -> None:
|
||||
"""With the conftest hass (real run_in_executor), queries still succeed."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "real.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
assert await database.fetch_one("SELECT 1") == (1,)
|
||||
@@ -210,7 +210,7 @@ async def test_real_executor_never_touches_loop_thread(tmp_path, monkeypatch) ->
|
||||
monkeypatch.setattr(db_module.sqlite3, "connect", _traced_connect)
|
||||
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "threads.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
await database.execute("INSERT INTO t (id) VALUES (?)", (1,))
|
||||
@@ -250,7 +250,9 @@ def test_db_sqlite3_calls_only_inside_executor_delegated_functions() -> None:
|
||||
"""Every sqlite3.* call in db.py sits inside an executor-delegating function.
|
||||
|
||||
A function is executor-delegating when it (or, for nested closures, any of
|
||||
its enclosing functions) passes work to ``hass.async_add_executor_job``.
|
||||
its enclosing functions) passes work to the wrapper's ``self._executor``
|
||||
callable (the Home-Assistant-free contract that mirrors
|
||||
``hass.async_add_executor_job``).
|
||||
"""
|
||||
source = Path(db_module.__file__).read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(db_module.__file__))
|
||||
@@ -262,7 +264,7 @@ def test_db_sqlite3_calls_only_inside_executor_delegated_functions() -> None:
|
||||
and any(
|
||||
isinstance(sub, ast.Call)
|
||||
and isinstance(sub.func, ast.Attribute)
|
||||
and sub.func.attr == "async_add_executor_job"
|
||||
and sub.func.attr == "_executor"
|
||||
for sub in ast.walk(node)
|
||||
)
|
||||
}
|
||||
@@ -375,7 +377,7 @@ async def test_commit_failure_rolls_back_and_next_transaction_works(
|
||||
must run cleanly.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "deferred.db")
|
||||
await database.execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
|
||||
await database.execute(
|
||||
@@ -416,7 +418,7 @@ async def test_cancel_during_open_closes_abandoned_connection(tmp_path) -> None:
|
||||
propagates, and a follow-up open() must succeed cleanly.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(executor_for(hass))
|
||||
|
||||
open_started = asyncio.Event()
|
||||
release_open = asyncio.Event()
|
||||
@@ -481,7 +483,7 @@ async def test_cancel_during_begin_settles_then_rolls_back(tmp_path) -> None:
|
||||
clear its state, and stay usable for the next transaction.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(executor_for(hass))
|
||||
await database.open(tmp_path / "cancel-begin.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
await database.execute("INSERT INTO t (name) VALUES (?)", ("before",))
|
||||
@@ -543,7 +545,7 @@ async def test_cancel_during_commit_settles_then_propagates_cancellation(
|
||||
transaction, and the wrapper must stay usable.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(executor_for(hass))
|
||||
await database.open(tmp_path / "cancel-commit.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
|
||||
@@ -603,7 +605,7 @@ async def test_cancel_during_in_transaction_execute_settles_then_rolls_back(
|
||||
not persist.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(executor_for(hass))
|
||||
await database.open(tmp_path / "cancel-execute.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
await database.execute("INSERT INTO t (name) VALUES (?)", ("before",))
|
||||
@@ -666,7 +668,7 @@ async def test_commit_and_rollback_failures_are_both_preserved(tmp_path) -> None
|
||||
commit failure behind the rollback failure.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(executor_for(hass))
|
||||
await database.open(tmp_path / "both-fail.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||||
|
||||
@@ -698,14 +700,14 @@ async def test_commit_and_rollback_failures_are_both_preserved(tmp_path) -> None
|
||||
async def test_transaction_persists_across_connections(tmp_path) -> None:
|
||||
"""A committed transaction is visible to a brand-new connection."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "persist.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
async with database.transaction():
|
||||
await database.execute("INSERT INTO t (name) VALUES (?)", ("kept",))
|
||||
await database.close()
|
||||
|
||||
other = NestQuestDatabase(hass)
|
||||
other = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await other.open(tmp_path / "persist.db")
|
||||
assert await other.fetch_all("SELECT name FROM t") == [("kept",)]
|
||||
await other.close()
|
||||
@@ -726,7 +728,7 @@ async def test_concurrent_writers_do_not_interleave_commits(tmp_path) -> None:
|
||||
while a transaction is open queue behind it on the wrapper lock.
|
||||
"""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "serial.db")
|
||||
await database.execute("CREATE TABLE t (kind TEXT, seq INTEGER)")
|
||||
tx_rounds, writers, writer_rounds = 6, 5, 8
|
||||
@@ -766,7 +768,7 @@ async def test_concurrent_writers_do_not_interleave_commits(tmp_path) -> None:
|
||||
async def test_transaction_span_excludes_concurrent_transaction(tmp_path) -> None:
|
||||
"""A second transaction caller is rejected with a clear error."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "reject.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER)")
|
||||
|
||||
@@ -791,7 +793,7 @@ async def test_transaction_span_excludes_concurrent_transaction(tmp_path) -> Non
|
||||
async def test_transaction_blocks_other_statements_until_commit(tmp_path) -> None:
|
||||
"""A statement issued during an open transaction queues behind it."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "block.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
|
||||
@@ -830,7 +832,7 @@ async def test_concurrent_transaction_is_rejected_with_clear_error(
|
||||
) -> None:
|
||||
"""A second concurrent transaction() gets a clear RuntimeError."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "concurrent-tx.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER)")
|
||||
|
||||
@@ -853,7 +855,7 @@ async def test_concurrent_transaction_is_rejected_with_clear_error(
|
||||
async def test_close_waits_for_in_flight_transaction(tmp_path) -> None:
|
||||
"""close() serializes: an open transaction finishes before shutdown."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "closerace.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
|
||||
@@ -875,7 +877,7 @@ async def test_close_waits_for_in_flight_transaction(tmp_path) -> None:
|
||||
await asyncio.gather(txn_task, close_task)
|
||||
assert database.connected is False
|
||||
|
||||
witness = NestQuestDatabase(hass)
|
||||
witness = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await witness.open(tmp_path / "closerace.db")
|
||||
assert await witness.fetch_all("SELECT name FROM t") == [("kept",)]
|
||||
await witness.close()
|
||||
@@ -886,7 +888,7 @@ async def test_close_during_transaction_from_same_task_is_rejected(
|
||||
) -> None:
|
||||
"""close() inside its own transaction body raises instead of deadlocking."""
|
||||
hass, _registry = make_hass()
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "selfclose.db")
|
||||
await database.execute("CREATE TABLE t (id INTEGER)")
|
||||
with pytest.raises(RuntimeError, match="transaction"):
|
||||
|
||||
@@ -472,7 +472,7 @@ async def test_setup_backfills_instances_over_horizon(hass, make_entry) -> None:
|
||||
# Seed the database BEFORE setup: a restart reopens an existing file that
|
||||
# already holds definitions. The startup backfill must pick them up.
|
||||
db_path = await async_get_db_path(hass)
|
||||
pre = NestQuestDatabase(hass)
|
||||
pre = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await pre.open(db_path)
|
||||
await apply_migrations(pre)
|
||||
child_id = await _seed_daily_child_and_definition(pre, _local_today(hass))
|
||||
@@ -499,7 +499,7 @@ async def test_setup_backfills_configured_horizon_days(hass, make_entry) -> None
|
||||
from custom_components.nestquest.store import async_get_db_path
|
||||
|
||||
db_path = await async_get_db_path(hass)
|
||||
pre = NestQuestDatabase(hass)
|
||||
pre = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await pre.open(db_path)
|
||||
await apply_migrations(pre)
|
||||
child_id = await _seed_daily_child_and_definition(pre, _local_today(hass))
|
||||
|
||||
@@ -51,7 +51,7 @@ NOW = "2026-09-14T12:00:00+00:00"
|
||||
|
||||
|
||||
async def _prepare(path):
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
return database
|
||||
@@ -1039,7 +1039,7 @@ def test_regenerate_for_child_uses_single_anchor_across_midnight(
|
||||
clock["reads"] += 1
|
||||
return day0 if clock["reads"] == 1 else day1
|
||||
|
||||
import custom_components.nestquest.dao_instances as dao_instances_module
|
||||
import custom_components.nestquest.core.dao_instances as dao_instances_module
|
||||
|
||||
monkeypatch.setattr(dao_instances_module, "_today", _fake_today)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def _make_hass_mock():
|
||||
|
||||
|
||||
def _open_db(path) -> NestQuestDatabase:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
_run(database.open(path))
|
||||
return database
|
||||
|
||||
@@ -707,7 +707,7 @@ def test_noop_run_logs_no_migration(caplog, tmp_path) -> None:
|
||||
try:
|
||||
_migrate(database)
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.INFO, logger="custom_components.nestquest.migrations"):
|
||||
with caplog.at_level(logging.INFO, logger="custom_components.nestquest.core.migrations"):
|
||||
_migrate(database)
|
||||
migrated = [
|
||||
r for r in caplog.records if "migrated" in r.getMessage().lower()
|
||||
@@ -722,7 +722,7 @@ def test_noop_run_is_logged_at_debug_level(caplog, tmp_path) -> None:
|
||||
try:
|
||||
_migrate(database)
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.DEBUG, logger="custom_components.nestquest.migrations"):
|
||||
with caplog.at_level(logging.DEBUG, logger="custom_components.nestquest.core.migrations"):
|
||||
_migrate(database)
|
||||
assert any(
|
||||
f"already at version {len(MIGRATIONS)}" in r.getMessage()
|
||||
@@ -983,7 +983,7 @@ def test_concurrent_runners_serialize_and_apply_once(tmp_path) -> None:
|
||||
results: dict[str, int] = {}
|
||||
|
||||
async def _main(tmp_name: str) -> None:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(tmp_name)
|
||||
try:
|
||||
# Start A, yield so it acquires the lock and enters its
|
||||
|
||||
@@ -113,7 +113,7 @@ def _open_allowlist_db(tmp_path, name):
|
||||
|
||||
hass = MagicMock()
|
||||
hass.async_add_executor_job = AsyncMock(side_effect=(lambda fn, *a: fn(*a)))
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
_run(database.open(tmp_path / name))
|
||||
_run(apply_migrations(database))
|
||||
return database
|
||||
|
||||
@@ -50,7 +50,7 @@ NOW = "2026-09-14T12:00:00+00:00"
|
||||
|
||||
|
||||
async def _prepare(path) -> tuple:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
children = ChildrenDao(database)
|
||||
@@ -1396,7 +1396,7 @@ def test_set_quest_definition_active_concurrent_opposite_transitions_return_own(
|
||||
gate_open = asyncio.Event()
|
||||
a_started = asyncio.Event()
|
||||
hass = _gated_hass(armed, gate_open, a_started)
|
||||
database = NestQuestDatabase(hass)
|
||||
database = NestQuestDatabase(hass.async_add_executor_job)
|
||||
await database.open(tmp_path / "active-race.db")
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
@@ -276,7 +276,7 @@ def test_recurrence_module_imports_only_stdlib() -> None:
|
||||
from pathlib import Path
|
||||
|
||||
source = Path(
|
||||
__import__("custom_components.nestquest.recurrence",
|
||||
__import__("custom_components.nestquest.core.recurrence",
|
||||
fromlist=["__file__"]).__file__
|
||||
).read_text()
|
||||
offenders = _scan_forbidden_imports(source)
|
||||
|
||||
@@ -9,10 +9,10 @@ from custom_components.nestquest.recurrence import (
|
||||
RuleType,
|
||||
RuleValidationError,
|
||||
ScheduleRule,
|
||||
_month_end,
|
||||
occurs_on,
|
||||
occurrences_between,
|
||||
)
|
||||
from custom_components.nestquest.core.recurrence import _month_end
|
||||
|
||||
|
||||
def _d(date: str) -> datetime.date:
|
||||
|
||||
@@ -9,10 +9,10 @@ from custom_components.nestquest.recurrence import (
|
||||
RuleType,
|
||||
RuleValidationError,
|
||||
ScheduleRule,
|
||||
_nth_weekday_of_month,
|
||||
occurs_on,
|
||||
occurrences_between,
|
||||
)
|
||||
from custom_components.nestquest.core.recurrence import _nth_weekday_of_month
|
||||
|
||||
|
||||
def _d(date: str) -> datetime.date:
|
||||
|
||||
@@ -37,7 +37,7 @@ def _make_hass_mock():
|
||||
|
||||
|
||||
def _open_db(path) -> NestQuestDatabase:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
_run(database.open(path))
|
||||
return database
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ def _make_hass_mock():
|
||||
|
||||
|
||||
def _open_db(path) -> NestQuestDatabase:
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
_run(database.open(path))
|
||||
return database
|
||||
|
||||
@@ -44,7 +44,7 @@ def _open_db(path) -> NestQuestDatabase:
|
||||
async def _avalid_database(path) -> None:
|
||||
"""Create a valid, migrated NestQuest database file (async, on the
|
||||
running loop, for use inside pytest-asyncio tests)."""
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
try:
|
||||
await apply_migrations(database)
|
||||
|
||||
+30
-7
@@ -23,7 +23,11 @@ def _make_hass(config_dir: Path, execute: bool = True):
|
||||
return hass
|
||||
|
||||
|
||||
def test_returns_absolute_path_from_config_path() -> None:
|
||||
def test_returns_absolute_path_from_config_path(monkeypatch) -> None:
|
||||
# Path resolution only: the parent-dir mkdir is a side effect of
|
||||
# core.store and is exercised elsewhere; suppress it here so the
|
||||
# absolute-path assertion can run against /config (non-writable).
|
||||
monkeypatch.setattr("custom_components.nestquest.core.store._ensure_dir", lambda path: None)
|
||||
hass = _make_hass(Path("/config"), execute=False)
|
||||
db_path = _run(store.async_get_db_path(hass))
|
||||
hass.config.path.assert_called_once_with(SQLITE_DB_FILENAME)
|
||||
@@ -75,18 +79,36 @@ def test_existing_directory_does_not_fail(tmp_path) -> None:
|
||||
assert not db_path.exists()
|
||||
|
||||
|
||||
def test_mkdir_runs_via_executor(tmp_path) -> None:
|
||||
def test_mkdir_runs_off_event_loop(tmp_path, monkeypatch) -> None:
|
||||
"""The parent-directory mkdir runs off the event loop via asyncio.to_thread.
|
||||
|
||||
core.store is Home-Assistant-free, so it schedules the blocking
|
||||
``mkdir`` through :func:`asyncio.to_thread` rather than
|
||||
``hass.async_add_executor_job``; the integration shim delegates to it
|
||||
unchanged. The intent is preserved: the mkdir still runs off the
|
||||
event loop, with ``_ensure_dir`` on the parent directory, and the
|
||||
directory exists afterwards.
|
||||
"""
|
||||
nested = tmp_path / "deep" / "nested"
|
||||
hass = _make_hass(nested)
|
||||
to_thread_calls: list = []
|
||||
real_to_thread = asyncio.to_thread
|
||||
|
||||
def _trace_to_thread(fn, *args, **kwargs):
|
||||
to_thread_calls.append((fn, args, kwargs))
|
||||
return real_to_thread(fn, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(asyncio, "to_thread", _trace_to_thread)
|
||||
db_path = _run(store.async_get_db_path(hass))
|
||||
assert nested.is_dir()
|
||||
hass.async_add_executor_job.assert_called_once()
|
||||
args, kwargs = hass.async_add_executor_job.call_args
|
||||
fn, fn_args = args[0], args[1:]
|
||||
assert len(to_thread_calls) == 1, (
|
||||
f"expected one asyncio.to_thread call, got {len(to_thread_calls)}"
|
||||
)
|
||||
fn, fn_args, fn_kwargs = to_thread_calls[0]
|
||||
assert callable(fn)
|
||||
assert fn is store._ensure_dir
|
||||
assert fn_args == (nested,)
|
||||
assert kwargs == {}
|
||||
assert fn_kwargs == {}
|
||||
assert db_path.parent == nested
|
||||
assert db_path.parent.is_dir()
|
||||
assert not db_path.exists()
|
||||
@@ -180,7 +202,8 @@ def test_no_file_creation_calls_in_store() -> None:
|
||||
assert "open(" not in source
|
||||
|
||||
|
||||
def test_container_style_path_resolves() -> None:
|
||||
def test_container_style_path_resolves(monkeypatch) -> None:
|
||||
monkeypatch.setattr("custom_components.nestquest.core.store._ensure_dir", lambda path: None)
|
||||
hass = _make_hass(Path("/config"), execute=False)
|
||||
assert _run(store.async_get_db_path(hass)) == Path("/config") / SQLITE_DB_FILENAME
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def _make_hass_mock():
|
||||
|
||||
|
||||
async def _prepare(path):
|
||||
database = NestQuestDatabase(_make_hass_mock())
|
||||
database = NestQuestDatabase(_make_hass_mock().async_add_executor_job)
|
||||
await database.open(path)
|
||||
await apply_migrations(database)
|
||||
return database
|
||||
@@ -313,7 +313,7 @@ def test_definition_business_regeneration_uses_pinned_today_not_host(
|
||||
business operations must regenerate against the pinned date and keep
|
||||
that first local day, proving the threaded ``today`` wins.
|
||||
"""
|
||||
import custom_components.nestquest.dao_instances as dao_instances_module
|
||||
import custom_components.nestquest.core.dao_instances as dao_instances_module
|
||||
|
||||
pinned = datetime.date(2026, 1, 14)
|
||||
host_today = datetime.date(2026, 1, 15)
|
||||
|
||||
Reference in New Issue
Block a user