Files
app/backend/tests/flow/test_brain_graph.py
T
stroblmeandClaude Fable 5 4bcd38354b Draw every flow as one graph, merged on what it talks to
A node type can now say which outside thing its parameters point at, and
nodes sharing one — a broker topic, a URL, a bucket — are drawn as a single
neuron on a new /brain canvas. That makes the wiring which runs between
flows through a broker visible for the first time; no single flow's canvas
can show it. The key is read off stored parameters, so a credential
reference never reaches an id.

Layout is a d3 force simulation settled once and then frozen, lit by the
socket the editor already listens to: a neuron pulses when any node behind
it publishes, and its connections light as values pass.

Fixes the message pulse while here: interpolating the stroke against the
edge's `color-mix()` resting colour went through oklab and left the gamut,
which turned every pulse on both canvases fluorescent yellow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 22:59:31 +02:00

83 lines
2.7 KiB
Python

"""Nodes talking to the same outside thing are one neuron, across flows.
That merge is the whole point of the brain graph: a broker topic is a single
physical thing, and two flows sharing one are wired together through it even
though neither canvas can draw the other end.
"""
from pathlib import Path
import pytest
from app.flow.controller import FlowController
from app.flow.messages import DType, MessageSpec
from app.flow.nodes import MqttNode
from app.flow.schemas import FlowDef, NodeDef
from app.flow.store import FlowStore
BROKER = {"broker_host": "mosquitto", "broker_port": 1883, "topic": "sensors/temp"}
def subscriber(node_id: str) -> NodeDef:
return NodeDef(
id=node_id,
type="mqtt",
params=dict(BROKER),
provides=[MessageSpec(name="temp", dtype=DType.FLOAT)],
)
@pytest.fixture
def controller(tmp_path: Path) -> FlowController:
store = FlowStore(tmp_path / "flows")
store.write_flow(
FlowDef(
name="house",
nodes=[
subscriber("sensor"),
NodeDef(
id="scale",
type="change",
requires=[MessageSpec(name="temp", dtype=DType.FLOAT)],
provides=[MessageSpec(name="scaled", dtype=DType.FLOAT)],
),
],
)
)
store.write_flow(FlowDef(name="shed", nodes=[subscriber("sensor")]))
return FlowController(store)
def test_two_flows_on_one_topic_are_one_neuron(controller: FlowController):
graph = controller.brain_graph()
merged = [node for node in graph.nodes if node.kind == "mqtt"]
assert len(merged) == 1
assert merged[0].id == "mqtt:mosquitto:1883/sensors/temp"
assert merged[0].members == ["house.sensor", "shed.sensor"]
assert merged[0].flows == ["house", "shed"]
def test_a_node_with_no_outside_thing_stays_its_own(controller: FlowController):
graph = controller.brain_graph()
scale = next(node for node in graph.nodes if node.kind == "change")
assert scale.id == "house.scale"
assert scale.members == ["house.scale"]
def test_an_edge_carries_the_qualified_message(controller: FlowController):
graph = controller.brain_graph()
assert [(edge.source, edge.target, edge.messages) for edge in graph.edges] == [
("mqtt:mosquitto:1883/sensors/temp", "house.scale", ["house.temp"])
]
def test_a_credential_never_reaches_the_key():
# Stored params, so a secret is still a reference. Neither its name nor its
# value belongs in something the browser gets to see.
key = MqttNode.instance_key({**BROKER, "password": {"$secret": "broker_pw"}})
assert key == "mosquitto:1883/sensors/temp"