A python node's settings are constants of its own function, so they are passed the way its ports are: by name. The controller binds them to the compiled function, the `params` field is gone from the worker and remote protocols, and a setting sharing a port's name is reported as a node error rather than shadowing it. The panel's scaffold follows suit and keeps the header in step with both ports and settings. The demo's `pace` moves from a flow input to a setting of the training node, which is what it always was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
134 lines
4.3 KiB
Python
134 lines
4.3 KiB
Python
"""Editing writes drafts; only publishing changes what the engine reads."""
|
|
|
|
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, StaleVersion
|
|
|
|
SOURCE = "def process():\n return {}\n"
|
|
EDITED = "def process():\n return {'temp': 1}\n"
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> FlowStore:
|
|
return FlowStore(tmp_path / "flows")
|
|
|
|
|
|
def a_flow(title: str = "") -> FlowDef:
|
|
return FlowDef(
|
|
name="heating",
|
|
title=title,
|
|
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
|
|
)
|
|
|
|
|
|
def test_a_draft_is_invisible_to_the_engine(store: FlowStore):
|
|
store.write_draft(a_flow(), 0)
|
|
|
|
assert store.has_draft("heating")
|
|
assert store.list_flows() == ["heating"] # the editor sees it
|
|
assert store.read_all() == [] # the engine does not
|
|
|
|
|
|
def test_the_version_counter_advances_per_save(store: FlowStore):
|
|
first = store.write_draft(a_flow(), 0)
|
|
assert first.version == 1
|
|
|
|
second = store.write_draft(a_flow(title="Warm"), first.version)
|
|
assert second.version == 2
|
|
|
|
|
|
def test_resaving_identical_content_neither_bumps_nor_commits(store: FlowStore):
|
|
stored = store.write_draft(a_flow(), 0)
|
|
|
|
again = store.write_draft(a_flow(), stored.version)
|
|
assert again.version == stored.version
|
|
|
|
|
|
def test_a_stale_write_is_refused(store: FlowStore):
|
|
store.write_draft(a_flow(), 0)
|
|
store.write_draft(a_flow(title="Mine"), 1)
|
|
|
|
# A second client still holding version 1 must not overwrite version 2.
|
|
with pytest.raises(StaleVersion) as excinfo:
|
|
store.write_draft(a_flow(title="Theirs"), 1)
|
|
assert excinfo.value.current == 2
|
|
|
|
|
|
def test_publishing_promotes_the_draft_and_its_sources(store: FlowStore):
|
|
stored = store.write_draft(a_flow(title="Warm"), 0)
|
|
store.write_node_source("heating", "sensor", EDITED, draft=True)
|
|
|
|
published = store.publish_flow("heating", stored.version)
|
|
|
|
assert published.title == "Warm"
|
|
assert not store.has_draft("heating")
|
|
assert store.read_flow("heating").title == "Warm"
|
|
assert store.read_node_source("heating", "sensor") == EDITED
|
|
assert [flow.name for flow in store.read_all()] == ["heating"]
|
|
|
|
|
|
def test_publishing_a_stale_draft_is_refused(store: FlowStore):
|
|
store.write_draft(a_flow(), 0)
|
|
|
|
with pytest.raises(StaleVersion):
|
|
store.publish_flow("heating", 99)
|
|
|
|
|
|
def test_discarding_goes_back_to_what_is_running(store: FlowStore):
|
|
store.write_flow(a_flow(title="Published"))
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.write_draft(a_flow(title="Edited"), a_flow().version)
|
|
store.write_node_source("heating", "sensor", EDITED, draft=True)
|
|
|
|
restored = store.discard_draft("heating")
|
|
|
|
assert restored.title == "Published"
|
|
assert not store.has_draft("heating")
|
|
assert store.read_node_source("heating", "sensor", draft=True) == SOURCE
|
|
|
|
|
|
def test_an_edited_source_alone_counts_as_a_draft(store: FlowStore):
|
|
"""Editing only code still has to be published before the engine runs it."""
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
|
|
store.write_node_source("heating", "sensor", EDITED, draft=True)
|
|
|
|
assert store.has_draft("heating")
|
|
assert store.read_node_source("heating", "sensor") == SOURCE
|
|
assert store.read_node_source("heating", "sensor", draft=True) == EDITED
|
|
|
|
store.publish_flow("heating", store.read_flow("heating").version)
|
|
assert store.read_node_source("heating", "sensor") == EDITED
|
|
|
|
|
|
def test_resaving_the_published_source_creates_no_draft(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
|
|
assert store.write_node_source("heating", "sensor", SOURCE, draft=True) is False
|
|
assert not store.has_draft("heating")
|
|
|
|
|
|
def test_renaming_carries_the_draft_and_repoints_other_drafts(store: FlowStore):
|
|
store.write_draft(a_flow(), 0)
|
|
store.write_draft(
|
|
FlowDef(
|
|
name="display",
|
|
nodes=[NodeDef(id="gauge", requires=[MessageSpec(name="heating.temp")])],
|
|
),
|
|
0,
|
|
)
|
|
|
|
renamed = store.rename_flow("heating", "warmth")
|
|
|
|
assert renamed.name == "warmth"
|
|
assert store.has_draft("warmth")
|
|
assert store.read_flow("display", draft=True).nodes[0].requires[0].name == (
|
|
"warmth.temp"
|
|
)
|