From 0746c94e0ae290acf7d65b47ccef415f20c99b7a Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 15 Oct 2026 09:45:24 +0200 Subject: [PATCH] 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. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx --- backend/fluksio/flow/controller.py | 20 ++++++++++++++++++++ backend/fluksio/flow/pipeline.py | 3 ++- backend/tests/flow/test_drafts.py | 23 +++++++++++++++++++++++ docs/concepts/flows.md | 3 ++- frontend/src/client/schemas.gen.ts | 2 +- frontend/src/client/types.gen.ts | 4 ++-- 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/backend/fluksio/flow/controller.py b/backend/fluksio/flow/controller.py index 6675701..1e1eb12 100644 --- a/backend/fluksio/flow/controller.py +++ b/backend/fluksio/flow/controller.py @@ -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 diff --git a/backend/fluksio/flow/pipeline.py b/backend/fluksio/flow/pipeline.py index 8076def..76375a6 100644 --- a/backend/fluksio/flow/pipeline.py +++ b/backend/fluksio/flow/pipeline.py @@ -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 = "" diff --git a/backend/tests/flow/test_drafts.py b/backend/tests/flow/test_drafts.py index b3d085f..5f54da5 100644 --- a/backend/tests/flow/test_drafts.py +++ b/backend/tests/flow/test_drafts.py @@ -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) diff --git a/docs/concepts/flows.md b/docs/concepts/flows.md index 70353cd..50e6b62 100644 --- a/docs/concepts/flows.md +++ b/docs/concepts/flows.md @@ -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. diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts index 69c579f..46f8c6a 100644 --- a/frontend/src/client/schemas.gen.ts +++ b/frontend/src/client/schemas.gen.ts @@ -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: { diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 6520c41..38b9681 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -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;