"""Stopping a flow takes it off the engine; pausing holds its nodes.""" from pathlib import Path from app.flow.messages import DType, MessageSpec from app.flow.nodes import Node from app.flow.pipeline import Pipeline from app.flow.store import FlowStore def spec(name: str) -> MessageSpec: return MessageSpec(name=name, dtype=DType.FLOAT) def make_node(node_id: str, flow: str, f, requires=(), provides=()) -> Node: node = Node(f=f, requires=list(requires), provides=list(provides), name=node_id) node.assign_flow(flow, node_id) return node def a_chain(flow: str, ran: list[str]) -> list[Node]: """source → middle: two nodes, so stepping has somewhere to stop.""" def source(params): ran.append(f"{flow}.source") return {"temp": 20.0} def middle(temp, params): ran.append(f"{flow}.middle") return {"warm": temp > 10} return [ make_node("source", flow, source, provides=[spec("temp")]), make_node( "middle", flow, middle, requires=[spec("temp")], provides=[MessageSpec(name="warm", dtype=DType.BOOL)], ), ] def test_a_stopped_flow_runs_nothing_and_its_neighbours_carry_on(): ran: list[str] = [] pipeline = Pipeline( nodes=a_chain("stopped", ran) + a_chain("running", ran), disabled_flows={"stopped"}, ) pipeline.run({}) assert not any(name.startswith("stopped.") for name in ran) assert {"running.source", "running.middle"} <= set(ran) def test_a_stopped_flow_ignores_a_trigger_from_its_own_nodes(): ran: list[str] = [] nodes = a_chain("stopped", ran) pipeline = Pipeline(nodes=nodes, disabled_flows={"stopped"}) # What an MQTT subscription or a webhook would do. nodes[0].inject({"temp": 20.0}) assert ran == [] assert "stopped.temp" not in pipeline.state def test_a_paused_flow_still_takes_values_but_acts_on_none_of_them(): ran: list[str] = [] nodes = a_chain("demo", ran) pipeline = Pipeline(nodes=nodes) pipeline.pause("demo") nodes[0].inject({"temp": 20.0}) # The value is there to look at; nothing downstream of it ran. assert pipeline.state["demo.temp"] == 20.0 assert ran == [] def test_resuming_runs_what_was_held_back(): ran: list[str] = [] pipeline = Pipeline(nodes=a_chain("demo", ran)) pipeline.pause("demo") pipeline.run({}) assert ran == [] pipeline.resume("demo") assert ran == ["demo.source", "demo.middle"] assert pipeline.paused_flows() == [] def test_stopped_survives_a_restart(tmp_path: Path): store = FlowStore(tmp_path / "flows") assert store.read_enabled("heating") is True store.write_enabled("heating", False) assert store.read_enabled("heating") is False # A second store over the same directory is what a restart looks like. assert FlowStore(tmp_path / "flows").read_enabled("heating") is False