New run: start a run from the app, on the working copy

The site promises simulated inputs and mocked sensor values, and nothing in
the app was that. A run already is: the values are the caller's, the state is
the run's own namespace, and nothing it computes reaches the live flow. What
was missing was a screen to do it from, and the draft flag being honoured.

`/runs/new` is a flow, a field per declared input, a seed and Run; `/runs`
stays the log. A comma-separated list in a number field expands into the grid
`fluksio sweep --param` builds and goes to the sweep route, so launching one
no longer needs a terminal. Only numbers split: a comma in a string is
content, and one in JSON is syntax.

`RunCreate.draft` was validated at submit and dropped before the run
executed, so "try the working copy" ran the published one. `Run.draft` is a
column now, the driver reads the same copy the submit checked, and a retry
carries it. `FlowSummary.mode` came with it so the rail can say which flows
are batch before one is picked.

Also here: a Retry button on a finished run, which the route has always had
and the UI never did, and parameter cells truncated to their column with the
full value on hover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TTfoK82awm8wvxXhHz3XF
This commit is contained in:
2026-09-02 15:10:24 +02:00
co-authored by Claude Opus 5
parent d471614e6a
commit 5b122341d5
20 changed files with 926 additions and 91 deletions
+56
View File
@@ -14,6 +14,7 @@ from sqlmodel import Session, col, select
from fluksio.core.config import settings
from fluksio.core.db import engine as db_engine
from fluksio.flow.artifacts import ArtifactStore
from fluksio.flow.controller import FlowController
from fluksio.flow.messages import DType, MessageSpec
from fluksio.flow.pipeline import NodeOutcome
from fluksio.flow.runs import (
@@ -27,6 +28,7 @@ from fluksio.flow.runs import (
seed_values,
)
from fluksio.flow.schemas import FlowDef, FlowInput, NodeDef
from fluksio.flow.store import FlowStore
from fluksio.models import Run, RunArtifact, RunMetric, RunNode
@@ -492,6 +494,56 @@ def test_the_seed_is_recorded_the_same_way_however_it_arrived():
session.commit()
DRAFT_SOURCE = "def process():\n return {'answer': 'draft'}\n"
PUBLISHED_SOURCE = "def process():\n return {'answer': 'published'}\n"
def test_a_draft_run_executes_the_draft(tmp_path):
"""The flag was checked at submit and forgotten by the time it ran.
Which made trying an edit before publishing it impossible from anywhere:
the submit validated the working copy and the driver then executed what
was published, and the two only agree when there is no draft.
"""
flow = FlowDef(
name="study",
mode="batch",
outputs=["answer"],
nodes=[
NodeDef(
id="answer",
provides=[MessageSpec(name="answer", dtype=DType.STR)],
)
],
)
store = FlowStore(tmp_path / "flows")
store.write_flow(flow)
store.write_node_source("study", "answer", PUBLISHED_SOURCE)
store.write_node_source("study", "answer", DRAFT_SOURCE, draft=True)
service = RunService(controller=FlowController(store), queue=_Collect())
made = []
try:
for draft, expected in ((True, "draft"), (False, "published")):
run = service.submit("study", {}, draft=draft)
made.append(run.id)
assert run.draft is draft
service._drive(run.id)
with Session(db_engine) as session:
stored = session.get(Run, run.id)
assert stored.status == "ok", stored.status_reason
assert stored.result == {"answer": expected}
finally:
with Session(db_engine) as session:
for run in session.exec(select(Run).where(col(Run.id).in_(made))).all():
session.delete(run)
for node in session.exec(
select(RunNode).where(col(RunNode.run_id).in_(made))
).all():
session.delete(node)
session.commit()
def test_a_retry_is_a_new_run_that_names_the_one_it_repeats():
"""The way back from a run an engine restart interrupted.
@@ -516,6 +568,7 @@ def test_a_retry_is_a_new_run_that_names_the_one_it_repeats():
status="abandoned",
params={"lr": 0.3},
group_id="sweep-9",
draft=True,
created_at=datetime.now(UTC),
)
)
@@ -537,6 +590,9 @@ def test_a_retry_is_a_new_run_that_names_the_one_it_repeats():
"sweep-9",
"retry",
)
# What it was a run of comes with it: retrying a run of the working
# copy that silently ran the published one would say nothing at all.
assert again.draft is True
assert queue.items[-1].run_id == again.id
# A run that has not finished is cancelled, not retried.