Give the controller a per-flow rebuild, not yet called by anything
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StpRc2C6au1WJ1EUU7fsfu
This commit is contained in:
@@ -10,11 +10,16 @@ from fastapi.testclient import TestClient
|
||||
|
||||
from fluksio.api.deps import get_current_user
|
||||
from fluksio.api.routes.flows import router
|
||||
from fluksio.flow.controller import FlowController, LoadedNode
|
||||
from fluksio.flow.controller import (
|
||||
NODE_TYPES,
|
||||
FlowController,
|
||||
LoadedNode,
|
||||
NodeType,
|
||||
)
|
||||
from fluksio.flow.messages import DType, MessageSpec
|
||||
from fluksio.flow.nodes import Node
|
||||
from fluksio.flow.pipeline import Pipeline
|
||||
from fluksio.flow.schemas import FlowDef
|
||||
from fluksio.flow.schemas import FlowDef, FlowInput, NodeDef
|
||||
from fluksio.flow.store import FlowStore
|
||||
|
||||
|
||||
@@ -198,3 +203,97 @@ def test_a_node_that_will_not_stop_does_not_wedge_the_rebuild(
|
||||
await engine.reload()
|
||||
|
||||
asyncio.run(asyncio.wait_for(scenario(), timeout=5))
|
||||
|
||||
|
||||
class _Lifecycle(Node):
|
||||
"""A node type that writes down every lifecycle call a rebuild makes."""
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(f=lambda params: None, **kwargs)
|
||||
|
||||
async def start(self, app: FastAPI | None = None) -> None:
|
||||
_Lifecycle.calls.append(f"start {self.id}")
|
||||
|
||||
async def stop(self, app: FastAPI | None = None) -> None:
|
||||
_Lifecycle.calls.append(f"stop {self.id}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lifecycle(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
||||
"""A node type on the registry, and the log of what it was asked to do."""
|
||||
monkeypatch.setitem(
|
||||
NODE_TYPES,
|
||||
"lifecycle",
|
||||
NodeType(
|
||||
title="Lifecycle", description="Counts start and stop.", cls=_Lifecycle
|
||||
),
|
||||
)
|
||||
_Lifecycle.calls.clear()
|
||||
return _Lifecycle.calls
|
||||
|
||||
|
||||
def test_rebuilding_one_flow_leaves_another_flows_node_running(
|
||||
tmp_path: Path, lifecycle: list[str]
|
||||
):
|
||||
"""The whole point: reconnecting one flow's nodes, not the installation's."""
|
||||
store = FlowStore(tmp_path / "flows")
|
||||
for name in ("a", "b"):
|
||||
store.write_flow(FlowDef(name=name, nodes=[NodeDef(id="io", type="lifecycle")]))
|
||||
engine = FlowController(store)
|
||||
|
||||
async def scenario() -> None:
|
||||
await engine.reload()
|
||||
untouched = engine.loaded["b.io"].node
|
||||
lifecycle.clear()
|
||||
|
||||
await engine.reload_flow("a")
|
||||
|
||||
# Not rebuilt, not restarted, and the very same object still bound.
|
||||
assert engine.loaded["b.io"].node is untouched
|
||||
assert lifecycle == ["stop a.io", "start a.io"]
|
||||
assert engine.loaded["a.io"].node is not None
|
||||
|
||||
asyncio.run(asyncio.wait_for(scenario(), timeout=10))
|
||||
|
||||
|
||||
def test_a_deleted_flow_leaves_its_consumers_reporting_a_missing_input(
|
||||
tmp_path: Path, lifecycle: list[str]
|
||||
):
|
||||
"""A flow is not a subgraph, so deleting one is another flow's problem."""
|
||||
store = FlowStore(tmp_path / "flows")
|
||||
store.write_flow(
|
||||
FlowDef(
|
||||
name="a",
|
||||
nodes=[NodeDef(id="meter", type="lifecycle", provides=[spec("temp")])],
|
||||
inputs=[FlowInput(spec=spec("spare"), initial=1.0)],
|
||||
)
|
||||
)
|
||||
store.write_flow(
|
||||
FlowDef(
|
||||
name="b",
|
||||
nodes=[
|
||||
NodeDef(id="load", type="lifecycle", requires=[spec("a.temp")]),
|
||||
NodeDef(id="watch", type="lifecycle", requires=[spec("a.spare")]),
|
||||
],
|
||||
)
|
||||
)
|
||||
engine = FlowController(store)
|
||||
|
||||
async def scenario() -> None:
|
||||
await engine.reload()
|
||||
assert engine.issues == []
|
||||
|
||||
store.delete_flow("a")
|
||||
await engine.reload_flow("a")
|
||||
|
||||
# Both the producer and the declared input went with the flow.
|
||||
assert sorted((issue.code, issue.node) for issue in engine.issues) == [
|
||||
("unconnected_input", "b.load"),
|
||||
("unconnected_input", "b.watch"),
|
||||
]
|
||||
assert engine.pipeline is not None
|
||||
assert engine.pipeline.get_node_by_id("a.meter") is None
|
||||
|
||||
asyncio.run(asyncio.wait_for(scenario(), timeout=10))
|
||||
|
||||
Reference in New Issue
Block a user