Replace HA config reads with an explicit settings object.
Done-condition
core/settings.py exposes a frozen, Home-Assistant-free NestQuestSettings dataclass carrying horizon_days, day_rollover_time (strict HH:MM), notify_target, the three notification times, and the four enable toggles.
NestQuestSettings.from_options(mapping) reads the CONF_* keys with the matching defaults and validates (horizon int >= 1, strict HH:MM, real booleans); horizon_window(today) returns the rolling [today, today+horizon_days] window.
The integration builds the settings object from entry.options and passes it through; core never reads HA config.
AST test asserts no module under custom_components/nestquest/core/ imports homeassistant, accesses hass.config / a config-entry attribute, or reads config-entry options (docstring mentions allowed).
A test constructs settings with NON-default horizon_days=3 and day_rollover_time=03:30 and drives materialization over that horizon, asserting the resulting instance window matches.
A from_options test: defaults applied when keys absent; invalid horizon/rollover rejected.
uv run pytest -q green (1575).
Invariants confirmed
No homeassistant import under core/.
No direct .core import at the integration top level (goes through the settings.py shim, matching the existing shim convention).
No repo-root core/.
Behaviour intentionally improved (review round 3)
A malformed day_rollover_time (and the other notification times / horizon_days) used to raise ValueError out of async_setup_entry, failing setup. Setup now builds settings via from_options_resilient, which falls each malformed field back to its default with a warning, so a single bad sibling option cannot reset the user's horizon_days or day_rollover_time. Two LIFECYCLE tests wire an entry via make_entry(options=...) and call async_setup_entry to prove this end-to-end (a fully malformed options dict succeeds with the default rollover time and default horizon; a bad sibling keeps the configured horizon).
1575 passed (was 1507; +39 new in tests/test_core_settings.py, +2 lifecycle tests for the malformed-option fallback in review round 3).
## Task 4dd8f870
Replace HA config reads with an explicit settings object.
### Done-condition
- `core/settings.py` exposes a frozen, Home-Assistant-free `NestQuestSettings` dataclass carrying `horizon_days`, `day_rollover_time` (strict HH:MM), `notify_target`, the three notification times, and the four enable toggles.
- `NestQuestSettings.from_options(mapping)` reads the `CONF_*` keys with the matching defaults and validates (horizon int >= 1, strict HH:MM, real booleans); `horizon_window(today)` returns the rolling `[today, today+horizon_days]` window.
- The integration builds the settings object from `entry.options` and passes it through; core never reads HA config.
- AST test asserts no module under `custom_components/nestquest/core/` imports `homeassistant`, accesses `hass.config` / a config-entry attribute, or reads config-entry options (docstring mentions allowed).
- A test constructs settings with NON-default `horizon_days=3` and `day_rollover_time=03:30` and drives materialization over that horizon, asserting the resulting instance window matches.
- A `from_options` test: defaults applied when keys absent; invalid horizon/rollover rejected.
- `uv run pytest -q` green (1575).
### Invariants confirmed
- No `homeassistant` import under `core/`.
- No direct `.core` import at the integration top level (goes through the `settings.py` shim, matching the existing shim convention).
- No repo-root `core/`.
### Behaviour intentionally improved (review round 3)
A malformed `day_rollover_time` (and the other notification times / `horizon_days`) used to raise `ValueError` out of `async_setup_entry`, failing setup. Setup now builds settings via `from_options_resilient`, which falls each malformed field back to its default with a warning, so a single bad sibling option cannot reset the user's `horizon_days` or `day_rollover_time`. Two LIFECYCLE tests wire an entry via `make_entry(options=...)` and call `async_setup_entry` to prove this end-to-end (a fully malformed options dict succeeds with the default rollover time and default horizon; a bad sibling keeps the configured horizon).
### SHA
d15a02f
### Test count
1575 passed (was 1507; +39 new in tests/test_core_settings.py, +2 lifecycle tests for the malformed-option fallback in review round 3).
Add core/settings.py: a frozen, HA-free NestQuestSettings dataclass
carrying horizon_days, day_rollover_time, notify_target, the three
notification times, and the four enable toggles. from_options() reads
the CONF_* keys with the matching defaults and validates (horizon int
>= 1, strict HH:MM times, real booleans); horizon_window(today) returns
the rolling [today, today+horizon_days] window.
The integration builds the settings object once from entry.options and
threads it into the horizon materialization and daily-rollover listener;
core never reads HA config itself. A settings.py shim re-exports
core.settings so the integration keeps its shim-only import convention.
Tests: an AST scan asserts no core module imports homeassistant or
accesses hass.config / entry.options / entry.data; a from_options test
covers defaults-when-absent and rejection of invalid horizon/rollover;
a materialization test drives a non-default horizon (3 days, 03:30
rollover) and asserts the resulting instance window matches.
P2-1: replace the all-or-nothing from_options try/except in setup with
from_options_resilient, which falls back PER FIELD (logging a warning
naming the bad field) so a malformed sibling (e.g. morning_summary_time
'25:00') cannot silently reset horizon_days/day_rollover_time.
P2-2: drop _run_horizon_materialization's legacy horizon_days keyword and
branch; default settings to NestQuestSettings(); remove the legacy paragraph.
P2-3: _run_horizon_materialization now calls settings.horizon_window(today)
so the helper is real and shared; extend tests to run the integration path
(_run_horizon_materialization + _register_day_rollover_listener) with
horizon_days=3 / day_rollover_time='03:30'.
P2-4: harden the AST scan to forbid hass/config_entry names, .hass/.entry/
.config_entry and .config/.options/.data attributes, and getattr/hasattr
calls with those string args; add a negative-control test for each shape.
P2-5: __post_init__ is strict (rejects None for every field); the
None->default mapping lives ONLY in from_options via _option_value.
P3: remove dead shims (_day_rollover_hour_minute, _configured_horizon_days)
and repoint test_lifecycle at from_options; de-dup the two HH:MM
validators into one using a shared TIME_PATTERN in core/const; make
horizon_window's today required; reword the notify_target docstring to
not claim options-flow parity; document _option_value's None-as-absent
semantics; remove unused test helpers.
P2: services._generation_kwargs now reads runtime.settings.horizon_days
instead of re-implementing the bool/non-int/<1 fallback against
runtime.options; drop the now-unused CONF_HORIZON_DAYS/DEFAULT_HORIZON_DAYS
imports. Remove NestQuestRuntimeData.options (kept only for that read; the
admin-id path reads entry.options directly) and the matching test assertion.
P3: _run_horizon_materialization's settings param is now REQUIRED (drop the
'| None = None' default + the NestQuestSettings() substitution branch);
update the test_timezones callers to pass NestQuestSettings() explicitly.
P3: __post_init__ normalises notify_target via _resolve_optional_str (strip)
so a hand-built NestQuestSettings(notify_target=' x ') behaves like the
options flow; keep the docstring accurate.
P3: delete the custom_components/nestquest/settings.py shim (settings is a
new module with no legacy import path) and import NestQuestSettings from
.core.settings directly in __init__.py, in alphabetical position.
P3: harden the AST scan for importlib.import_module('homeassistant...')
(string-based import) and call keyword names like hass= (ast.keyword);
add negative-control cases for each; explain in the failure message why
.config/.options/.data are banned (HA config-entry attributes with no
legitimate core use).
P3: drive from_options and from_options_resilient from one _FIELDS table of
(field, key, default, validator) to avoid three-way drift.
uv run pytest -q: 1573 passed.
Add LIFECYCLE tests that wire an entry via make_entry(options=...) and
call async_setup_entry, covering the from_options_resilient fallback
production actually uses (P2): a fully malformed options dict
(horizon_days=True, day_rollover_time='99:99',
morning_summary_time='25:00') succeeds with the day-rollover listener
at the default 00:00 and the backfill over DEFAULT_HORIZON_DAYS; and a
bad sibling (horizon_days=5, morning_summary_time='25:00') keeps
horizon_days == 5. Swapping from_options_resilient for from_options in
setup now fails these tests.
Reword comments/tests describing review history rather than the code:
__init__.py setup comment, test_core_settings P2-3/P2-1/P2-5 markers,
the moved from_options horizon test, and the AST scanner docstrings.
Test hygiene: move freeze_nestquest_clock into tests/conftest.py and
use it from test_core_settings and test_timezones; simplify CORE_PKG to
a plain import plus Path(mod.__file__).parent; move
test_from_options_horizon_validates_and_defaults into test_core_settings.
1575 passed.
dev_agent
merged commit b3715ccdcd into feature/a9513378-856c-4ced-8722-c99850d3a524-domain-core-extraction2026-09-21 15:55:18 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task 4dd8f870
Replace HA config reads with an explicit settings object.
Done-condition
core/settings.pyexposes a frozen, Home-Assistant-freeNestQuestSettingsdataclass carryinghorizon_days,day_rollover_time(strict HH:MM),notify_target, the three notification times, and the four enable toggles.NestQuestSettings.from_options(mapping)reads theCONF_*keys with the matching defaults and validates (horizon int >= 1, strict HH:MM, real booleans);horizon_window(today)returns the rolling[today, today+horizon_days]window.entry.optionsand passes it through; core never reads HA config.custom_components/nestquest/core/importshomeassistant, accesseshass.config/ a config-entry attribute, or reads config-entry options (docstring mentions allowed).horizon_days=3andday_rollover_time=03:30and drives materialization over that horizon, asserting the resulting instance window matches.from_optionstest: defaults applied when keys absent; invalid horizon/rollover rejected.uv run pytest -qgreen (1575).Invariants confirmed
homeassistantimport undercore/..coreimport at the integration top level (goes through thesettings.pyshim, matching the existing shim convention).core/.Behaviour intentionally improved (review round 3)
A malformed
day_rollover_time(and the other notification times /horizon_days) used to raiseValueErrorout ofasync_setup_entry, failing setup. Setup now builds settings viafrom_options_resilient, which falls each malformed field back to its default with a warning, so a single bad sibling option cannot reset the user'shorizon_daysorday_rollover_time. Two LIFECYCLE tests wire an entry viamake_entry(options=...)and callasync_setup_entryto prove this end-to-end (a fully malformed options dict succeeds with the default rollover time and default horizon; a bad sibling keeps the configured horizon).SHA
d15a02fTest count
1575 passed (was 1507; +39 new in tests/test_core_settings.py, +2 lifecycle tests for the malformed-option fallback in review round 3).
P2: services._generation_kwargs now reads runtime.settings.horizon_days instead of re-implementing the bool/non-int/<1 fallback against runtime.options; drop the now-unused CONF_HORIZON_DAYS/DEFAULT_HORIZON_DAYS imports. Remove NestQuestRuntimeData.options (kept only for that read; the admin-id path reads entry.options directly) and the matching test assertion. P3: _run_horizon_materialization's settings param is now REQUIRED (drop the '| None = None' default + the NestQuestSettings() substitution branch); update the test_timezones callers to pass NestQuestSettings() explicitly. P3: __post_init__ normalises notify_target via _resolve_optional_str (strip) so a hand-built NestQuestSettings(notify_target=' x ') behaves like the options flow; keep the docstring accurate. P3: delete the custom_components/nestquest/settings.py shim (settings is a new module with no legacy import path) and import NestQuestSettings from .core.settings directly in __init__.py, in alphabetical position. P3: harden the AST scan for importlib.import_module('homeassistant...') (string-based import) and call keyword names like hass= (ast.keyword); add negative-control cases for each; explain in the failure message why .config/.options/.data are banned (HA config-entry attributes with no legitimate core use). P3: drive from_options and from_options_resilient from one _FIELDS table of (field, key, default, validator) to avoid three-way drift. uv run pytest -q: 1573 passed.