Warn when a node that runs no code names a device

A device on a node without source was accepted and silently ignored. The
build now reports it as ignored_device, an advisory issue.
This commit is contained in:
2026-10-15 09:45:24 +02:00
parent c9fda50601
commit 96f4fa5a38
6 changed files with 50 additions and 5 deletions
+20
View File
@@ -174,6 +174,8 @@ class LoadedNode:
#: Published, and with no body of its own — so what runs is the new-node
#: template, which returns nothing at all.
missing_source: bool = False
#: The device this node names although its type never runs on a worker.
ignored_device: str = ""
@dataclass
@@ -1041,6 +1043,8 @@ class FlowController:
node_type = NODE_TYPES.get(node_def.type)
if node_type is None:
raise ValueError(f"Unknown node type '{node_def.type}'")
if node_def.device and not node_type.has_source:
entry.ignored_device = node_def.device
params = resolve_params(node_def.params)
@@ -1923,6 +1927,22 @@ def _collect_issues(
for entry in loaded.values()
if entry.missing_source
]
# Only code travels to a worker, so a device on any other node is a field
# nothing reads. Saying so beats a setting that looks like it took.
issues += [
ValidationIssue(
code="ignored_device",
message=(
f"Node '{entry.id.rpartition('.')[2]}' names device "
f"'{entry.ignored_device}', but only a Function node runs on a "
"worker. The setting is ignored."
),
flow=entry.flow,
node=entry.id,
)
for entry in loaded.values()
if entry.ignored_device
]
return issues
+2 -1
View File
@@ -46,7 +46,7 @@ MANUAL_RUN_PREFIX = "manual-"
# neither the brain graph nor the health summary treats them as a fault.
# Lives here rather than beside the controller so `ValidationIssue` can carry
# the distinction itself, and every reader gets it for free.
ADVISORY_ISSUES = frozenset({"unauthenticated_hook"})
ADVISORY_ISSUES = frozenset({"unauthenticated_hook", "ignored_device"})
class ValidationIssue(BaseModel):
@@ -61,6 +61,7 @@ class ValidationIssue(BaseModel):
"self_loop_needs_initial",
"missing_source",
"node_unhealthy",
"ignored_device",
]
message: str
flow: str = ""
+23
View File
@@ -125,6 +125,29 @@ def test_a_published_node_with_no_body_is_reported(store: FlowStore):
assert [i for i in controller.issues if i.code == "missing_source"] == []
def test_a_device_on_a_node_without_code_is_reported(store: FlowStore):
"""Only code travels to a worker, so the device would be ignored silently."""
store.write_flow(
FlowDef(
name="heating",
nodes=[
NodeDef(
id="relay",
type="change",
device="gpu",
provides=[MessageSpec(name="temp")],
)
],
)
)
controller = FlowController(store)
asyncio.run(controller.reload())
(issue,) = [i for i in controller.issues if i.code == "ignored_device"]
assert issue.node == "heating.relay"
assert issue.advisory
def test_a_node_being_written_in_the_editor_is_not_reported(store: FlowStore):
"""A draft legitimately has no published body yet — that is what a draft is."""
store.write_draft(a_flow(), 0)