Keep a flow's runs when the flow goes

A run record is a record of what ran, so deleting a flow no longer sweeps its
history: `_forget_runs` is gone from the flow-delete path, and `DELETE /runs/{id}`
is the only thing that removes a run one at a time. The in-flight guard stays —
that is about work, not history.

`DELETE /runs?flow=` is the counterpart to the list's flow filter and what a
reseed needs, sharing the four statements with the single-run delete and
refusing the same way while a run of that flow is still going.

A run whose flow is gone reads as one: `useFlowInputs` reports the 404 rather
than an empty declaration set, so the run page says "flow deleted", explains it,
and disables Retry, which the route would refuse anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
This commit is contained in:
2026-09-06 14:53:23 +02:00
co-authored by Claude Opus 5
parent 062a2aac60
commit 15c3dd5838
12 changed files with 276 additions and 80 deletions
+44 -1
View File
@@ -774,6 +774,49 @@ def test_a_running_run_is_refused_rather_than_raced(client, superuser_token_head
session.commit()
def test_deleting_a_flows_runs_clears_that_flow_only(client, superuser_token_headers):
"""The counterpart to the list's `flow` filter — what a reseed needs.
Deleting the flow itself leaves the history standing, so this is the tool
that clears one. It refuses while a run of that flow is still going, for
the same reason deleting a single run does.
"""
made = datetime.now(UTC)
url = f"{settings.API_V1_STR}/runs"
with Session(db_engine) as session:
session.add(Run(id="bulk-1", flow="swept", status="ok", created_at=made))
session.add(RunNode(run_id="bulk-1", node="swept.a", status="ok"))
session.add(RunMetric(run_id="bulk-1", name="swept.loss", step=0, value=1.0))
session.add(Run(id="bulk-2", flow="swept", status="running", created_at=made))
session.add(Run(id="keep-1", flow="other", status="ok", created_at=made))
session.commit()
busy = client.delete(url, params={"flow": "swept"}, headers=superuser_token_headers)
assert busy.status_code == 409
assert "bulk-2" in busy.json()["detail"]
with Session(db_engine) as session:
session.get(Run, "bulk-2").status = "cancelled"
session.commit()
answer = client.delete(
url, params={"flow": "swept"}, headers=superuser_token_headers
)
assert answer.status_code == 200
assert "2" in answer.json()["message"]
with Session(db_engine) as session:
assert session.exec(select(Run).where(col(Run.flow) == "swept")).all() == []
for table in (RunNode, RunMetric):
assert (
session.exec(select(table).where(col(table.run_id) == "bulk-1")).all()
== []
)
assert session.get(Run, "keep-1") is not None
session.delete(session.get(Run, "keep-1"))
session.commit()
# -----------------------------------------------------------------------------
# A cached node's curve
#
@@ -847,7 +890,7 @@ def test_a_cached_node_answers_with_the_curve_it_was_restored_from(
def test_a_curve_whose_run_is_gone_is_empty_rather_than_an_error(
client, superuser_token_headers
):
"""Deleting a flow deletes its runs; what pointed at one is left holding it."""
"""Delete the run a cache hit points at and the reusing run holds nothing."""
with Session(db_engine) as session:
session.delete(session.get(Run, "src-1"))
session.commit()