Files
app/backend/tests/flow/test_store.py
T
Melvin StroblandClaude Fable 5 fd666743d2 Add flow settings, pulse emitting nodes, and simplify node state
- One dot per node now carries the whole story: primary while running, sage
  after a good run, red when anything is wrong, with the explanation on hover.
  The corner badge is gone, along with the second way of saying the same thing.
- A node that publishes something flashes a ring, so a running flow is legible
  without reading the edge values. Nodes that consume but publish nothing stay
  quiet, which is why the event carries an output count.
- Flow settings open in the same panel its nodes use, from a pencil in the
  dock: the title, the name, and deleting the flow. NodePanel and FlowPanel
  share the panel chrome rather than each drawing their own.
- Renaming is a server operation, because a flow's name is the namespace of its
  messages: the directory moves and every other flow reading `old.message` is
  repointed, instead of being left pointing at a flow that no longer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
2026-08-15 19:46:23 +02:00

120 lines
3.2 KiB
Python

import subprocess
from pathlib import Path
import pytest
from app.flow.messages import MessageSpec
from app.flow.schemas import FlowDef, NodeDef
from app.flow.store import FlowExists, FlowNotFound, FlowStore
@pytest.fixture
def store(tmp_path: Path) -> FlowStore:
return FlowStore(tmp_path / "flows")
def commit_count(store: FlowStore) -> int:
result = subprocess.run(
["git", "-C", str(store.root), "rev-list", "--count", "HEAD"],
capture_output=True,
text=True,
check=True,
)
return int(result.stdout.strip())
def a_flow() -> FlowDef:
return FlowDef(
name="heating",
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
)
def test_flow_round_trips(store: FlowStore):
store.write_flow(a_flow())
assert store.list_flows() == ["heating"]
assert store.read_flow("heating").nodes[0].provides[0].name == "temp"
def test_every_change_is_committed(store: FlowStore):
before = commit_count(store)
store.write_flow(a_flow())
assert commit_count(store) == before + 1
store.write_node_source(
"heating", "sensor", "def process(params):\n return {}\n"
)
assert commit_count(store) == before + 2
def test_saving_unchanged_content_does_nothing(store: FlowStore):
store.write_flow(a_flow())
commits = commit_count(store)
# Autosave repeats the same document; history should not grow.
assert store.write_flow(a_flow()) is False
assert commit_count(store) == commits
def test_missing_flow_is_reported(store: FlowStore):
with pytest.raises(FlowNotFound):
store.read_flow("nope")
def test_deleting_removes_flow_and_its_nodes(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source(
"heating", "sensor", "def process(params):\n return {}\n"
)
store.delete_flow("heating")
assert store.list_flows() == []
assert not (store.root / "heating").exists()
def test_renaming_a_flow_carries_its_nodes(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source(
"heating", "sensor", "def process(params):\n return {}\n"
)
renamed = store.rename_flow("heating", "warmth")
assert renamed.name == "warmth"
assert store.list_flows() == ["warmth"]
assert "def process" in store.read_node_source("warmth", "sensor")
def test_renaming_a_flow_repoints_the_flows_reading_it(store: FlowStore):
store.write_flow(a_flow())
store.write_flow(
FlowDef(
name="display",
nodes=[
NodeDef(
id="gauge",
# Reads across the flow boundary, so the name must follow.
requires=[MessageSpec(name="heating.temp")],
)
],
)
)
store.rename_flow("heating", "warmth")
display = store.read_flow("display")
assert display.nodes[0].requires[0].name == "warmth.temp"
def test_renaming_onto_an_existing_name_is_refused(store: FlowStore):
store.write_flow(a_flow())
store.write_flow(FlowDef(name="warmth"))
with pytest.raises(FlowExists):
store.rename_flow("heating", "warmth")
assert store.list_flows() == ["heating", "warmth"]