362e089be0
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""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
|