"""Tests for the per-type ValueSource factory registry. Locks in the contract that ``CREATE_BUILDERS`` and ``UPDATE_APPLIERS`` stay symmetric with storage's ``_VALUE_SOURCE_MAP``, and exercises a couple of representative builders end-to-end. """ from __future__ import annotations from datetime import datetime, timezone import pytest from ledgrab.storage import value_source_factories as vsf from ledgrab.storage.value_source import ( AdaptiveValueSource, HAEntityValueSource, StaticValueSource, _VALUE_SOURCE_MAP, ) # --------------------------------------------------------------------------- # Coverage / shape # --------------------------------------------------------------------------- def test_create_builders_cover_every_storage_kind(): assert set(vsf.CREATE_BUILDERS.keys()) == set(_VALUE_SOURCE_MAP.keys()) def test_update_appliers_cover_every_storage_kind(): assert set(vsf.UPDATE_APPLIERS.keys()) == set(_VALUE_SOURCE_MAP.keys()) def test_coverage_assertion_raises_when_create_drifts(monkeypatch): """Removing a builder makes _assert_factory_coverage() fail loudly.""" pruned = {k: v for k, v in vsf.CREATE_BUILDERS.items() if k != "static"} monkeypatch.setattr(vsf, "CREATE_BUILDERS", pruned) with pytest.raises(RuntimeError, match="static"): vsf._assert_factory_coverage() def test_coverage_assertion_raises_when_update_drifts(monkeypatch): pruned = {k: v for k, v in vsf.UPDATE_APPLIERS.items() if k != "http"} monkeypatch.setattr(vsf, "UPDATE_APPLIERS", pruned) with pytest.raises(RuntimeError, match="http"): vsf._assert_factory_coverage() # --------------------------------------------------------------------------- # build_source — representative paths # --------------------------------------------------------------------------- def _now() -> datetime: return datetime.now(timezone.utc) def test_build_source_static_uses_value_default(): src = vsf.build_source( source_type="static", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ) assert isinstance(src, StaticValueSource) assert src.value == 1.0 assert src.source_type == "static" def test_build_source_static_honours_supplied_value(): src = vsf.build_source( source_type="static", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, value=0.42, ) assert src.value == 0.42 def test_build_source_adaptive_time_requires_schedule_with_two_points(): with pytest.raises(ValueError, match="Time of day schedule"): vsf.build_source( source_type="adaptive_time", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, schedule=[{"time": "00:00", "value": 0.5}], # only one point ) def test_build_source_adaptive_scene_routes_to_adaptive_class_with_scene_type(): """``adaptive_scene`` and ``adaptive_time`` share AdaptiveValueSource but differ in source_type — the builder must set the right discriminator.""" src = vsf.build_source( source_type="adaptive_scene", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, picture_source_id="pic_42", ) assert isinstance(src, AdaptiveValueSource) assert src.source_type == "adaptive_scene" assert src.picture_source_id == "pic_42" def test_build_source_ha_entity_requires_ha_source_id_and_entity_id(): with pytest.raises(ValueError, match="HA source ID"): vsf.build_source( source_type="ha_entity", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, entity_id="light.foo", # missing ha_source_id ) with pytest.raises(ValueError, match="Entity ID"): vsf.build_source( source_type="ha_entity", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ha_source_id="ha_main", # missing entity_id ) def test_build_source_http_validates_interval(): with pytest.raises(ValueError, match="interval_s"): vsf.build_source( source_type="http", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, http_endpoint_id="ep_1", interval_s=0, ) def test_build_source_unknown_type_raises(): with pytest.raises(ValueError, match="Invalid source type"): vsf.build_source( source_type="totally_unregistered", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ) def test_build_source_game_event_is_not_creatable_via_store(): """game_event sources were never creatable through the store API. The builder exists for coverage-assertion symmetry but raises so the historical contract holds (game-event value sources are wired up by the game-integration setup path, not user CRUD). """ with pytest.raises(NotImplementedError, match="game_event"): vsf.build_source( source_type="game_event", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ) # --------------------------------------------------------------------------- # apply_update — representative paths # --------------------------------------------------------------------------- def test_apply_update_static_mutates_value(): src = vsf.build_source( source_type="static", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ) vsf.apply_update(src, value=0.75) assert src.value == 0.75 def test_apply_update_ignores_unknown_kwargs(): """Stray kwargs are silently dropped by each applier's ``**_``.""" src = vsf.build_source( source_type="static", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ) vsf.apply_update(src, value=0.5, totally_made_up_field="ignored") assert src.value == 0.5 def test_apply_update_ha_entity_validates_smoothing_path(): src = vsf.build_source( source_type="ha_entity", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, ha_source_id="ha_main", entity_id="light.foo", ) assert isinstance(src, HAEntityValueSource) vsf.apply_update(src, smoothing=0.5, attribute="brightness") assert src.smoothing == 0.5 assert src.attribute == "brightness" def test_apply_update_http_rejects_zero_interval(): src = vsf.build_source( source_type="http", sid="vs_t", name="Test", now=_now(), description=None, tags=None, icon=None, icon_color=None, http_endpoint_id="ep_1", ) with pytest.raises(ValueError, match="interval_s"): vsf.apply_update(src, interval_s=0)