Add fluksio status, and ask for a run's parameters at a terminal

Two halves of the same gap: the CLI could start work but not show you any.

`fluksio status` draws the home screen's top half in a terminal — health and
what is wrong with it, every flow with its state and node count, and the
recent runs and failures under them. `--watch` keeps it there. Rich does the
drawing; it was already installed under fastapi's own CLI, and is named now
because a command depends on it.

`fluksio run` with no parameters at a terminal asks for them, one line per
declared input with its declared value in brackets — so Enter through the lot
is what running the defaults looks like, and an artifact input takes the
`@run:` spelling the engine now resolves. A scripted run is untouched: passing
any parameter, or piping the command, skips the questions, as does --defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
This commit is contained in:
2026-08-25 07:42:09 +02:00
co-authored by Claude Opus 5
parent 93374a310e
commit 4941c2787c
12 changed files with 312 additions and 16 deletions
+3 -1
View File
@@ -149,7 +149,9 @@ def made_artifact():
def test_a_run_reference_resolves_to_what_that_run_produced(made_artifact):
run_id, reference = made_artifact
resolved = resolve_references(artifact_flow(), {"dataset": f"@run:{run_id}.dataset"})
resolved = resolve_references(
artifact_flow(), {"dataset": f"@run:{run_id}.dataset"}
)
# The producer's own reference, file name and all — not one rebuilt from
# the row, which carries the message name instead.
+1 -3
View File
@@ -314,9 +314,7 @@ def test_work_in_flight_is_touched_until_it_finishes(monkeypatch):
consumer.assign_flow("f", "consumer")
queue = RecordingQueue()
pipeline = Pipeline(
nodes=[source, consumer], state=MemoryState(), work_queue=queue
)
pipeline = Pipeline(nodes=[source, consumer], state=MemoryState(), work_queue=queue)
service = ExecutionService(queue)
service.bind(pipeline)
service.start()
-1
View File
@@ -443,4 +443,3 @@ def test_a_node_with_no_fingerprint_is_never_looked_up():
assert calls == [1]
assert cache.asked == []
assert seen[0].cache_key == ""
+50
View File
@@ -255,3 +255,53 @@ def test_ctrl_c_while_waiting_cancels_the_run(monkeypatch) -> None:
args = _parser().parse_args(["run", "train", "--local", "--no-sync"])
assert cli.cmd_run(args, []) == 130
assert cancelled == ["run-1"]
def test_an_artifact_input_may_be_named_rather_than_pasted() -> None:
"""The engine resolves either spelling; the CLI just stops mangling them."""
import json
from fluksio.sdk.cli import _coerce
assert _coerce("@run:123-abc.dataset", "artifact") == "@run:123-abc.dataset"
digest = "sha256:" + "a1" * 32
assert _coerce(digest, "artifact") == digest
# A reference a script already holds still arrives as the object it is.
reference = {"digest": digest, "size": 3}
assert _coerce(json.dumps(reference), "artifact") == reference
def test_the_run_prompt_keeps_declared_values_for_anything_left_blank() -> None:
"""Enter through the lot is what running with the defaults looks like."""
from unittest.mock import patch
from fluksio.sdk.cli import _ask_params
definition = {
"inputs": [
{"spec": {"name": "lr", "dtype": "float"}, "initial": 0.05},
{"spec": {"name": "epochs", "dtype": "int"}, "initial": 10},
]
}
with patch("builtins.input", side_effect=["", ""]):
# Nothing sent: an input the caller leaves out keeps what it declares,
# which is the same contract the browser's dialog has.
assert _ask_params(definition) == {}
with patch("builtins.input", side_effect=["0.01", "50"]):
assert _ask_params(definition) == {"lr": 0.01, "epochs": 50}
def test_the_run_prompt_names_an_answer_of_the_wrong_type() -> None:
from unittest.mock import patch
import pytest
from fluksio.sdk import SyncError
from fluksio.sdk.cli import _ask_params
definition = {"inputs": [{"spec": {"name": "lr", "dtype": "float"}}]}
with patch("builtins.input", side_effect=["fast"]):
with pytest.raises(SyncError, match="lr"):
_ask_params(definition)