diff --git a/backend/tests/flow/test_workers.py b/backend/tests/flow/test_workers.py index 02c57e1..a06b0ed 100644 --- a/backend/tests/flow/test_workers.py +++ b/backend/tests/flow/test_workers.py @@ -9,10 +9,14 @@ import pytest from fluksio_worker.worker_main import ARTIFACT_DIR_ENV from fluksio.flow.artifacts import ArtifactStore -from fluksio.flow.controller import EmitSink +from fluksio.flow.controller import EmitSink, FlowController from fluksio.flow.messages import DType, MessageSpec from fluksio.flow.nodes import Node from fluksio.flow.nodes.base import NodeOutputError +from fluksio.flow.pipeline import Pipeline +from fluksio.flow.schemas import FlowDef, NodeDef +from fluksio.flow.state import MemoryState +from fluksio.flow.store import FlowStore from fluksio.flow.workers import NodeCancelled, NodeTimeout, PythonWorkerPool @@ -248,6 +252,49 @@ def test_a_generator_node_publishes_each_yield_and_returns_the_end(pool): assert {event["call_id"] for event in seen} == {"r1:demo.train"} +def test_a_node_the_controller_built_reclassifies_its_last_yield(pool, tmp_path): + """The sink only reaches a node if `_build_node` wrapped its function with it. + + Without that, a generator node's last yield is lost and its result is None — + which the pieces of this checked one at a time cannot see. + """ + store = FlowStore(tmp_path / "flows") + store.write_flow( + FlowDef( + name="study", + mode="batch", + nodes=[ + NodeDef( + id="train", + provides=[MessageSpec(name="loss", dtype=DType.FLOAT, stream=True)], + ) + ], + ) + ) + store.write_node_source( + "study", + "train", + "def process():\n" + " yield {'loss': 1.0}\n" + " yield {'loss': 2.0}\n" + " yield {'loss': 3.0}\n", + ) + controller = FlowController(store, workers=pool) + nodes, _loaded, _initial, _inputs = controller._build_flows( + [(store.read_flow("study"), False)] + ) + emitted: list[dict] = [] + pipeline = Pipeline( + nodes=nodes, + state=MemoryState(), + emission_observer=lambda _node, outputs: emitted.append(outputs), + ) + pipeline.run() + + assert [e["study.loss"] for e in emitted] == [1.0, 2.0] + assert pipeline.state["study.loss"] == 3.0 + + def test_without_a_return_the_last_yield_is_the_result(pool): """The worker sends every yield; the engine is what holds the last one back.""" node, published = _sink_node("count", "out", DType.INT)