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)
+2 -1
View File
@@ -132,9 +132,10 @@ The canvas reports issues on the affected nodes:
| `missing_source` | a Function node has no saved code |
| `node_error` | code or configuration could not load |
| `unauthenticated_hook` | a webhook or served websocket has no shared secret; advisory |
| `ignored_device` | a node other than a Function node names a device, which nothing reads; advisory |
| `node_unhealthy` | a loaded node reports a runtime problem, such as a lost connection |
Build issues prevent the affected flow from running. The last two are runtime
Build issues prevent the affected flow from running. The last three are runtime
or advisory issues: other work continues, and node health clears when the
connection recovers.
+1 -1
View File
@@ -4325,7 +4325,7 @@ export const ValidationIssueSchema = {
properties: {
code: {
type: 'string',
enum: ['cycle', 'unconnected_input', 'missing_initial_value', 'node_error', 'unauthenticated_hook', 'self_loop_needs_initial', 'missing_source', 'node_unhealthy'],
enum: ['cycle', 'unconnected_input', 'missing_initial_value', 'node_error', 'unauthenticated_hook', 'self_loop_needs_initial', 'missing_source', 'node_unhealthy', 'ignored_device'],
title: 'Code'
},
message: {
+2 -2
View File
@@ -1406,7 +1406,7 @@ export type ValidationError = {
* Something wrong with a flow — a fault, or merely advisory.
*/
export type ValidationIssue = {
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy';
code: 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device';
message: string;
flow?: string;
nodes?: Array<(string)>;
@@ -1419,7 +1419,7 @@ export type ValidationIssue = {
readonly advisory: boolean;
};
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy';
export type code = 'cycle' | 'unconnected_input' | 'missing_initial_value' | 'node_error' | 'unauthenticated_hook' | 'self_loop_needs_initial' | 'missing_source' | 'node_unhealthy' | 'ignored_device';
export type ValidationResult = {
issues?: Array<ValidationIssue>;