Let a node's failure outlive the run that followed it
A node's error cleared the moment it ran again, so a failure that genuinely fired an alert could leave no trace on the canvas by the time anyone looked. The engine records it now — on the node's status, so it survives a reload and every client agrees — and reading the traceback is what clears it. The seam is the event bus, which is where every failing path already meets: a queued live run, an explicit run, a preview, and a single triggered node all publish `node_error`, while the controller's own observer would have seen only one of them. That was half the confusion. The other half: clicking a failed neuron on Home often landed on a flow where everything looked fine. Nodes merge into one neuron by instance key — every InfluxDB node pointing at the same bucket is one neuron — and the click went to whichever flow contributed a member first, not the one that failed. It now goes to the failing member and selects it, and the canvas marks a failing node rather than leaving it to the dot alone. The inject node emitted one payload to every port it declared, whatever their types, so an inject on a bool port carrying the text "true" raised at publish time. Each port gets its own field now, typed and parsed by that port's dtype, and remembers what it last sent. A port that is renamed carries its value with it; one that is removed takes its value with it. An inject written before this keeps emitting exactly what it did. The derived-cron chip also appeared on the delay node, where `interval` is a rate limit and a schedule derived from it means nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -43,7 +43,7 @@ FIXTURES: dict[str, dict] = {
|
||||
"provides": [MessageSpec(name="score", dtype=DType.FLOAT)],
|
||||
},
|
||||
"inject": {
|
||||
"params": {"payload": 1.0, "interval": 60},
|
||||
"params": {"payload": 1.0, "payloads": {"tick": 2.0}, "interval": 60},
|
||||
"requires": [],
|
||||
"provides": [MessageSpec(name="tick", dtype=DType.FLOAT)],
|
||||
},
|
||||
@@ -124,6 +124,29 @@ def test_mqtt_routes_topics_by_port():
|
||||
assert node._topic_to_ports == {"house/temp": ["temp"], "house/hum": ["humidity"]}
|
||||
|
||||
|
||||
def test_inject_emits_per_port_and_falls_back_to_one_payload():
|
||||
"""A port named in `payloads` gets its own value; the rest share `payload`."""
|
||||
# Each port is published as its own declared type, which `check` enforces.
|
||||
typed = NODE_TYPES["inject"].cls(
|
||||
provides=[
|
||||
MessageSpec(name="flag", dtype=DType.BOOL),
|
||||
MessageSpec(name="count", dtype=DType.INT),
|
||||
],
|
||||
params={"payloads": {"flag": True, "count": 3}},
|
||||
)
|
||||
assert typed.execute({}) == {"flag": True, "count": 3}
|
||||
|
||||
# No `payloads` at all is what every flow written so far carries.
|
||||
shared = NODE_TYPES["inject"].cls(
|
||||
provides=[
|
||||
MessageSpec(name="left", dtype=DType.FLOAT),
|
||||
MessageSpec(name="right", dtype=DType.FLOAT),
|
||||
],
|
||||
params={"payload": 1.0},
|
||||
)
|
||||
assert shared.execute({}) == {"left": 1.0, "right": 1.0}
|
||||
|
||||
|
||||
def test_every_offered_type_has_a_fixture():
|
||||
# A new built-in without a fixture here would ship untested. Connectors are
|
||||
# separate packages and carry their own tests, so they are not this suite's.
|
||||
|
||||
Reference in New Issue
Block a user