"""Dashboards: documents beside the flows, and the values their widgets move.""" import pytest from app.flow.dashboards import ( DashboardDef, DashboardNotFound, DashboardStore, PageDef, SectionDef, WidgetDef, default_dashboard, ) from app.flow.messages import DType, MessageSpec from app.flow.nodes import Node from app.flow.pipeline import Pipeline from app.flow.state import MemoryState from app.flow.store import FlowStore, StaleVersion @pytest.fixture def store(tmp_path) -> DashboardStore: return DashboardStore(FlowStore(tmp_path / "flows")) def chart(message: str, points: int) -> WidgetDef: return WidgetDef( id="temp", type="chart", config={"series": [{"message": message}], "history": {"points": points}}, ) def test_a_dashboard_survives_a_round_trip(store: DashboardStore): saved = store.write(default_dashboard("house")) read = store.read("house") assert read.name == "house" assert [p.id for p in read.pages] == ["main"] assert read.version == saved.version def test_dashboards_are_invisible_to_the_flow_listing(store: DashboardStore): """They share the repository; they are not flows.""" store.write(default_dashboard("house")) assert store.flows.list_flows() == [] assert [d.name for d in store.list()] == ["house"] def test_a_save_based_on_a_version_someone_moved_past_is_refused( store: DashboardStore, ): first = store.write(default_dashboard("house")) store.write(first, first.version) with pytest.raises(StaleVersion): store.write(first, first.version) def test_an_edit_reaches_a_panel_only_once_it_is_published(store: DashboardStore): """A wall panel reads the published file; the editor writes beside it.""" published = store.write(default_dashboard("house")) draft = store.write_draft( published.model_copy(update={"title": "Kitchen"}), published.version ) assert store.has_draft("house") assert store.read("house").title == published.title assert store.read("house", draft=True).title == "Kitchen" store.publish("house", draft.version) assert store.read("house").title == "Kitchen" assert not store.has_draft("house") def test_discarding_leaves_what_is_published(store: DashboardStore): published = store.write(default_dashboard("house")) store.write_draft( published.model_copy(update={"title": "Kitchen"}), published.version ) assert store.discard_draft("house").title == published.title assert not store.has_draft("house") def test_deleting_and_renaming(store: DashboardStore): store.write(default_dashboard("house")) renamed = store.rename("house", "home") assert renamed.name == "home" assert not store.exists("house") store.delete("home") with pytest.raises(DashboardNotFound): store.read("home") def test_the_deepest_chart_decides_how_much_past_is_kept(store: DashboardStore): store.write( DashboardDef( name="house", pages=[ PageDef( id="main", sections=[ SectionDef(id="a", widgets=[chart("heating.temp", 400)]), SectionDef( id="b", widgets=[ chart("heating.temp", 900), chart("solar.watts", 100), ], ), ], ) ], ) ) assert store.history_requirements() == {"heating.temp": 900, "solar.watts": 100} def test_a_chart_cannot_ask_for_an_unbounded_series(store: DashboardStore): store.write( DashboardDef( name="house", pages=[ PageDef( id="main", sections=[ SectionDef(id="a", widgets=[chart("heating.temp", 10**9)]) ], ) ], ) ) assert store.history_requirements() == {"heating.temp": 5000} def test_history_is_kept_to_the_depth_a_chart_asked_for(): state = MemoryState() limits = {"f.temp": 300} for i in range(400): state.append_history({"f.temp": float(i)}, float(i), limits) assert len(state.history("f.temp")) == 300 # --------------------------------------------------------------------------- # What an input widget does # --------------------------------------------------------------------------- def test_publishing_a_value_runs_what_consumes_it(): """A slider is a value arriving; the graph should not care who sent it.""" seen: list[float] = [] def consume(setpoint, params): seen.append(setpoint) return {"applied": setpoint} node = Node( f=consume, requires=[MessageSpec(name="setpoint", port="setpoint", dtype=DType.FLOAT)], provides=[MessageSpec(name="applied", port="applied", dtype=DType.FLOAT)], name="thermostat", ) node.assign_flow("heating", "thermostat") state = MemoryState() pipeline = Pipeline(nodes=[node], state=state) pipeline.publish({"heating.setpoint": 21.5}) assert seen == [21.5] assert state["heating.applied"] == 21.5 def test_publishing_nothing_does_nothing(): pipeline = Pipeline(nodes=[], state=MemoryState()) pipeline.publish({}) assert pipeline.values() == {}