Files
app/backend/tests/flow/test_library.py
T
Melvin StroblandClaude Fable 5 3724b68f23 Add the connector contract, reusable nodes and per-port intervals
Connectors are the device-facing node class third parties write, so the
surface they build against is versioned and documented: ConnectorNode carries
a declared contract version, a polling loop that publishes only what changed
and reports health around it, and parameters whose credential fields are
marked x-secret so the editor offers the secrets store instead of a text box.
They are found through the fluksio.node_types entry point group, with the
package's own metadata as the manifest. docs/connectors/ has the contract and
the authoring guide; connector-skeleton/ is a working one to copy.

The controller no longer knows what any node type is: start, stop and
report_health are protocol methods on Node, and the built-ins were migrated to
them first, so the hooks a connector implements are the ones the engine has
been driving all along.

Marking a node reusable moves its source to _lib/ and points the node at it by
name. Other flows instantiate it with their own ports and settings, one fix
reaches all of them, and a shared source still in use cannot be deleted.

Ports gained an interval: an output publishes, and an input wakes its node, at
most every n seconds. State keeps the latest value, so only the delivery is
skipped, and pressing Run is never throttled.

Also fixes autosave sending no version on its first save of a session, which
made every flow saved more than once conflict with itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 23:57:44 +02:00

97 lines
3.0 KiB
Python

"""Nodes shared across flows: one source, many instances."""
from pathlib import Path
import pytest
from app.flow.messages import MessageSpec
from app.flow.schemas import FlowDef, NodeDef
from app.flow.store import FlowStore, LibExists, LibNotFound
SOURCE = "def process(params):\n return {'temp': 1}\n"
@pytest.fixture
def store(tmp_path: Path) -> FlowStore:
return FlowStore(tmp_path / "flows")
def a_flow(name: str = "heating") -> FlowDef:
return FlowDef(
name=name,
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
)
def test_sharing_moves_the_source_and_points_the_node_at_it(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source("heating", "sensor", SOURCE)
store.share_node("heating", "sensor", "read_temp")
assert store.list_lib() == ["read_temp"]
assert store.read_lib_source("read_temp") == SOURCE
node = store.read_flow("heating", draft=True).nodes[0]
assert node.source_ref == "read_temp"
# The private copy is gone; the library one is what it runs.
assert not (store.root / "heating" / "nodes" / "sensor.py").exists()
def test_the_library_is_not_mistaken_for_a_flow(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source("heating", "sensor", SOURCE)
store.share_node("heating", "sensor", "read_temp")
assert store.list_flows() == ["heating"]
assert [flow.name for flow in store.read_all()] == ["heating"]
def test_a_second_flow_can_use_the_same_source(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source("heating", "sensor", SOURCE)
store.share_node("heating", "sensor", "read_temp")
store.write_flow(
FlowDef(
name="cooling",
nodes=[
NodeDef(
id="sensor",
source_ref="read_temp",
provides=[MessageSpec(name="temp")],
)
],
)
)
assert store.usages("read_temp") == ["cooling.sensor", "heating.sensor"]
def test_a_shared_name_is_not_taken_twice(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source("heating", "sensor", SOURCE)
store.share_node("heating", "sensor", "read_temp")
store.write_flow(FlowDef(name="cooling", nodes=[NodeDef(id="sensor")]))
with pytest.raises(LibExists):
store.share_node("cooling", "sensor", "read_temp")
def test_unsharing_takes_a_private_copy_back(store: FlowStore):
store.write_flow(a_flow())
store.write_node_source("heating", "sensor", SOURCE)
store.share_node("heating", "sensor", "read_temp")
store.unshare_node("heating", "sensor")
node = store.read_flow("heating", draft=True).nodes[0]
assert node.source_ref is None
assert store.read_node_source("heating", "sensor", draft=True) == SOURCE
# The library keeps its copy for whoever else is using it.
assert store.list_lib() == ["read_temp"]
def test_a_missing_shared_source_is_reported(store: FlowStore):
with pytest.raises(LibNotFound):
store.read_lib_source("nothing_here")