Clients take a flow's id or key: one resolver in the SDK, id first, explicit id:/key:, a warning for reads and a refusal for changes when a name means two flows, deleted flows found by their last key; sync creates by POST, retries a create race through the drift checks and reports the id; MCP gets create_flow and resolves every flow argument with the caller's token; displays show flow_name; an engine from before ids is named as such

This commit is contained in:
2026-11-02 19:39:14 +01:00
parent fd637e0f1e
commit 53916d869b
20 changed files with 816 additions and 147 deletions
+62 -1
View File
@@ -7,6 +7,7 @@ from pathlib import Path
import pytest
from fluksio.cli import load_or_create_secret_key
from fluksio.sdk.resolve import Resolved
def test_importing_the_cli_does_not_build_the_settings() -> None:
@@ -242,6 +243,9 @@ def test_a_local_run_always_waits(monkeypatch) -> None:
return self
class FakeClient:
def resolve_flow(self, flow: str, **how: bool) -> Resolved:
return Resolved(flow, flow)
def get_flow(self, name: str) -> dict[str, object]:
return {"definition": {"inputs": []}}
@@ -264,7 +268,8 @@ def test_a_local_run_always_waits(monkeypatch) -> None:
assert cli.cmd_run(args, []) == 0
# "cli" rather than the SDK's default: the history says which asked.
assert submitted == {
"flow": "train",
# Resolved once, then named by id so the submit does not ask again.
"flow": "id:train",
"no_cache": True,
"waited": True,
"cause": "cli",
@@ -289,6 +294,9 @@ def test_ctrl_c_while_waiting_cancels_the_run(monkeypatch) -> None:
raise KeyboardInterrupt
class FakeClient:
def resolve_flow(self, flow: str, **how: bool) -> Resolved:
return Resolved(flow, flow)
def get_flow(self, name: str) -> dict[str, object]:
return {"definition": {"inputs": []}}
@@ -429,6 +437,9 @@ def test_a_failed_run_prints_what_each_node_said(monkeypatch, capsys) -> None:
return self
class FakeClient:
def resolve_flow(self, flow: str, **how: bool) -> Resolved:
return Resolved(flow, flow)
def get_flow(self, name: str) -> dict[str, object]:
return {"definition": {"inputs": []}}
@@ -699,6 +710,9 @@ def test_run_imports_the_module_that_declared_the_flow(tmp_path, monkeypatch):
class Engine:
origin: dict = {}
def resolve_flow(self, flow, **how):
return Resolved(flow, flow)
def get_flow(self, name):
return {"definition": {"origin": self.origin}}
@@ -1196,3 +1210,50 @@ def test_the_dashboard_subscribes_where_the_browser_does() -> None:
assert socket_url("http://127.0.0.1:8000") == "ws://127.0.0.1:8000/api/v1/flows/ws"
# TLS on the one side is TLS on the other.
assert socket_url("https://engine.example.com").startswith("wss://")
def test_a_flow_argument_goes_through_the_resolver(monkeypatch, capsys) -> None:
"""A run of a name two flows answer to is refused; a listing warns."""
from contextlib import contextmanager
import httpx
from fluksio.cli import _parser
from fluksio.sdk import cli
from fluksio.sdk.client import Client
listed = [
{"id": "heating", "name": "warmth"},
{"id": "f_heat0000", "name": "heating"},
]
asked: list[str] = []
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/api/v1/flows/":
return httpx.Response(200, json={"data": listed, "count": 2})
if request.url.path == "/api/v1/runs/overview":
return httpx.Response(200, json=[])
asked.append(f"{request.url.path}?{request.url.query.decode()}")
return httpx.Response(200, json=[])
@contextmanager
def engine():
http = httpx.Client(transport=httpx.MockTransport(handler), base_url="http://e")
yield Client(http=http, token="t")
monkeypatch.setattr(cli, "_engine_client", engine)
parser = _parser()
run = parser.parse_args(["run", "heating", "--local", "--no-sync"])
assert cli.cmd_run(run, []) == 1
assert "Pass id:heating or key:heating" in capsys.readouterr().err
assert asked == []
assert (
cli.cmd_runs(parser.parse_args(["runs", "--flow", "heating", "--local"])) == 0
)
assert (
"fluksio: warning: 'heating' is the id of 'warmth' and the key of "
"'f_heat0000'. Using the id. Pass key:heating for the other.\n"
) in capsys.readouterr().err
assert asked == ["/api/v1/runs?limit=20&flow=heating"]