From e4428efb8d8878fbe015fb12c002b5e4f9e1bb8e Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 30 Aug 2026 17:25:14 +0200 Subject: [PATCH] Draw a node that is down as a troubled neuron MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `brain_graph` read `self.issues` alone, which is what a build found — so a node that loaded and then lost its device was a well neuron on Home, and that is what someone comes to this view to find. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KYM38KSb4V4v2T71eifnZv --- backend/fluksio/flow/controller.py | 8 +++++--- backend/tests/flow/test_brain_graph.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/fluksio/flow/controller.py b/backend/fluksio/flow/controller.py index 6f43120..5b57dfe 100644 --- a/backend/fluksio/flow/controller.py +++ b/backend/fluksio/flow/controller.py @@ -1497,10 +1497,12 @@ class FlowController: compiled pipeline, so a node that failed to load still appears — a broken neuron is exactly what someone comes to this view to find. """ - # What validation found, by the node it names. A cycle names every node - # in it, so all of them are marked rather than an arbitrary one. + # What validation found, by the node it names, plus what is down right + # now — a neuron someone comes here to find is as often a node that + # stopped working as one that never built. A cycle names every node in + # it, so all of them are marked rather than an arbitrary one. troubled: dict[str, str] = {} - for issue in self.issues: + for issue in self.issues + self._health_issues(): if issue.code in ADVISORY_ISSUES: continue for member in (*issue.nodes, *filter(None, [issue.node])): diff --git a/backend/tests/flow/test_brain_graph.py b/backend/tests/flow/test_brain_graph.py index 3be0868..8cf982c 100644 --- a/backend/tests/flow/test_brain_graph.py +++ b/backend/tests/flow/test_brain_graph.py @@ -9,7 +9,7 @@ from pathlib import Path import pytest -from fluksio.flow.controller import FlowController +from fluksio.flow.controller import FlowController, LoadedNode from fluksio.flow.messages import DType, MessageSpec from fluksio.flow.nodes import MqttNode from fluksio.flow.pipeline import ValidationIssue @@ -105,6 +105,26 @@ def test_a_neuron_carries_what_stops_it_running(controller: FlowController): } +def test_a_node_that_is_down_is_a_troubled_neuron(controller: FlowController): + """A node that stopped working is what someone comes to this view to find. + + `self.issues` is what a build found; being down happens afterwards, so the + graph has to read the health issues too or a dead node draws as a well one. + """ + controller.loaded["house.scale"] = LoadedNode( + id="house.scale", + flow="house", + health="down", + health_detail="broker unreachable", + ) + + marked = { + node.id: node.issue for node in controller.brain_graph().nodes if node.issue + } + assert list(marked) == ["house.scale"] + assert "broker unreachable" in marked["house.scale"] + + def test_an_advisory_issue_leaves_the_graph_clean(controller: FlowController): controller.issues = [ ValidationIssue(