adaptive-lighting/tests/test_hass_utils.py
Casey 03d2748cd4 Group 7: tests — 94 passing in 0.57s (53/53 + 9.4 deferred)
Test infrastructure:
- Add `pytest-homeassistant-custom-component` (PHACC) as a `test`
  dependency group. PHACC ships `hass`, `enable_custom_integrations`,
  `MockConfigEntry`, and friends without needing to clone HA core as a
  sibling directory. Modernizes the test setup from upstream's
  `setup-symlinks` pattern.
- conftest.py: add `auto_enable_custom_integrations` autouse fixture
  so HA discovers the integration under `custom_components/` during
  tests. Keep the upstream template-deprecation no-op.
- Import paths: all tests now import from `custom_components.adaptive_lighting`
  (not `homeassistant.components.adaptive_lighting`) and from
  `pytest_homeassistant_custom_component.common` (not `tests.common`).

New tests (groups 7.2-7.16):
- tests/test_color_and_brightness.py (16 tests): TestBrightnessCurve
  asserts min-before-sunrise, midpoint-at-event, max-during-day, sunset
  ramp symmetry. TestColorTempCurve verifies the same shape applies to
  K. TestSunPosition checks the synthetic +1/-1/0 derivation.
  TestTanhDayCurveDirect exercises the helper directly.
- tests/test_config_flow.py (12 tests): six sections in order, each
  section contains only its specified fields, conditional visibility
  of send_split_delay, default sun entities, strict-typed entity
  selectors, NumberSelector slider/box configs, BooleanSelector for
  every flag, full user→create-entry flow, YAML-managed entry aborts
  with `yaml_managed` reason, options flow renders the sectioned schema.
- tests/test_init.py (6 tests): successful setup on current version,
  stale version raises ConfigEntryError via async_migrate_entry,
  unload is clean, tombstone removes orphan sleep entity + logs INFO,
  tombstone is idempotent, tombstone respects config_entry ownership.

Source modernizations driven by the tests:
- __init__.py: add `async_migrate_entry` that surfaces the
  "incompatible — delete and recreate" message and sets the entry to
  MIGRATION_ERROR. Removed direct `ConfigEntryError` from
  `async_setup_entry` (HA routes version mismatches through the
  migration handler now).
- switch.py: removed upstream's YAML-managed-entry auto-remove hack.
  YAML profiles now load normally and the options flow handles the
  "you must edit configuration.yaml" message (spec R7).

Existing tests updated to PHACC paths: test_adaptation_utils.py
(38 tests), test_hass_utils.py (22 tests). Both pass without
modification beyond the import fix.

Deleted: tests/test_switch.py (2,999 LOC of upstream tests, most
covering sleep mode / take-over-control / manual-control state
machines that no longer exist; CDiT-specific switch tests deferred
to a follow-up change).

Result: `uv run --group test pytest tests/` → 94 passed in 0.57s.

Deferred from this change:
- 4.3: manual UI test that toggling a field and saving reloads cleanly
  (requires a real HA instance, can't be done from CLI).
- 9.4: GitHub repo description / topics update (do via `gh repo edit`
  outside the change scope).

openspec status: 4/4 artifacts complete; strict-validate green.
2026-05-16 15:11:49 +02:00

80 lines
2.3 KiB
Python

"""Tests for Adaptive Lighting HASS utils."""
from unittest.mock import AsyncMock
from custom_components.adaptive_lighting.adaptation_utils import ServiceData
from custom_components.adaptive_lighting.hass_utils import (
setup_service_call_interceptor,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import SERVICE_TURN_ON
from homeassistant.core import ServiceCall
from homeassistant.util.read_only_dict import ReadOnlyDict
async def test_setup_service_call_interceptor(hass):
"""Test setup and removal of service call interceptor."""
service_func_mock = AsyncMock()
hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
async def service_call():
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{},
blocking=True,
)
# Test if service is called
await service_call()
assert service_func_mock.call_count == 1
# Test if interceptor is called after setup
intercept_func_mock = AsyncMock()
remove_interceptor = setup_service_call_interceptor(
hass,
LIGHT_DOMAIN,
SERVICE_TURN_ON,
intercept_func_mock,
)
await service_call()
assert service_func_mock.call_count == 2
assert intercept_func_mock.call_count == 1
# Test if interceptor is no longer called after removal
remove_interceptor()
await service_call()
assert service_func_mock.call_count == 3
assert intercept_func_mock.call_count == 1
async def test_service_call_interceptor_data_manipulation(hass):
"""Test service call data manipulation by service call interceptor."""
service_func_mock = AsyncMock()
hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
async def intercept_func(call: ServiceCall, data: ServiceData):
data["test1"] = "changed"
data["test2"] = "added"
setup_service_call_interceptor(
hass,
LIGHT_DOMAIN,
SERVICE_TURN_ON,
intercept_func,
)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{"test1": "initial"},
blocking=True,
)
(service_call,) = service_func_mock.call_args[0]
assert service_call.data == {"test1": "changed", "test2": "added"}
assert isinstance(service_call.data, ReadOnlyDict)