"""Unit-Tests Project-State-Store (PLAN.md §6.4 State Sync, §24.2, §18.1).""" from __future__ import annotations import pytest from hms_domain import PresetScene from hms_persistence.state_store import ProjectStateStore def test_revisions_start_at_zero() -> None: store = ProjectStateStore() assert store.state_revision == 0 assert store.project_revision == 0 assert store.snapshot()["values"] == {} def test_activate_project_resets_live_state() -> None: """Kein Mischzustand: Projektwechsel leert den Livezustand (§24.2).""" from hms_domain import Project store = ProjectStateStore() store.set_value("master/intensity", 0.5) store.activate_project(Project(name="Show A")) assert store.project_revision == 1 assert store.project is not None and store.project.name == "Show A" assert store.get_value("master/intensity") is None # Live geleert assert store.state_revision == 2 # set_value (1) + Projektwechsel (2) def test_set_and_clear_value_increase_state_revision() -> None: store = ProjectStateStore() r1 = store.set_value("master/intensity", 0.7) assert r1 == 1 r2 = store.set_value("master/intensity", 0.9) assert r2 == 2 r3 = store.clear_value("master/intensity") assert r3 == 3 assert store.get_value("master/intensity") is None # Löschen eines nicht existierenden Pfades erhöht trotzdem deterministisch r4 = store.clear_value("master/intensity") assert r4 == 4 def test_snapshot_contains_revisions_and_values() -> None: store = ProjectStateStore() store.set_value("master/intensity", 1.0) snap = store.snapshot() assert snap["state_revision"] == 1 assert snap["project_revision"] == 0 assert snap["values"] == {"master/intensity": 1.0} assert snap["monotonic_ns"] > 0 def test_delta_since_reports_changes_and_deletions() -> None: """Delta überträgt Änderungen; gelöschte Pfade als None (§6.4).""" store = ProjectStateStore() store.set_value("a", 1.0) seen = store.state_revision # nach „Disconnect": neue Änderungen store.set_value("a", 2.0) store.set_value("b", 3.0) store.clear_value("c") # c war nie gesetzt – bleibt neutral delta = store.delta_since(seen, pending={"a": 1.0, "c": 0.0}) assert delta is not None assert delta.state_revision == store.state_revision assert delta.changes["a"] == 2.0 # geändert assert "b" in delta.changes and delta.changes["b"] == 3.0 # neu assert delta.changes.get("c") is None # gelöscht def test_delta_none_when_up_to_date() -> None: store = ProjectStateStore() store.set_value("a", 1.0) assert store.delta_since(store.state_revision, pending={}) is None def test_delta_rejects_future_revision() -> None: store = ProjectStateStore() with pytest.raises(ValueError, match="Zukunft"): store.delta_since(99, pending={}) def test_apply_scene_overwrites_live_as_target_state() -> None: """Szenenabruf erzeugt Zielzustand; Übergänge macht der Renderer (§18.1).""" store = ProjectStateStore() store.set_value("layer/x/opacity", 0.2) scene = PresetScene( name="Look", composition_snapshot={ "values": {"layer/x/opacity": 0.8, "master/intensity": 1.0} }, ) rev = store.apply_scene(scene) assert rev == 2 # set + scene assert store.get_value("layer/x/opacity") == pytest.approx(0.8) assert store.get_value("master/intensity") == pytest.approx(1.0) def test_apply_scene_rejects_scene_without_values() -> None: scene = PresetScene(name="Kaputt", composition_snapshot={"values": "kein dict"}) store = ProjectStateStore() with pytest.raises(ValueError, match="Werte"): store.apply_scene(scene) def test_bump_project_revision_independent_of_state() -> None: """Projektinhalt (Medien/Szenen) und Livezustand getrennt (§24.2).""" store = ProjectStateStore() state_before = store.state_revision store.bump_project_revision() assert store.project_revision == 1 assert store.state_revision == state_before # Live unverändert